refactor: simplify admin code — extract shared helpers, fix quality issues
- Extract KEBAB_RE, SESSION_COOKIE, SESSION_MAX_AGE_MS, COOKIE_MAX_AGE,
authRequest(), and sessionResponse() to auth.ts — eliminating 4 copies
of the auth() wrapper and 3 copies of KEBAB_RE across admin API routes
- Remove timing side-channel: drop a.length !== b.length early-return
before timingSafeEqual in verifySession (SHA-256 HMAC always 64 chars)
- Fix YOLO localStorage write-before-read race using initialization guard
- Fix admin layout /api/ bypass to return {} instead of {authenticated:true}
- Extract extractError() helper in admin page — removes 4 inline patterns
- Parallel Promise.all in admin loadData() — halves round-trip latency
- Parallel Promise.all in moveCheckedUnits() + surface per-request errors
- Hoist Big(inputValue) above .map() in converter — one alloc per recompute
- O(n) single-pass orderedResults using Map instead of O(n*groups) filters
- Fix expandedGroups.add() mutation — use Set assignment for consistency
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,23 +24,29 @@
|
||||
|
||||
// ── Load data on mount ───────────────────────────────────────────────────────
|
||||
async function loadData() {
|
||||
const res = await fetch('/admin/api/units');
|
||||
const [res, gres] = await Promise.all([
|
||||
fetch('/admin/api/units'),
|
||||
fetch('/admin/api/groups')
|
||||
]);
|
||||
if (!res.ok) return;
|
||||
const units: Unit[] = await res.json();
|
||||
const gres = await fetch('/admin/api/groups');
|
||||
const groups: Group[] = gres.ok ? await gres.json() : [];
|
||||
unitsData = { units, groups };
|
||||
// Initialize expanded state
|
||||
for (const g of groups) {
|
||||
expandedGroups.add(g.id);
|
||||
}
|
||||
// Expand all groups — use assignment for consistent reactive update
|
||||
expandedGroups = new Set([...expandedGroups, ...groups.map((g) => g.id)]);
|
||||
}
|
||||
|
||||
// Load on mount
|
||||
// ── Load on mount ────────────────────────────────────────────────────────────
|
||||
$effect(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
/** Extract a server error message from a non-ok response. */
|
||||
async function extractError(res: Response, fallback: string): Promise<string> {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
return (body as Record<string, string>).error ?? fallback;
|
||||
}
|
||||
|
||||
// ── Derived ──────────────────────────────────────────────────────────────────
|
||||
let unitsByGroup = $derived.by(() => {
|
||||
const map = new Map<string | null, Unit[]>();
|
||||
@@ -113,8 +119,7 @@
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
formError = (err as Record<string, string>).error ?? 'Save failed';
|
||||
formError = await extractError(res, 'Save failed');
|
||||
return;
|
||||
}
|
||||
formSuccess = unitFormNew ? 'Unit created.' : 'Unit saved.';
|
||||
@@ -135,8 +140,7 @@
|
||||
try {
|
||||
const res = await fetch(`/admin/api/units/${selectedItem.id}`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
formError = (err as Record<string, string>).error ?? 'Delete failed';
|
||||
formError = await extractError(res, 'Delete failed');
|
||||
return;
|
||||
}
|
||||
selectedItem = null;
|
||||
@@ -167,8 +171,7 @@
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
formError = (err as Record<string, string>).error ?? 'Save failed';
|
||||
formError = await extractError(res, 'Save failed');
|
||||
return;
|
||||
}
|
||||
formSuccess = groupFormNew ? 'Group created.' : 'Group saved.';
|
||||
@@ -220,8 +223,7 @@
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
formError = (err as Record<string, string>).error ?? 'Delete failed';
|
||||
formError = await extractError(res, 'Delete failed');
|
||||
return;
|
||||
}
|
||||
deleteGroupTarget = null;
|
||||
@@ -236,12 +238,19 @@
|
||||
// ── Multi-select move ────────────────────────────────────────────────────────
|
||||
async function moveCheckedUnits() {
|
||||
if (!moveTargetGroupId || checkedUnitIds.size === 0) return;
|
||||
for (const unitId of checkedUnitIds) {
|
||||
await fetch(`/admin/api/units/${unitId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group: moveTargetGroupId })
|
||||
});
|
||||
formError = null;
|
||||
const results = await Promise.all(
|
||||
[...checkedUnitIds].map((unitId) =>
|
||||
fetch(`/admin/api/units/${unitId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group: moveTargetGroupId })
|
||||
})
|
||||
)
|
||||
);
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
if (failed.length) {
|
||||
formError = `${failed.length} unit(s) failed to move`;
|
||||
}
|
||||
checkedUnitIds = new Set();
|
||||
moveTargetGroupId = '';
|
||||
|
||||
Reference in New Issue
Block a user