chore(frontend): remove the legacy vanilla-JS frontend and its tooling

The SvelteKit app under /frontend has fully superseded the legacy
vanilla-JS/CSS frontend in /static, which was only ever served by a
debug `cargo run` / `PROFILE=dev` and never shipped to production.
Remove it together with the whole subsystem that existed only to
support it (~54k lines).

Frontend & assets:
- Delete /static (js/, css/, *.html, sw.js, basemaps/, locales symlink).
- Relocate the brand/PWA assets (logo/, favicon.ico, manifest.webmanifest)
  to frontend/static/ so they ship with the SPA. This also fixes the
  favicon, which app.html referenced but was missing from the prod bundle.
- Migrate the Nextcloud login-flow redirects from /nextcloud-error.html
  to the SvelteKit /nextcloud/error route.

Web layer:
- Simplify resolve_static_path: drop the PROFILE=dev branch; always prefer
  the Vite static-dist/ build, fall back to the configured path.
- Resolve i18n locales from the served SPA dir with a frontend/static
  fallback so `just dev` works without a prior build.

Build:
- Prune build.rs from 1262 to ~70 lines (git metadata only); the Rust asset
  pipeline and the OXICLOUD_RUST_ASSETS rollback flag are gone.
- Drop the now-unused build-dependencies (oxc_*, lightningcss).
- Remove the COPY static lines from the Dockerfile (cacher + builder).

Tooling & docs:
- Delete biome.json, jsconfig.json, tools/check-*.py, identifier.sh.
- Remove the legacy front-* justfile recipes; repoint the design-system
  scripts (locales, dead-tokens, brand-drift, token-docs) at the frontend,
  and drop check-contrast/check-headings (coupled to the old token
  taxonomy / multi-page HTML).
