feat: global Settings page for AbleSign + Cloud Cam account config

- New /admin/settings page with AbleSign account setup (API key) and
  link to Cloud Cams config
- Settings nav item in sidebar (gear icon, before Account)
- Removed AbleSign Config from AbleSign dropdown (now in Settings)
- AbleSign account delete redirects to Settings
- Cloud Cams nav item kept for its own CRUD page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mitchell R 2026-05-27 02:47:54 +02:00
parent f0088836e9
commit d6a52df27a
4 changed files with 78 additions and 3 deletions

View file

@ -217,7 +217,7 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
app.post("/admin/ablesign/:id/delete", async (event) => { app.post("/admin/ablesign/:id/delete", async (event) => {
const id = getRouterParam(event, "id") ?? ""; const id = getRouterParam(event, "id") ?? "";
await deps.repo.deleteAbleSignAccount(id); await deps.repo.deleteAbleSignAccount(id);
return new Response(null, { status: 302, headers: { location: "/admin/ablesign" } }); return new Response(null, { status: 302, headers: { location: "/admin/settings" } });
}); });
app.post("/admin/ablesign/screens/:sid/delete", async (event) => { app.post("/admin/ablesign/screens/:sid/delete", async (event) => {

View file

@ -34,6 +34,7 @@ import {
renderKioskLabels, renderKioskLabels,
renderDisplayLayouts, renderDisplayLayouts,
renderDefaultLayoutSelect, renderDefaultLayoutSelect,
SettingsPage,
} from "../../web-templates/admin-pages.js"; } from "../../web-templates/admin-pages.js";
import { discover as onvifDiscover, getEventProperties as onvifGetEventProperties } from "../../shared/onvif.js"; import { discover as onvifDiscover, getEventProperties as onvifGetEventProperties } from "../../shared/onvif.js";
import { generateBundle } from "../../shared/bundle.js"; import { generateBundle } from "../../shared/bundle.js";
@ -2219,6 +2220,14 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
return new Response(null, { status: 302, headers: { location: `/admin/kiosks/${id}` } }); return new Response(null, { status: 302, headers: { location: `/admin/kiosks/${id}` } });
}); });
// ---- Settings page ----------------------------------------------------------
app.get("/admin/settings", async () => {
const cloudAccounts = await deps.repo.listCloudAccounts();
const ablesignAccounts = await deps.repo.listAbleSignAccounts();
return htmlPage(SettingsPage({ cloudAccounts, ablesignAccounts }));
});
// ---- Tenant switcher fragment (htmx) ---------------------------------------- // ---- Tenant switcher fragment (htmx) ----------------------------------------
app.get("/admin/_tenant_switcher", async (event) => { app.get("/admin/_tenant_switcher", async (event) => {
const tenants = await deps.repo.listTenants(); const tenants = await deps.repo.listTenants();

View file

@ -4380,6 +4380,73 @@ export function TenantEditPage(props: TenantEditPageProps) {
); );
} }
// ---- Settings Page ----------------------------------------------------------
interface SettingsPageProps {
cloudAccounts: any[];
ablesignAccounts: any[];
error?: string;
}
export function SettingsPage(props: SettingsPageProps) {
return (
<Layout title="Settings" activeNav="settings">
<h1 style="font-size:1.5rem; margin:0 0 1.5rem">Settings</h1>
{props.error ? <div class="alert alert-error" style="margin-bottom:1rem">{props.error}</div> : ""}
<div class="card" style="margin-bottom:1.5rem">
<h2 style="font-size:1.1rem; margin:0 0 1rem">AbleSign Account</h2>
{props.ablesignAccounts.length > 0 ? (
<div class="table-wrap">
<table>
<thead><tr><th>Name</th><th>Screens</th><th>Last Sync</th><th>Actions</th></tr></thead>
<tbody>
{props.ablesignAccounts.map((a: any) => (
<tr>
<td>{a.name}</td>
<td>{String(a.screen_count ?? 0)}</td>
<td style="font-size:0.85rem">{a.last_sync_at ? formatTime(a.last_sync_at) : "Never"}</td>
<td>
<form method="POST" action={`/admin/ablesign/${String(a.id)}/delete`} style="display:inline">
<button type="submit" class="btn btn-sm btn-ghost" style="color:#c00">Remove</button>
</form>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<form method="POST" action="/admin/ablesign/add" style="display:flex; gap:0.5rem; flex-wrap:wrap; align-items:end">
<label style="font-size:0.85rem">
{"Name"}<br/>
<input type="text" name="name" required style="width:10rem" placeholder="My AbleSign" />
</label>
<label style="font-size:0.85rem">
{"API Key"}<br/>
<input type="password" name="api_key" required style="width:14rem" placeholder="ak_..." />
</label>
<label style="font-size:0.85rem">
{"Workspace ID"}<br/>
<input type="text" name="workspace_id" style="width:6rem" />
</label>
<button type="submit" class="btn btn-sm">Connect</button>
</form>
)}
</div>
<div class="card" style="margin-bottom:1.5rem">
<h2 style="font-size:1.1rem; margin:0 0 1rem">Cloud Camera Accounts</h2>
<p style="font-size:0.85rem; color:#999">
{"Manage cloud camera integrations at "}
<a href="/admin/cloud-accounts">Cloud Cams</a>.
</p>
</div>
</Layout>
);
}
// ---- AbleSign Pages --------------------------------------------------------- // ---- AbleSign Pages ---------------------------------------------------------
interface AbleSignPageProps { interface AbleSignPageProps {

View file

@ -71,18 +71,17 @@ function Sidebar(props: { activeNav?: string }) {
<NavItem href="/admin/kiosks" label="Kiosks" icon="&#9672;" active={a === "kiosks"} /> <NavItem href="/admin/kiosks" label="Kiosks" icon="&#9672;" active={a === "kiosks"} />
<NavItem href="/admin/firmware" label="Firmware" icon="&#9650;" active={a === "firmware"} /> <NavItem href="/admin/firmware" label="Firmware" icon="&#9650;" active={a === "firmware"} />
<NavItem href="/admin/os-updates" label="OS Updates" icon="&#9679;" active={a === "os-updates"} /> <NavItem href="/admin/os-updates" label="OS Updates" icon="&#9679;" active={a === "os-updates"} />
<NavItem href="/admin/cloud-accounts" label="Cloud Cams" icon="&#9729;" active={a === "cloud"} />
<NavGroup label="AbleSign" icon="&#9654;" active={a?.startsWith("ablesign")}> <NavGroup label="AbleSign" icon="&#9654;" active={a?.startsWith("ablesign")}>
<NavItem href="/admin/ablesign/screens" label="Screens" icon=" " active={a === "ablesign-screens"} /> <NavItem href="/admin/ablesign/screens" label="Screens" icon=" " active={a === "ablesign-screens"} />
<NavItem href="/admin/ablesign/content" label="Content" icon=" " active={a === "ablesign-content"} /> <NavItem href="/admin/ablesign/content" label="Content" icon=" " active={a === "ablesign-content"} />
<NavItem href="/admin/ablesign/playlists" label="Playlists" icon=" " active={a === "ablesign-playlists"} /> <NavItem href="/admin/ablesign/playlists" label="Playlists" icon=" " active={a === "ablesign-playlists"} />
<NavItem href="/admin/ablesign" label="Config" icon=" " active={a === "ablesign"} />
</NavGroup> </NavGroup>
<NavItem href="/admin/labels" label="Labels" icon="&#9670;" active={a === "labels"} /> <NavItem href="/admin/labels" label="Labels" icon="&#9670;" active={a === "labels"} />
<NavItem href="/admin/audit" label="Audit" icon="&#9678;" active={a === "audit"} /> <NavItem href="/admin/audit" label="Audit" icon="&#9678;" active={a === "audit"} />
<NavItem href="/admin/backup" label="Backup" icon="&#9788;" active={a === "backup"} /> <NavItem href="/admin/backup" label="Backup" icon="&#9788;" active={a === "backup"} />
<NavItem href="/admin/tenants" label="Tenants" icon="&#9783;" active={a === "tenants"} /> <NavItem href="/admin/tenants" label="Tenants" icon="&#9783;" active={a === "tenants"} />
<hr /> <hr />
<NavItem href="/admin/settings" label="Settings" icon="&#9881;" active={a === "settings"} />
<NavItem href="/admin/account" label="Account" icon="&#9679;" active={a === "account"} /> <NavItem href="/admin/account" label="Account" icon="&#9679;" active={a === "account"} />
<NavItem href="/admin/nodered" label="Node-RED" icon="&#8594;" active={a === "nodered"} /> <NavItem href="/admin/nodered" label="Node-RED" icon="&#8594;" active={a === "nodered"} />
<div class="tenant-switcher" {...{"hx-get": "/admin/_tenant_switcher", "hx-trigger": "load", "hx-swap": "innerHTML"}}></div> <div class="tenant-switcher" {...{"hx-get": "/admin/_tenant_switcher", "hx-trigger": "load", "hx-swap": "innerHTML"}}></div>