Files
route-commerce/src/app/admin/water-log/headgates/HeadgatesManager.tsx
T
Nora 29d9d23a26 fix: react-doctor → 64/100 (Bugs 122, Perf 286, A11y 613, Maint 436)
- CartContext: lazy initializers replace mount-only useEffect
  hydration; remove 8 no-initialize-state warnings
- Toast/AdminSearchInput: React 19 useContext/use + drop
  forwardRef (3 no-react19-deprecated-apis)
- ProductFormModal: lazy initializers + useSyncExternalStore
  for mount; parent adds key=editingProduct.id
- InstallPrompt: useReducer for prompt state (no-cascading-set-state)
- QRScanModal: ref-based latest-callback pattern replaces
  useEffectEvent deps mistake
- OnboardingFlow: functional setState (rerender-functional-setstate)
- UsersPage/StopsCalendar/FeaturesAndStats: lazy initializers
  (rerender-lazy-state-init)
- FAQClientPage: server-side brand settings fetch via getBrandSettingsPublic
  in layout; remove supabase import
- LandingPageWrapper: href='#' → href='#top' (anchor-is-valid)
- TuxedoVideoHero: replace animate-bounce with ease-out-expo
  (no-inline-bounce-easing)
- ProductTableClient: useCallback for handleDeleted
  (jsx-no-new-function-as-prop)
- excel-parser: pre-compile delimiter regexes (js-hoist-regexp)
- water-log/settings: Promise.all for parallel DB calls
  (async-parallel)
- ToastNotification: extract toast store to separate file
  (only-export-components)
- WholesaleClient: inline <WholesaleIcon/> instead of hoisting to
  const (rendering-hoist-jsx)
2026-06-26 02:41:56 -06:00

