mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-27 01:46:35 +00:00
refactor: AbleSign UI — single account, screen detail, no kiosk assign
- Remove Accounts from AbleSign nav (one account per tenant) - Screens page: create button, no kiosk assignment - Screen detail page with config form - Internal/External badge Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e0941f533d
commit
65de42d495
3 changed files with 140 additions and 55 deletions
|
|
@ -6,7 +6,7 @@ import { type H3, getRouterParam, readBody, createError } from "h3";
|
||||||
import { htmlPage } from "./html-response.js";
|
import { htmlPage } from "./html-response.js";
|
||||||
import type { AdminDeps } from "./index.js";
|
import type { AdminDeps } from "./index.js";
|
||||||
import * as ablesign from "../../shared/ablesign.js";
|
import * as ablesign from "../../shared/ablesign.js";
|
||||||
import { AbleSignPage, AbleSignScreensPage, AbleSignContentPage, AbleSignPlaylistsPage } from "../../web-templates/admin-pages.js";
|
import { AbleSignPage, AbleSignScreensPage, AbleSignScreenDetailPage, AbleSignContentPage, AbleSignPlaylistsPage } from "../../web-templates/admin-pages.js";
|
||||||
|
|
||||||
export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
||||||
|
|
||||||
|
|
@ -55,19 +55,12 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
||||||
});
|
});
|
||||||
} catch { /* sync failure is non-fatal */ }
|
} catch { /* sync failure is non-fatal */ }
|
||||||
|
|
||||||
return new Response(null, { status: 302, headers: { location: `/admin/ablesign/${accountId}/screens` } });
|
return new Response(null, { status: 302, headers: { location: "/admin/ablesign/screens" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/admin/ablesign/:id/screens", async (event) => {
|
// Redirect old per-account route to global screens page.
|
||||||
const id = getRouterParam(event, "id") ?? "";
|
app.get("/admin/ablesign/:id/screens", async () => {
|
||||||
const account = await deps.repo.getAbleSignAccount(id);
|
return new Response(null, { status: 302, headers: { location: "/admin/ablesign/screens" } });
|
||||||
if (!account) throw createError({ statusCode: 404, statusMessage: "Account not found" });
|
|
||||||
const screens = await deps.repo.listAbleSignScreens(id);
|
|
||||||
const kiosks = await deps.repo.listKiosks();
|
|
||||||
for (const s of screens) {
|
|
||||||
(s as any).has_entity = !!(await deps.repo.getEntityByAbleSignScreen(s.id));
|
|
||||||
}
|
|
||||||
return htmlPage(AbleSignScreensPage({ account, screens, kiosks }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/admin/ablesign/:id/sync", async (event) => {
|
app.post("/admin/ablesign/:id/sync", async (event) => {
|
||||||
|
|
@ -114,7 +107,7 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
||||||
const body = await readBody<Record<string, string>>(event);
|
const body = await readBody<Record<string, string>>(event);
|
||||||
const title = (body?.title ?? "").trim();
|
const title = (body?.title ?? "").trim();
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return new Response(null, { status: 302, headers: { location: `/admin/ablesign/${accountId}/screens` } });
|
return new Response(null, { status: 302, headers: { location: "/admin/ablesign/screens" } });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -155,7 +148,7 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
||||||
// redirect back — error handling TODO
|
// redirect back — error handling TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(null, { status: 302, headers: { location: `/admin/ablesign/${accountId}/screens` } });
|
return new Response(null, { status: 302, headers: { location: "/admin/ablesign/screens" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/admin/ablesign/screens/:sid/assign", async (event) => {
|
app.post("/admin/ablesign/screens/:sid/assign", async (event) => {
|
||||||
|
|
@ -166,7 +159,55 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
||||||
|
|
||||||
const screen = await deps.repo.getAbleSignScreen(sid);
|
const screen = await deps.repo.getAbleSignScreen(sid);
|
||||||
const accountId = screen?.account_id ?? "";
|
const accountId = screen?.account_id ?? "";
|
||||||
return new Response(null, { status: 302, headers: { location: `/admin/ablesign/${accountId}/screens` } });
|
return new Response(null, { status: 302, headers: { location: "/admin/ablesign/screens" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Screen detail + config -------------------------------------------------
|
||||||
|
|
||||||
|
app.get("/admin/ablesign/screens/:sid", async (event) => {
|
||||||
|
const sid = getRouterParam(event, "sid") ?? "";
|
||||||
|
const screen = await deps.repo.getAbleSignScreen(sid);
|
||||||
|
if (!screen) throw createError({ statusCode: 404, statusMessage: "Screen not found" });
|
||||||
|
const account = await deps.repo.getAbleSignAccount(screen.account_id);
|
||||||
|
let remoteScreen: any = null;
|
||||||
|
if (account) {
|
||||||
|
try {
|
||||||
|
const apiKey = deps.secrets.decryptString(account.api_key_encrypted, "ablesign-key");
|
||||||
|
remoteScreen = await ablesign.getScreen(
|
||||||
|
{ apiKey, workspaceId: account.workspace_id || undefined },
|
||||||
|
Number(screen.ablesign_screen_id),
|
||||||
|
);
|
||||||
|
} catch { /* remote fetch failed */ }
|
||||||
|
}
|
||||||
|
const entity = await deps.repo.getEntityByAbleSignScreen(sid);
|
||||||
|
return htmlPage(AbleSignScreenDetailPage({ screen, remoteScreen, entity }));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/admin/ablesign/screens/:sid", async (event) => {
|
||||||
|
const sid = getRouterParam(event, "sid") ?? "";
|
||||||
|
const screen = await deps.repo.getAbleSignScreen(sid);
|
||||||
|
if (!screen) throw createError({ statusCode: 404, statusMessage: "Screen not found" });
|
||||||
|
const account = await deps.repo.getAbleSignAccount(screen.account_id);
|
||||||
|
if (!account) throw createError({ statusCode: 404, statusMessage: "Account not found" });
|
||||||
|
|
||||||
|
const body = await readBody<Record<string, string>>(event);
|
||||||
|
const title = (body?.title ?? "").trim();
|
||||||
|
const orientation = body?.orientation ?? "landscape";
|
||||||
|
const description = (body?.description ?? "").trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const apiKey = deps.secrets.decryptString(account.api_key_encrypted, "ablesign-key");
|
||||||
|
await ablesign.updateScreen(
|
||||||
|
{ apiKey, workspaceId: account.workspace_id || undefined },
|
||||||
|
Number(screen.ablesign_screen_id),
|
||||||
|
{ title: title || undefined, orientation, description: description || undefined },
|
||||||
|
);
|
||||||
|
if (title) {
|
||||||
|
await deps.repo.updateAbleSignScreen(sid, { title, orientation });
|
||||||
|
}
|
||||||
|
} catch { /* update failed */ }
|
||||||
|
|
||||||
|
return new Response(null, { status: 302, headers: { location: `/admin/ablesign/screens/${sid}` } });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/admin/ablesign/:id/delete", async (event) => {
|
app.post("/admin/ablesign/:id/delete", async (event) => {
|
||||||
|
|
@ -192,19 +233,19 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
||||||
await deps.repo.deleteAbleSignScreen(sid);
|
await deps.repo.deleteAbleSignScreen(sid);
|
||||||
}
|
}
|
||||||
const accountId = screen?.account_id ?? "";
|
const accountId = screen?.account_id ?? "";
|
||||||
return new Response(null, { status: 302, headers: { location: `/admin/ablesign/${accountId}/screens` } });
|
return new Response(null, { status: 302, headers: { location: "/admin/ablesign/screens" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---- Global views (all accounts aggregated) --------------------------------
|
// ---- Global views (all accounts aggregated) --------------------------------
|
||||||
|
|
||||||
app.get("/admin/ablesign/screens", async () => {
|
app.get("/admin/ablesign/screens", async () => {
|
||||||
const screens = await deps.repo.listAbleSignScreens();
|
|
||||||
const kiosks = await deps.repo.listKiosks();
|
|
||||||
const accounts = await deps.repo.listAbleSignAccounts();
|
const accounts = await deps.repo.listAbleSignAccounts();
|
||||||
|
const account = accounts[0] ?? null;
|
||||||
|
const screens = account ? await deps.repo.listAbleSignScreens(account.id) : [];
|
||||||
for (const s of screens) {
|
for (const s of screens) {
|
||||||
(s as any).has_entity = !!(await deps.repo.getEntityByAbleSignScreen(s.id));
|
(s as any).has_entity = !!(await deps.repo.getEntityByAbleSignScreen(s.id));
|
||||||
}
|
}
|
||||||
return htmlPage(AbleSignScreensPage({ account: null, screens, kiosks, accounts }));
|
return htmlPage(AbleSignScreensPage({ screens, accountId: account?.id ?? null }));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/admin/ablesign/content", async () => {
|
app.get("/admin/ablesign/content", async () => {
|
||||||
|
|
|
||||||
|
|
@ -4453,51 +4453,51 @@ export function AbleSignPage(props: AbleSignPageProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AbleSignScreensPageProps {
|
interface AbleSignScreensPageProps {
|
||||||
account: any | null;
|
|
||||||
screens: any[];
|
screens: any[];
|
||||||
kiosks: any[];
|
accountId: string | null;
|
||||||
accounts?: any[];
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AbleSignScreensPage(props: AbleSignScreensPageProps) {
|
export function AbleSignScreensPage(props: AbleSignScreensPageProps) {
|
||||||
const a = props.account;
|
const aid = props.accountId;
|
||||||
const isGlobal = !a;
|
|
||||||
const title = isGlobal ? "AbleSign — All Screens" : `AbleSign — ${String(a.name)}`;
|
|
||||||
return (
|
return (
|
||||||
<Layout title={title} activeNav={isGlobal ? "ablesign-screens" : "ablesign"}>
|
<Layout title="AbleSign — Screens" activeNav="ablesign-screens">
|
||||||
<h1 style="font-size:1.5rem; margin:0 0 0.5rem">{isGlobal ? "All AbleSign Screens" : `${String(a.name)} — Screens`}</h1>
|
<h1 style="font-size:1.5rem; margin:0 0 1.5rem">AbleSign Screens</h1>
|
||||||
{a ? (
|
|
||||||
<p style="color:#999; margin:0 0 1.5rem; font-size:0.85rem">
|
|
||||||
{String(a.screen_count ?? 0)} screens
|
|
||||||
{a.last_sync_at ? ` · synced ${formatTime(a.last_sync_at)}` : ""}
|
|
||||||
</p>
|
|
||||||
) : ""}
|
|
||||||
|
|
||||||
{a ? (
|
{props.error ? <div class="alert alert-error" style="margin-bottom:1rem">{props.error}</div> : ""}
|
||||||
|
|
||||||
|
{!aid ? (
|
||||||
<div class="card" style="margin-bottom:1.5rem">
|
<div class="card" style="margin-bottom:1.5rem">
|
||||||
<h2 style="font-size:1rem; margin:0 0 0.75rem">Add Screen</h2>
|
<p style="color:#999; font-size:0.85rem">No AbleSign account configured. Add one under Account settings first.</p>
|
||||||
<form method="POST" action={`/admin/ablesign/${String(a.id)}/screens/add`} style="display:flex; gap:0.5rem; align-items:end">
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="card" style="margin-bottom:1.5rem">
|
||||||
|
<h2 style="font-size:1rem; margin:0 0 0.75rem">Create Screen</h2>
|
||||||
|
<form method="POST" action={`/admin/ablesign/${aid}/screens/add`} style="display:flex; gap:0.5rem; align-items:end">
|
||||||
<label style="font-size:0.85rem">
|
<label style="font-size:0.85rem">
|
||||||
{"Screen Name"}<br/>
|
{"Screen Name"}<br/>
|
||||||
<input type="text" name="title" required style="width:16rem" placeholder="Lobby Display" />
|
<input type="text" name="title" required style="width:16rem" placeholder="Lobby Display" />
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" class="btn btn-sm">{"Create & Pair"}</button>
|
<button type="submit" class="btn btn-sm">{"Create & Pair"}</button>
|
||||||
</form>
|
</form>
|
||||||
|
<p style="font-size:0.8rem; color:#999; margin:0.5rem 0 0">
|
||||||
|
Registers a new screen in AbleSign headlessly and creates a linked entity for use in layouts.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : ""}
|
)}
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:1rem">
|
<div class="card" style="margin-bottom:1rem">
|
||||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem">
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem">
|
||||||
<h2 style="font-size:1rem; margin:0">Screens</h2>
|
<h2 style="font-size:1rem; margin:0">Screens</h2>
|
||||||
{a ? (
|
{aid ? (
|
||||||
<form method="POST" action={`/admin/ablesign/${String(a.id)}/sync`}>
|
<form method="POST" action={`/admin/ablesign/${aid}/sync`}>
|
||||||
<button type="submit" class="btn btn-sm btn-ghost">Sync from AbleSign</button>
|
<button type="submit" class="btn btn-sm btn-ghost">Sync from AbleSign</button>
|
||||||
</form>
|
</form>
|
||||||
) : ""}
|
) : ""}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{props.screens.length === 0 ? (
|
{props.screens.length === 0 ? (
|
||||||
<p style="color:#999; font-size:0.85rem">No screens yet. Add one above or sync from AbleSign.</p>
|
<p style="color:#999; font-size:0.85rem">No screens yet. Create one above or sync from AbleSign.</p>
|
||||||
) : (
|
) : (
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
|
|
@ -4506,13 +4506,12 @@ export function AbleSignScreensPage(props: AbleSignScreensPageProps) {
|
||||||
<th>Orientation</th>
|
<th>Orientation</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th>Assigned Kiosk</th>
|
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{props.screens.map((s: any) => (
|
{props.screens.map((s: any) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{s.title}</td>
|
<td><a href={`/admin/ablesign/screens/${String(s.id)}`}>{s.title}</a></td>
|
||||||
<td style="font-size:0.85rem">{s.orientation}</td>
|
<td style="font-size:0.85rem">{s.orientation}</td>
|
||||||
<td>
|
<td>
|
||||||
{s.online
|
{s.online
|
||||||
|
|
@ -4524,18 +4523,6 @@ export function AbleSignScreensPage(props: AbleSignScreensPageProps) {
|
||||||
? <span class="badge badge-blue">Internal</span>
|
? <span class="badge badge-blue">Internal</span>
|
||||||
: <span class="badge badge-gray">External</span>}
|
: <span class="badge badge-gray">External</span>}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<form method="POST" action={`/admin/ablesign/screens/${String(s.id)}/assign`}
|
|
||||||
style="display:flex; gap:0.25rem; align-items:center">
|
|
||||||
<select name="kiosk_id" style="font-size:0.85rem; max-width:14rem">
|
|
||||||
<option value="">— None —</option>
|
|
||||||
{props.kiosks.map((k: any) => (
|
|
||||||
<option value={String(k.id)} selected={k.id === s.kiosk_id}>{k.name}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button type="submit" class="btn btn-sm btn-ghost">Assign</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" action={`/admin/ablesign/screens/${String(s.id)}/delete`} style="display:inline">
|
<form method="POST" action={`/admin/ablesign/screens/${String(s.id)}/delete`} style="display:inline">
|
||||||
<button type="submit" class="btn btn-sm btn-ghost" style="color:#c00">Delete</button>
|
<button type="submit" class="btn btn-sm btn-ghost" style="color:#c00">Delete</button>
|
||||||
|
|
@ -4552,6 +4539,64 @@ export function AbleSignScreensPage(props: AbleSignScreensPageProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- AbleSign Screen Detail Page ---------------------------------------------
|
||||||
|
|
||||||
|
interface AbleSignScreenDetailPageProps {
|
||||||
|
screen: any;
|
||||||
|
remoteScreen: any | null;
|
||||||
|
entity: any | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AbleSignScreenDetailPage(props: AbleSignScreenDetailPageProps) {
|
||||||
|
const s = props.screen;
|
||||||
|
const r = props.remoteScreen;
|
||||||
|
return (
|
||||||
|
<Layout title={`AbleSign — ${String(s.title)}`} activeNav="ablesign-screens">
|
||||||
|
<h1 style="font-size:1.5rem; margin:0 0 1.5rem">{s.title}</h1>
|
||||||
|
|
||||||
|
<div class="card" style="margin-bottom:1.5rem">
|
||||||
|
<h2 style="font-size:1rem; margin:0 0 0.75rem">Screen Configuration</h2>
|
||||||
|
<form method="POST" action={`/admin/ablesign/screens/${String(s.id)}`}>
|
||||||
|
<div style="display:flex; gap:1rem; flex-wrap:wrap; margin-bottom:1rem">
|
||||||
|
<label style="font-size:0.85rem">
|
||||||
|
{"Title"}<br/>
|
||||||
|
<input type="text" name="title" value={s.title} style="width:16rem" />
|
||||||
|
</label>
|
||||||
|
<label style="font-size:0.85rem">
|
||||||
|
{"Orientation"}<br/>
|
||||||
|
<select name="orientation" style="font-size:0.85rem">
|
||||||
|
<option value="landscape" selected={s.orientation === "landscape"}>Landscape</option>
|
||||||
|
<option value="portrait" selected={s.orientation === "portrait"}>Portrait</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label style="font-size:0.85rem">
|
||||||
|
{"Description"}<br/>
|
||||||
|
<input type="text" name="description" value={r?.description ?? ""} style="width:20rem" placeholder="Optional" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-sm">Save</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-bottom:1.5rem">
|
||||||
|
<h2 style="font-size:1rem; margin:0 0 0.5rem">Status</h2>
|
||||||
|
<div style="display:flex; gap:1.5rem; flex-wrap:wrap; font-size:0.85rem; color:#666">
|
||||||
|
<div>{"AbleSign ID: "}{String(s.ablesign_screen_id)}</div>
|
||||||
|
<div>{"Status: "}{s.online ? "Online" : "Offline"}</div>
|
||||||
|
<div>{"Source: "}{props.entity ? "Internal" : "External"}</div>
|
||||||
|
{props.entity ? <div>{"Entity: "}<a href={`/admin/entities/${String(props.entity.id)}`}>{props.entity.name}</a></div> : ""}
|
||||||
|
{r?.heartbeatTime ? <div>{"Last heartbeat: "}{formatTime(r.heartbeatTime)}</div> : ""}
|
||||||
|
{r?.timezone ? <div>{"Timezone: "}{String(r.timezone)}</div> : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:0.5rem">
|
||||||
|
<a href="/admin/ablesign/screens" class="btn btn-sm btn-ghost">Back to Screens</a>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface AbleSignContentPageProps { content: any[]; accounts: any[]; }
|
interface AbleSignContentPageProps { content: any[]; accounts: any[]; }
|
||||||
|
|
||||||
export function AbleSignContentPage(props: AbleSignContentPageProps) {
|
export function AbleSignContentPage(props: AbleSignContentPageProps) {
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,6 @@ function Sidebar(props: { activeNav?: string }) {
|
||||||
<NavItem href="/admin/os-updates" label="OS Updates" icon="●" active={a === "os-updates"} />
|
<NavItem href="/admin/os-updates" label="OS Updates" icon="●" active={a === "os-updates"} />
|
||||||
<NavItem href="/admin/cloud-accounts" label="Cloud Cams" icon="☁" active={a === "cloud"} />
|
<NavItem href="/admin/cloud-accounts" label="Cloud Cams" icon="☁" active={a === "cloud"} />
|
||||||
<NavGroup label="AbleSign" icon="▶" active={a?.startsWith("ablesign")}>
|
<NavGroup label="AbleSign" icon="▶" active={a?.startsWith("ablesign")}>
|
||||||
<NavItem href="/admin/ablesign" label="Accounts" icon=" " active={a === "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"} />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue