`/admin` was 404 — only `/admin/team` existed. Add a browseable index so
the admin area has a root, with the existing Team-Verwaltung tile alongside
greyed-out roadmap placeholders (Departments, Audit-Log, Email-Templates,
Feature-Flags) so admins see what's coming.
- internal/handlers/admin_users.go: handleAdminIndexPage serves
dist/admin.html. Same RequireAdminFunc gate as /admin/team — non-admins
get the standard 302 to /dashboard?forbidden=admin.
- internal/handlers/handlers.go: register GET /admin under the existing
admin-conditional block.
- frontend/src/admin.tsx + client/admin.ts: card grid built from the
shared .grid + .card landing-page pattern. .admin-card-soon dims the
placeholders + adds a "Kommt bald" badge so they read as roadmap, not
broken links.
- frontend/src/components/Sidebar.tsx: add Admin-Bereich (/admin) above
Team-Verwaltung in the existing admin group. Both items live in the
same display:none group that sidebar.ts reveals after /api/me confirms
global_role='global_admin'.
- frontend/src/client/i18n.ts: nav.admin.bereich + admin.title /
.heading / .subtitle / .section.{available,planned} / .coming_soon
plus per-card title+desc, DE+EN.
- frontend/src/styles/global.css: .admin-section-planned spacing,
.admin-card-soon dimming, .admin-soon-badge pill.
- frontend/build.ts: register the renderAdmin entrypoint and admin.ts
client bundle.
184 lines
9.0 KiB
TypeScript
184 lines
9.0 KiB
TypeScript
import { mkdir, cp, rm, readdir } from "fs/promises";
|
|
import { join } from "path";
|
|
import { renderIndex } from "./src/index";
|
|
import { renderLogin } from "./src/login";
|
|
import { renderKostenrechner } from "./src/kostenrechner";
|
|
import { renderFristenrechner } from "./src/fristenrechner";
|
|
import { renderDownloads } from "./src/downloads";
|
|
import { renderLinks } from "./src/links";
|
|
import { renderGlossary } from "./src/glossary";
|
|
import { renderGebuehrentabellen } from "./src/gebuehrentabellen";
|
|
import { renderChecklists } from "./src/checklists";
|
|
import { renderChecklistsDetail } from "./src/checklists-detail";
|
|
import { renderChecklistsInstance } from "./src/checklists-instance";
|
|
import { renderCourts } from "./src/courts";
|
|
import { renderProjects } from "./src/projects";
|
|
import { renderProjectsNew } from "./src/projects-new";
|
|
import { renderProjectsDetail } from "./src/projects-detail";
|
|
import { renderDeadlines } from "./src/deadlines";
|
|
import { renderDeadlinesNew } from "./src/deadlines-new";
|
|
import { renderDeadlinesDetail } from "./src/deadlines-detail";
|
|
import { renderDeadlinesCalendar } from "./src/deadlines-calendar";
|
|
import { renderAppointments } from "./src/appointments";
|
|
import { renderAppointmentsNew } from "./src/appointments-new";
|
|
import { renderAppointmentsDetail } from "./src/appointments-detail";
|
|
import { renderAppointmentsCalendar } from "./src/appointments-calendar";
|
|
import { renderSettings } from "./src/settings";
|
|
import { renderDashboard } from "./src/dashboard";
|
|
import { renderAgenda } from "./src/agenda";
|
|
import { renderOnboarding } from "./src/onboarding";
|
|
import { renderChangelog } from "./src/changelog";
|
|
import { renderTeam } from "./src/team";
|
|
import { renderAdmin } from "./src/admin";
|
|
import { renderAdminTeam } from "./src/admin-team";
|
|
import { renderNotFound } from "./src/notfound";
|
|
|
|
const DIST = join(import.meta.dir, "dist");
|
|
|
|
async function build() {
|
|
// Clean dist/
|
|
await rm(DIST, { recursive: true, force: true });
|
|
await mkdir(join(DIST, "assets"), { recursive: true });
|
|
|
|
// Bundle client-side JS
|
|
const result = await Bun.build({
|
|
entrypoints: [
|
|
// app.ts is loaded on every page (SW registration + bottom-nav init +
|
|
// install prompt). Keep it ahead of per-page bundles so name collisions
|
|
// surface fast.
|
|
join(import.meta.dir, "src/client/app.ts"),
|
|
join(import.meta.dir, "src/client/index.ts"),
|
|
join(import.meta.dir, "src/client/login.ts"),
|
|
join(import.meta.dir, "src/client/kostenrechner.ts"),
|
|
join(import.meta.dir, "src/client/fristenrechner.ts"),
|
|
join(import.meta.dir, "src/client/downloads.ts"),
|
|
join(import.meta.dir, "src/client/links.ts"),
|
|
join(import.meta.dir, "src/client/glossary.ts"),
|
|
join(import.meta.dir, "src/client/gebuehrentabellen.ts"),
|
|
join(import.meta.dir, "src/client/checklists.ts"),
|
|
join(import.meta.dir, "src/client/checklists-detail.ts"),
|
|
join(import.meta.dir, "src/client/checklists-instance.ts"),
|
|
join(import.meta.dir, "src/client/courts.ts"),
|
|
join(import.meta.dir, "src/client/projects.ts"),
|
|
join(import.meta.dir, "src/client/projects-new.ts"),
|
|
join(import.meta.dir, "src/client/projects-detail.ts"),
|
|
join(import.meta.dir, "src/client/deadlines.ts"),
|
|
join(import.meta.dir, "src/client/deadlines-new.ts"),
|
|
join(import.meta.dir, "src/client/deadlines-detail.ts"),
|
|
join(import.meta.dir, "src/client/deadlines-calendar.ts"),
|
|
join(import.meta.dir, "src/client/appointments.ts"),
|
|
join(import.meta.dir, "src/client/appointments-new.ts"),
|
|
join(import.meta.dir, "src/client/appointments-detail.ts"),
|
|
join(import.meta.dir, "src/client/appointments-calendar.ts"),
|
|
join(import.meta.dir, "src/client/settings.ts"),
|
|
join(import.meta.dir, "src/client/dashboard.ts"),
|
|
join(import.meta.dir, "src/client/agenda.ts"),
|
|
join(import.meta.dir, "src/client/onboarding.ts"),
|
|
join(import.meta.dir, "src/client/changelog.ts"),
|
|
join(import.meta.dir, "src/client/team.ts"),
|
|
join(import.meta.dir, "src/client/admin.ts"),
|
|
join(import.meta.dir, "src/client/admin-team.ts"),
|
|
join(import.meta.dir, "src/client/notfound.ts"),
|
|
],
|
|
outdir: join(DIST, "assets"),
|
|
naming: "[name].js",
|
|
minify: true,
|
|
// IIFE wraps each bundle's top-level scope so per-page modules don't
|
|
// leak globals into each other. Without this, app.js's minified
|
|
// `var d = "patholo-sidebar-pinned"` (the legacy sidebar storage key)
|
|
// clobbered projects.js's minified `function d()` (applyTranslations)
|
|
// on the global object — projects.js then crashed with
|
|
// "TypeError: d is not a function" inside its DOMContentLoaded init,
|
|
// making /projects appear empty (t-paliad-043).
|
|
format: "iife",
|
|
});
|
|
|
|
if (!result.success) {
|
|
console.error("JS build failed:");
|
|
for (const log of result.logs) {
|
|
console.error(log);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
// Copy CSS
|
|
await cp(
|
|
join(import.meta.dir, "src/styles/global.css"),
|
|
join(DIST, "assets/global.css"),
|
|
);
|
|
|
|
// Copy public/ → dist/ (manifest.json, sw.js, icons/) — served at the
|
|
// application root so the service worker can claim scope=/ and so the
|
|
// manifest is reachable at /manifest.json without a sub-path rewrite.
|
|
await cp(
|
|
join(import.meta.dir, "public"),
|
|
DIST,
|
|
{ recursive: true },
|
|
);
|
|
|
|
// Stamp a unique version into sw.js so each deploy opens a fresh cache.
|
|
// Activate-time eviction (in sw.js) deletes any prior cache, including
|
|
// pre-versioning names like paliad-v1-static — that's what stops a stale
|
|
// /assets/projects.js from a previous deploy lingering on a user's device.
|
|
const swPath = join(DIST, "sw.js");
|
|
const swSrc = await Bun.file(swPath).text();
|
|
const buildVersion = `v${Date.now()}`;
|
|
await Bun.write(swPath, swSrc.replace("__PALIAD_BUILD_VERSION__", buildVersion));
|
|
|
|
// Render HTML pages
|
|
await Bun.write(join(DIST, "index.html"), renderIndex());
|
|
await Bun.write(join(DIST, "login.html"), renderLogin("login.js"));
|
|
await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner());
|
|
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
|
|
await Bun.write(join(DIST, "downloads.html"), renderDownloads());
|
|
await Bun.write(join(DIST, "links.html"), renderLinks());
|
|
await Bun.write(join(DIST, "glossary.html"), renderGlossary());
|
|
await Bun.write(join(DIST, "gebuehrentabellen.html"), renderGebuehrentabellen());
|
|
await Bun.write(join(DIST, "checklists.html"), renderChecklists());
|
|
await Bun.write(join(DIST, "checklists-detail.html"), renderChecklistsDetail());
|
|
await Bun.write(join(DIST, "checklists-instance.html"), renderChecklistsInstance());
|
|
await Bun.write(join(DIST, "courts.html"), renderCourts());
|
|
await Bun.write(join(DIST, "projects.html"), renderProjects());
|
|
await Bun.write(join(DIST, "projects-new.html"), renderProjectsNew());
|
|
await Bun.write(join(DIST, "projects-detail.html"), renderProjectsDetail());
|
|
await Bun.write(join(DIST, "deadlines.html"), renderDeadlines());
|
|
await Bun.write(join(DIST, "deadlines-new.html"), renderDeadlinesNew());
|
|
await Bun.write(join(DIST, "deadlines-detail.html"), renderDeadlinesDetail());
|
|
await Bun.write(join(DIST, "deadlines-calendar.html"), renderDeadlinesCalendar());
|
|
await Bun.write(join(DIST, "appointments.html"), renderAppointments());
|
|
await Bun.write(join(DIST, "appointments-new.html"), renderAppointmentsNew());
|
|
await Bun.write(join(DIST, "appointments-detail.html"), renderAppointmentsDetail());
|
|
await Bun.write(join(DIST, "appointments-calendar.html"), renderAppointmentsCalendar());
|
|
await Bun.write(join(DIST, "settings.html"), renderSettings());
|
|
await Bun.write(join(DIST, "dashboard.html"), renderDashboard());
|
|
await Bun.write(join(DIST, "agenda.html"), renderAgenda());
|
|
await Bun.write(join(DIST, "onboarding.html"), renderOnboarding());
|
|
await Bun.write(join(DIST, "changelog.html"), renderChangelog());
|
|
await Bun.write(join(DIST, "team.html"), renderTeam());
|
|
await Bun.write(join(DIST, "admin.html"), renderAdmin());
|
|
await Bun.write(join(DIST, "admin-team.html"), renderAdminTeam());
|
|
await Bun.write(join(DIST, "notfound.html"), renderNotFound());
|
|
|
|
// Append ?v=<buildVersion> to every /assets/*.js and /assets/*.css URL in
|
|
// every emitted HTML file. Cache-Control alone isn't enough: a browser that
|
|
// cached a script in a previous deploy keeps serving it from disk because
|
|
// the cache entry was stored without the no-cache directive. Versioning the
|
|
// URL changes the cache key, so the next page load fetches a fresh bundle
|
|
// unconditionally \u2014 this is what guarantees t-paliad-043's IIFE wrap fix
|
|
// actually reaches users on their next visit even without a SW.
|
|
const htmlFiles = (await readdir(DIST)).filter((f) => f.endsWith(".html"));
|
|
for (const f of htmlFiles) {
|
|
const path = join(DIST, f);
|
|
const html = await Bun.file(path).text();
|
|
const stamped = html.replace(
|
|
/(\/assets\/[\w-]+\.(?:js|css))/g,
|
|
`$1?v=${buildVersion}`,
|
|
);
|
|
await Bun.write(path, stamped);
|
|
}
|
|
|
|
console.log("Build complete \u2192 dist/");
|
|
}
|
|
|
|
build();
|