604 lines
27 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import {
getWaterHeadgatesAdmin,
createWaterHeadgate,
regenerateHeadgateToken,
updateWaterHeadgate,
deleteWaterHeadgate,
} from "@/actions/water-log/admin";
import { AdminButton } from "@/components/admin/design-system";
type Headgate = {
id: string;
name: string;
active: boolean;
unit: string;
created_at: string;
deleted_at?: string | null;
headgate_token?: string | null;
last_used_at?: string | null;
high_threshold?: number | null;
low_threshold?: number | null;
};
type Props = {
initialHeadgates: Headgate[];
brandId: string;
};
const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
export default function HeadgatesManager({ initialHeadgates, brandId }: Props) {
const router = useRouter();
const [headgates, setHeadgates] = useState<Headgate[]>(initialHeadgates);
const [showAdd, setShowAdd] = useState(false);
const [newName, setNewName] = useState("");
const [newUnit, setNewUnit] = useState("CFS");
const [saving, setSaving] = useState(false);
const [qrModal, setQrModal] = useState<Headgate | null>(null);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [printLoading, setPrintLoading] = useState(false);
const [toast, setToast] = useState<{ msg: string; ok: boolean } | null>(null);
// Edit modal state
const [editHg, setEditHg] = useState<Headgate | null>(null);
const [editName, setEditName] = useState("");
const [editUnit, setEditUnit] = useState("CFS");
const [editActive, setEditActive] = useState(true);
const [editHigh, setEditHigh] = useState("");
const [editLow, setEditLow] = useState("");
const [editSaving, setEditSaving] = useState(false);
function showToast(msg: string, ok = true) {
setToast({ msg, ok });
setTimeout(() => setToast(null), 2500);
}
function openEdit(hg: Headgate) {
setEditHg(hg);
setEditName(hg.name);
setEditUnit(hg.unit);
setEditActive(hg.active);
setEditHigh(hg.high_threshold != null ? String(hg.high_threshold) : "");
setEditLow(hg.low_threshold != null ? String(hg.low_threshold) : "");
}
async function handleEditSave(e: React.FormEvent) {
e.preventDefault();
if (!editHg) return;
setEditSaving(true);
const res = await updateWaterHeadgate(
editHg.id,
editName,
editActive,
editUnit,
editHigh ? parseFloat(editHigh) : null,
editLow ? parseFloat(editLow) : null
);
if (res.success) {
const refreshed = await getWaterHeadgatesAdmin(brandId);
setHeadgates(refreshed);
setEditHg(null);
showToast("Headgate updated");
} else {
showToast(res.error ?? "Failed", false);
}
setEditSaving(false);
}
async function handleAdd(e: React.FormEvent) {
e.preventDefault();
if (!newName.trim()) return;
setSaving(true);
const res = await createWaterHeadgate(brandId, newName.trim(), newUnit);
if (res.success) {
const refreshed = await getWaterHeadgatesAdmin(brandId);
setHeadgates(refreshed);
setNewName("");
setShowAdd(false);
showToast("Headgate added");
} else {
showToast(res.error ?? "Failed", false);
}
setSaving(false);
}
async function handleRegenerate(hg: Headgate) {
if (!confirm(`Regenerate token for "${hg.name}"? Existing QR codes will stop working.`)) return;
const res = await regenerateHeadgateToken(hg.id);
if (res.success) {
setHeadgates((prev) =>
prev.map((h) => (h.id === hg.id ? { ...h, headgate_token: res.token! } : h))
);
showToast("Token regenerated");
} else {
showToast(res.error ?? "Failed", false);
}
}
const [deletingId, setDeletingId] = useState<string | null>(null);
async function handleDelete(hg: Headgate) {
if (!window.confirm(`Delete headgate "${hg.name}"? Existing log entries will be preserved.`)) return;
setDeletingId(hg.id);
const res = await deleteWaterHeadgate(hg.id);
if (res.success) {
// Optimistic update + refresh
setHeadgates((prev) => prev.filter((h) => h.id !== hg.id));
const refreshed = await getWaterHeadgatesAdmin(brandId);
setHeadgates(refreshed);
showToast("Headgate deleted");
} else {
showToast(res.error ?? "Failed to delete headgate", false);
}
setDeletingId(null);
}
function toggleSelect(id: string) {
setSelected((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}
function toggleAll() {
if (selected.size === headgates.length) {
setSelected(new Set());
} else {
setSelected(new Set(headgates.map((h) => h.id)));
}
}
async function printSelected() {
if (selected.size === 0) return;
setPrintLoading(true);
const hgs = headgates.filter((h) => selected.has(h.id));
try {
const res = await fetch("/api/water-qr-sheet", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ headgates: hgs, baseUrl: BASE_URL }),
});
const html = await res.text();
openPrintWindow(html);
} finally {
setPrintLoading(false);
}
}
async function openPrintWindow(html: string) {
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob);
const w = window.open(url, "_blank", "width=700,height=700");
if (w) {
// Revoke the blob URL once the new window has had time to load the document.
w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
// Safety net: revoke after 60s even if the load event never fires.
setTimeout(() => URL.revokeObjectURL(url), 60_000);
} else {
URL.revokeObjectURL(url);
}
}
return (
<div className="min-h-screen bg-[var(--admin-bg)]">
{/* Header */}
<div className="bg-white border-b border-[var(--admin-border)] px-6 py-4">
<div className="mx-auto max-w-4xl flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<button type="button" onClick={() => router.back()} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] text-sm"> Back</button>
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">Headgates & QR Codes</h1>
</div>
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">Manage headgates and generate QR codes for field access</p>
</div>
<AdminButton onClick={() => setShowAdd(true)}>
+ Add Headgate
</AdminButton>
</div>
</div>
<div className="mx-auto max-w-4xl px-6 py-6 space-y-6">
{/* Bulk actions bar */}
{headgates.length > 0 && (
<div className="flex items-center gap-3 rounded-xl border border-[var(--admin-border)] bg-white px-4 py-3">
<button type="button" onClick={toggleAll} className="text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
{selected.size === headgates.length ? "Deselect all" : "Select all"}
</button>
<span className="text-xs text-[var(--admin-text-muted)]">|</span>
<span className="text-xs text-[var(--admin-text-muted)]">{selected.size} selected</span>
<div className="flex-1" />
<AdminButton
onClick={printSelected}
disabled={selected.size === 0}
isLoading={printLoading}
>
Print {selected.size > 0 ? `(${selected.size})` : ""} QR Codes
</AdminButton>
</div>
)}
{/* Add form */}
{showAdd && (
<div className="rounded-xl border border-[var(--admin-border)] bg-white p-5">
<form onSubmit={handleAdd} className="flex gap-3 items-end">
<div className="flex-1">
<label htmlFor="headgate-add-name" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Headgate Name</label>
<input aria-label=". North Field Gate 1"
id="headgate-add-name"
autoFocus
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
placeholder="e.g. North Field Gate 1"
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
required
aria-required="true"
/>
</div>
<div>
<label htmlFor="headgate-add-unit" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
<select aria-label="Headgate Add Unit"
id="headgate-add-unit"
value={newUnit}
onChange={(e) => setNewUnit(e.target.value)}
className="rounded-xl border border-[var(--admin-border)] px-3 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
>
{["CFS", "GPM", "Inches", "AF/Day"].map((u) => (
<option key={u} value={u}>{u}</option>
))}
</select>
</div>
<AdminButton type="submit" disabled={saving} isLoading={saving}>
Create
</AdminButton>
<AdminButton type="button" variant="secondary" onClick={() => setShowAdd(false)}>
Cancel
</AdminButton>
</form>
</div>
)}
{/* Table */}
{headgates.length === 0 ? (
<div className="rounded-xl bg-white py-16 text-center text-[var(--admin-text-muted)] border border-[var(--admin-border)]">
No headgates yet. Create one to get started.
</div>
) : (
<div className="rounded-xl border border-[var(--admin-border)] bg-white overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--admin-border)] bg-[var(--admin-bg-subtle)] text-left">
<th className="px-4 py-3 w-8">
<input aria-label="Checkbox" type="checkbox" className="rounded" onChange={toggleAll} checked={selected.size === headgates.length} />
</th>
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Name</th>
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Token</th>
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Unit</th>
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Last Used</th>
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)]">Status</th>
<th className="px-4 py-3 text-xs font-semibold text-[var(--admin-text-muted)] text-right">QR Code</th>
</tr>
</thead>
<tbody>
{headgates.map((hg) => (
<tr key={hg.id} className={`border-b border-[var(--admin-border)] last:border-0 hover:bg-[var(--admin-bg-subtle)] ${selected.has(hg.id) ? "bg-blue-50" : ""}`}>
<td className="px-4 py-3">
<input aria-label="Checkbox" type="checkbox" className="rounded" checked={selected.has(hg.id)} onChange={() => toggleSelect(hg.id)} />
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-semibold text-[var(--admin-text-primary)]">{hg.name}</span>
{hg.high_threshold != null && (
<span className="inline-flex items-center rounded-full bg-red-100 border border-red-200 px-1.5 py-0.5 text-xs font-semibold text-red-700">
High: {hg.high_threshold}
</span>
)}
{hg.low_threshold != null && (
<span className="inline-flex items-center rounded-full bg-blue-100 border border-blue-200 px-1.5 py-0.5 text-xs font-semibold text-blue-700">
Low: {hg.low_threshold}
</span>
)}
</div>
</td>
<td className="px-4 py-3">
<code className="text-xs text-[var(--admin-text-muted)]">{hg.headgate_token?.slice(0, 8)}</code>
</td>
<td className="px-4 py-3">
<span className="text-xs text-[var(--admin-text-muted)]">{hg.unit}</span>
</td>
<td className="px-4 py-3">
<span className="text-xs text-[var(--admin-text-muted)]">
{hg.last_used_at
? new Date(hg.last_used_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })
: "Never"}
</span>
</td>
<td className="px-4 py-3">
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs font-semibold ${hg.active ? "bg-green-100 text-green-700" : "bg-stone-100 text-stone-500"}`}>
{hg.active ? "Active" : "Inactive"}
</span>
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-2">
<AdminButton variant="secondary" size="sm" onClick={() => openEdit(hg)}>
Edit
</AdminButton>
{/* QR preview thumbnail */}
<button type="button" onClick={() => setQrModal(hg)} className="hover:opacity-80" title="View / Print QR">
<Image
src={`/api/water-qr?token=${hg.headgate_token}&size=80`}
alt={`QR for ${hg.name}`}
width={40}
height={40}
unoptimized
className="w-10 h-10 rounded border border-[var(--admin-border)]"
/>
</button>
<AdminButton variant="secondary" size="sm" onClick={() => setQrModal(hg)}>
Print
</AdminButton>
<AdminButton
variant="danger"
size="sm"
onClick={() => handleDelete(hg)}
disabled={deletingId === hg.id}
isLoading={deletingId === hg.id}
>
Delete
</AdminButton>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Toast */}
{toast && (
<div className={`fixed bottom-6 right-6 rounded-xl px-4 py-3 text-sm font-semibold shadow-lg ${toast.ok ? "bg-green-100 text-green-800 border border-green-200" : "bg-red-100 text-red-800 border border-red-200"}`}>
{toast.msg}
</div>
)}
{/* Edit Modal */}
{editHg && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setEditHg(null)}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
<p className="font-bold text-[var(--admin-text-primary)]">Edit Headgate</p>
<button type="button" onClick={() => setEditHg(null)} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleEditSave} className="p-5 space-y-4">
<div>
<label htmlFor="headgate-edit-name" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Name</label>
<input aria-label="Headgate Edit Name"
id="headgate-edit-name"
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
required
aria-required="true"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label htmlFor="headgate-edit-unit" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Unit</label>
<select aria-label="Headgate Edit Unit"
id="headgate-edit-unit"
value={editUnit}
onChange={(e) => setEditUnit(e.target.value)}
className="w-full rounded-xl border border-[var(--admin-border)] px-3 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
>
{["CFS", "GPM", "Inches", "AF/Day"].map((u) => (
<option key={u} value={u}>{u}</option>
))}
</select>
</div>
<div className="flex items-center">
<label className="flex items-center gap-2 text-sm text-[var(--admin-text-secondary)] cursor-pointer">
<input id="headgate-edit-active" type="checkbox" checked={editActive} onChange={(e) => setEditActive(e.target.checked)} className="rounded" />
Active
</label>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label htmlFor="headgate-edit-high" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">High Alert Threshold</label>
<input aria-label=". 15.0"
id="headgate-edit-high"
type="number"
step="any"
value={editHigh}
onChange={(e) => setEditHigh(e.target.value)}
placeholder="e.g. 15.0"
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
/>
</div>
<div>
<label htmlFor="headgate-edit-low" className="block text-xs font-semibold text-[var(--admin-text-muted)] mb-1">Low Alert Threshold</label>
<input aria-label=". 5.0"
id="headgate-edit-low"
type="number"
step="any"
value={editLow}
onChange={(e) => setEditLow(e.target.value)}
placeholder="e.g. 5.0"
className="w-full rounded-xl border border-[var(--admin-border)] px-4 py-3 text-sm outline-none focus:border-[var(--admin-accent)]"
/>
</div>
</div>
<p className="text-xs text-[var(--admin-text-muted)]">Leave thresholds blank to disable alerts for this headgate.</p>
<div className="flex gap-2">
<AdminButton type="submit" disabled={editSaving} isLoading={editSaving} fullWidth>
Save Changes
</AdminButton>
<AdminButton type="button" variant="secondary" onClick={() => setEditHg(null)}>
Cancel
</AdminButton>
</div>
</form>
</div>
</div>
)}
{/* QR Modal */}
{qrModal && (
<QRModal hg={qrModal} onClose={() => setQrModal(null)} onRegenerate={handleRegenerate} />
)}
</div>
);
}
function QRModal({ hg, onClose, onRegenerate }: { hg: Headgate; onClose: () => void; onRegenerate: (hg: Headgate) => void }) {
const [tab, setTab] = useState<"preview" | "print" | "download">("preview");
const [loading, setLoading] = useState(false);
const code = hg.name.replace(/\s+/g, "-").toUpperCase().slice(0, 6) + "-1";
const qrUrl = `/api/water-qr?token=${hg.headgate_token}&size=360`;
const waterUrl = `${process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"}/water?h=${hg.headgate_token}`;
async function handlePrintLabel() {
setLoading(true);
try {
const res = await fetch("/api/water-qr-label", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: hg.headgate_token, name: hg.name }),
});
const html = await res.text();
const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob);
const w = window.open(url, "_blank", "width=500,height=600");
if (w) {
w.addEventListener("load", () => URL.revokeObjectURL(url), { once: true });
setTimeout(() => URL.revokeObjectURL(url), 60_000);
} else {
URL.revokeObjectURL(url);
}
} finally {
setLoading(false);
}
}
async function handleDownload() {
setLoading(true);
try {
const res = await fetch(`/api/water-qr?token=${hg.headgate_token}&size=400`);
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${hg.name.replace(/\s+/g, "-").toLowerCase()}-qr.png`;
a.click();
URL.revokeObjectURL(url);
} finally {
setLoading(false);
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden border border-[var(--admin-border)]" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[var(--admin-border)]">
<div>
<p className="font-bold text-[var(--admin-text-primary)]">{hg.name}</p>
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">QR Label Preview</p>
</div>
<button type="button" onClick={onClose} className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* ── Simplified label preview ── */}
<div className="flex justify-center bg-[var(--admin-bg-subtle)] py-6 px-4">
{/* Mini version of the new label design */}
<div className="w-40 border-2 border-black rounded-lg p-3 text-center bg-white shadow-sm">
<div className="text-xl font-black text-black leading-tight tracking-tight">{hg.name}</div>
<div className="text-[9px] font-bold text-gray-500 uppercase tracking-widest mt-0.5 mb-3">{code}</div>
<div className="flex justify-center mb-2">
<Image
src={`/api/water-qr?token=${hg.headgate_token}&size=160`}
alt="QR"
width={112}
height={112}
unoptimized
className="w-28 h-28 rounded"
/>
</div>
<div className="text-[5.5px] text-gray-500 leading-tight break-all">{waterUrl}</div>
</div>
</div>
{/* Tab toggle */}
<div className="flex border-b border-[var(--admin-border)]">
{(["preview", "print", "download"] as const).map((t) => (
<button type="button"
key={t}
onClick={() => setTab(t)}
className={`flex-1 py-3 text-sm font-semibold transition-colors ${tab === t ? "text-[var(--admin-text-primary)] border-b-2 border-[var(--admin-accent)]" : "text-[var(--admin-text-muted)]"}`}
>
{t === "preview" ? "Preview" : t === "print" ? "🖨️ Print" : "💾 Download"}
</button>
))}
</div>
<div className="p-5 space-y-3">
{tab === "preview" ? (
<div className="space-y-2 text-sm text-[var(--admin-text-secondary)]">
<p>This label is optimized for physical signs and thermal printing:</p>
<ul className="list-disc list-inside space-y-1 text-xs text-[var(--admin-text-muted)]">
<li><strong>Large name</strong> (24pt bold) scannable from distance</li>
<li><strong>Short code</strong> (e.g. <code className="font-mono bg-[var(--admin-bg-subtle)] px-1 rounded">{code}</code>) field reference</li>
<li><strong>High-contrast QR</strong> error correction H for outdoor use</li>
<li><strong>Tiny URL</strong> at bottom reference only</li>
</ul>
<AdminButton onClick={() => setTab("print")} fullWidth>
🖨 Print This Label
</AdminButton>
</div>
) : tab === "print" ? (
<AdminButton onClick={handlePrintLabel} disabled={loading} isLoading={loading} fullWidth>
🖨 Print Label
</AdminButton>
) : (
<AdminButton onClick={handleDownload} disabled={loading} isLoading={loading} fullWidth>
💾 Download PNG
</AdminButton>
)}
<AdminButton
onClick={() => { onRegenerate(hg); onClose(); }}
variant="secondary"
fullWidth
>
🔄 Regenerate Token
</AdminButton>
<div className="rounded-lg border border-[var(--admin-border)] p-3">
<p className="text-xs text-[var(--admin-text-muted)] mb-1">Token</p>
<code className="text-xs text-[var(--admin-text-secondary)] font-mono break-all">{hg.headgate_token}</code>
</div>
</div>
</div>
</div>
);
}