feat(ops): health-fail alerts, optional DB backup, admin UX polish
Wire jobs-worker health probes to Discord/email alerts with cooldown, optional mysqldump, rate-limit /api/health, mark-all-read alerts, ConfirmDialog on destructive admin actions, and raise coverage floors. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
3bd712e744
commit
725e1cb338
16 files changed
+327
-32
No files matched your search
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useTransition } from "react";
|
||||
import { deleteArticle } from "@/actions/admin-articles";
|
||||
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
||||
|
||||
export function ArticleCard({
|
||||
id,
|
||||
@@ -18,9 +21,29 @@ export function ArticleCard({
|
||||
author?: string;
|
||||
}) {
|
||||
const t = useTranslations("pages.admin.articles");
|
||||
const router = useRouter();
|
||||
const [pending, startTransition] = useTransition();
|
||||
const { confirm, dialog: confirmDialog } = useConfirmDialog();
|
||||
|
||||
async function handleDelete() {
|
||||
const ok = await confirm({
|
||||
title: t("delete"),
|
||||
description: t("confirmDelete", { title }),
|
||||
confirmLabel: t("delete"),
|
||||
variant: "danger",
|
||||
});
|
||||
if (!ok) return;
|
||||
startTransition(async () => {
|
||||
const fd = new FormData();
|
||||
fd.set("id", id);
|
||||
await deleteArticle(fd);
|
||||
router.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="admin-card overflow-hidden flex flex-col">
|
||||
{confirmDialog}
|
||||
<div className="relative w-full aspect-[16/9] bg-[var(--admin-canvas)]">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
@@ -56,20 +79,14 @@ export function ArticleCard({
|
||||
>
|
||||
{t("edit")}
|
||||
</Link>
|
||||
<form action={deleteArticle} className="m-0">
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-danger"
|
||||
onClick={(e) => {
|
||||
if (!window.confirm(t("confirmDelete", { title }))) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
disabled={pending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{t("delete")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { deleteMedia, uploadMedia } from "@/actions/admin-media";
|
||||
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
|
||||
type MediaFile = {
|
||||
@@ -32,6 +33,7 @@ function extOf(name: string): string {
|
||||
|
||||
export function AdminMediaGrid() {
|
||||
const t = useTranslations("pages.admin.media");
|
||||
const { confirm, dialog: confirmDialog } = useConfirmDialog();
|
||||
const [files, setFiles] = useState<MediaFile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
@@ -103,7 +105,13 @@ export function AdminMediaGrid() {
|
||||
}
|
||||
|
||||
async function remove(name: string) {
|
||||
if (!window.confirm(t("confirmDelete"))) return;
|
||||
const ok = await confirm({
|
||||
title: t("delete"),
|
||||
description: t("confirmDelete"),
|
||||
confirmLabel: t("delete"),
|
||||
variant: "danger",
|
||||
});
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteMedia(name);
|
||||
setFiles((prev) => prev.filter((f) => f.name !== name));
|
||||
@@ -115,6 +123,7 @@ export function AdminMediaGrid() {
|
||||
|
||||
return (
|
||||
<div className="admin-card">
|
||||
{confirmDialog}
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
<label className="btn btn-primary cursor-pointer text-sm">
|
||||
|
||||
Reference in new issue
Block a user