- Repoint docs/DESIGN-SYSTEM.md links; remove 5 superseded docs/plan/*.

Backend dead code:
- Remove the dead `folder_repo` field from FileBlobWriteRepository.
- Remove the deprecated GET /api/folders/{id}/listing endpoint
  (superseded by /resources).

Verified: cargo clippy (all-features/all-targets) clean, cargo test
--workspace 448 passed, cargo fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-21 03:17:34 +02:00
parent 6be3c99580
commit 54639d466a
211 changed files with 171 additions and 53965 deletions
+3 -3
View File
@@ -19,12 +19,12 @@ const sha16 = (s) => createHash('sha256').update(s).digest('hex').slice(0, 16);
// ── Locked baseline ─────────────────────────────────────────────────────────
// Update INTENTIONALLY when the brand changes (and only then).
const LOCK = {
logoHash: '7fbd2016e9caeac1', // sha256(static/logo/logo-plain.svg)[:16]
logoHash: '7fbd2016e9caeac1', // sha256(frontend/static/logo/logo-plain.svg)[:16]
gradient: 'linear-gradient(135deg, #ff5e3a 0%, #ff8a5c 100%)' // --color-logo-gradient
};
const logo = readFileSync(join(repo, 'static/logo/logo-plain.svg'), 'utf8');
const vars = readFileSync(join(repo, 'static/css/base/variables.css'), 'utf8');
const logo = readFileSync(join(repo, 'frontend/static/logo/logo-plain.svg'), 'utf8');
const vars = readFileSync(join(repo, 'frontend/src/lib/styles/base/variables.css'), 'utf8');
const gradMatch = vars.match(/--color-logo-gradient:\s*([^;]+);/);
const gradient = gradMatch ? gradMatch[1].trim().replace(/\s+/g, ' ') : '(token missing!)';
-103
View File
@@ -1,103 +0,0 @@
#!/usr/bin/env node
// WCAG AA contrast guardrail.
//
// Resolves every text/background design token (through light-dark() and var()
// aliases) and fails (exit 1) if any text-on-surface or semantic text-on-tint
// pair drops below 4.5:1 in either light or dark mode. Keeps the palette from
// silently regressing into unreadable greys.
//
// node scripts/check-contrast.mjs
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const varsPath = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'static',
'css',
'base',
'variables.css'
);
const src = readFileSync(varsPath, 'utf8');
/** First (\:root) definition of each token. */
const raw = {};
for (const m of src.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/gi)) {
if (!(m[1] in raw)) raw[m[1]] = m[2].trim();
}
function resolve(val, mode, depth = 0) {
if (depth > 12 || val == null) return null;
val = String(val).trim();
let m = val.match(/^light-dark\(\s*(.+?),\s*(.+)\)\s*$/);
if (m) return resolve(mode === 'light' ? m[1] : m[2], mode, depth + 1);
m = val.match(/^var\(\s*(--[a-z0-9-]+)\s*\)/);
if (m) return resolve(raw[m[1]], mode, depth + 1);
m = val.match(/^#([0-9a-fA-F]{3,8})\b/);
if (m) {
let h = m[1];
if (h.length === 3)
h = [...h].map((c) => c + c).join('');
return '#' + h.slice(0, 6).toLowerCase();
}
if (val === 'white') return '#ffffff';
if (val === 'black') return '#000000';
return null;
}
const lin = (c) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
};
const lum = (h) =>
0.2126 * lin(parseInt(h.slice(1, 3), 16)) +
0.7152 * lin(parseInt(h.slice(3, 5), 16)) +
0.0722 * lin(parseInt(h.slice(5, 7), 16));
const ratio = (fg, bg) => {
const a = lum(fg);
const b = lum(bg);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
};
const texts = [
'--color-text', '--color-text-heading', '--color-text-secondary', '--color-text-muted',
'--color-text-subtle', '--color-text-faint', '--color-text-placeholder', '--color-text-gray',
'--color-text-medium', '--color-text-light', '--color-text-faint2', '--color-text-dim', '--color-text-dark'
];
const bgs = [
'--color-bg-surface', '--color-bg-page', '--color-bg-hover', '--color-bg-input',
'--color-bg-subtle', '--color-bg-muted', '--color-bg-input-alt'
];
const semantic = [
['--color-success-text', '--color-success-bg'],
['--color-error-text', '--color-error-bg'],
['--color-warning-text', '--color-warning-bg'],
['--color-info-text', '--color-info-bg'],
['--color-accent-text', '--color-bg-surface'],
['--color-accent-text', '--color-bg-page'],
['--color-accent-text', '--color-bg-muted']
];
const fails = [];
for (const mode of ['light', 'dark']) {
for (const t of texts)
for (const b of bgs) {
const fg = resolve(raw[t], mode);
const bg = resolve(raw[b], mode);
if (fg && bg && ratio(fg, bg) < 4.5)
fails.push(`${mode}: ${t}(${fg}) on ${b}(${bg}) = ${ratio(fg, bg).toFixed(2)}`);
}
for (const [t, b] of semantic) {
const fg = resolve(raw[t], mode);
const bg = resolve(raw[b], mode);
if (fg && bg && ratio(fg, bg) < 4.5)
fails.push(`${mode}: ${t}(${fg}) on ${b}(${bg}) = ${ratio(fg, bg).toFixed(2)}`);
}
}
if (fails.length) {
console.error('✖ WCAG AA contrast failures (<4.5:1):\n ' + fails.join('\n '));
process.exit(1);
}
console.log('✓ All text/background token pairs pass WCAG AA (4.5:1) in light and dark.');
+4 -4
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Dead-token report (informational, exit 0): design tokens defined in
// variables.css but never referenced via var() anywhere in static/.
// variables.css but never referenced via var() anywhere in frontend/src.
//
// NOTE: a cleanup AID, not a hard gate — some tokens (file-type / calendar
// colours) are referenced by JS string construction, so excluded prefixes are
@@ -11,17 +11,17 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'static');
const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'frontend', 'src');
const EXCLUDE_PREFIX = ['--color-ft-', '--color-cal-']; // referenced dynamically from JS
const varsSrc = readFileSync(join(root, 'css', 'base', 'variables.css'), 'utf8');
const varsSrc = readFileSync(join(root, 'lib', 'styles', 'base', 'variables.css'), 'utf8');
const defined = [...varsSrc.matchAll(/(--[a-z0-9-]+)\s*:/gi)].map((m) => m[1]);
function walk(dir, files = []) {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) walk(p, files);
else if (/\.(css|js|html|webmanifest)$/.test(name)) files.push(p);
else if (/\.(css|svelte|ts|js|html|webmanifest)$/.test(name)) files.push(p);
}
return files;
}
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env node
// Heading-order guardrail: each page has at least one <h1> and never skips a
// level going deeper (e.g. h1 → h3 without an h2). Fails (exit 1) on violations.
//
// node scripts/check-headings.mjs
import { readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const staticDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'static');
const pages = readdirSync(staticDir).filter((f) => f.endsWith('.html'));
let problems = 0;
for (const page of pages) {
const html = readFileSync(join(staticDir, page), 'utf8');
const levels = [...html.matchAll(/<h([1-6])\b/gi)].map((m) => Number(m[1]));
if (!levels.length) continue; // heading-less shells (error pages) are fine
const issues = [];
if (!levels.includes(1)) issues.push('no <h1>');
let prev = 0;
for (const lvl of levels) {
if (prev && lvl > prev + 1) issues.push(`skips h${prev}→h${lvl}`);
prev = lvl;
}
if (issues.length) {
problems++;
console.error(`${page}: ${issues.join(', ')} [order: ${levels.join(',')}]`);
}
}
if (problems) {
console.error(`\n✖ ${problems} page(s) with heading-order issues.`);
process.exit(1);
}
console.log('✓ All pages have an h1 and no skipped heading levels.');
+7 -1
View File
@@ -10,7 +10,13 @@ import { readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const localesDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'static', 'locales');
const localesDir = join(
dirname(fileURLToPath(import.meta.url)),
'..',
'frontend',
'static',
'locales'
);
/** Flatten a nested translation object to dotted keys. */
function flat(obj, prefix = '', out = {}) {
+6 -3
View File
@@ -1,13 +1,16 @@
#!/usr/bin/env node
// Generates docs/TOKENS.md — a grouped reference of every design token defined
// in static/css/base/variables.css. Run after changing tokens:
// in frontend/src/lib/styles/base/variables.css. Run after changing tokens:
// node scripts/gen-token-docs.mjs
import { readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const repo = join(dirname(fileURLToPath(import.meta.url)), '..');
const src = readFileSync(join(repo, 'static', 'css', 'base', 'variables.css'), 'utf8');
const src = readFileSync(
join(repo, 'frontend', 'src', 'lib', 'styles', 'base', 'variables.css'),
'utf8'
);
const tokens = [];
for (const m of src.matchAll(/^\s*(--[a-z0-9-]+)\s*:\s*([^;]+);/gim)) {
@@ -46,7 +49,7 @@ for (const [name, value] of tokens) {
let md = `# Design tokens
> Auto-generated from \`static/css/base/variables.css\` by \`scripts/gen-token-docs.mjs\`.
> Auto-generated from \`frontend/src/lib/styles/base/variables.css\` by \`scripts/gen-token-docs.mjs\`.
> Do not edit by hand — re-run the generator after changing tokens.
**${tokens.length} tokens** across ${[...buckets].filter(([, v]) => v.length).length} groups.