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:
@@ -1,211 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
check-icons.py — Audit FA icon usage against the inline SVG registry.
|
||||
|
||||
Usage:
|
||||
python3 tools/check-icons.py [--dry-run] [--check-only]
|
||||
|
||||
Modes:
|
||||
(default) Scan, diff, and **patch** icons.js — clones Font-Awesome
|
||||
to tmp/ if missing so SVG paths can be resolved.
|
||||
--dry-run Same as default but print the proposed insertions
|
||||
instead of writing icons.js.
|
||||
--check-only CI-friendly: just scan + diff and exit with status 1
|
||||
if any used FA icon is absent from OxiIcons. No
|
||||
Font-Awesome clone, no file writes, no SVG parsing.
|
||||
|
||||
What the full mode does:
|
||||
1. Scans every file under static/ for fas fa-<name> occurrences.
|
||||
2. Reads the OxiIcons registry from static/js/core/icons.js.
|
||||
3. For each icon name that is missing from the registry, looks up
|
||||
tmp/Font-Awesome/svgs/solid/<name>.svg
|
||||
and adds the entry '<name>': [<width>, '<path>'] to OxiIcons.
|
||||
4. Rewrites icons.js in-place (unless --dry-run is given).
|
||||
|
||||
FontAwesome source: tmp/Font-Awesome/svgs/solid/
|
||||
Icons registry: static/js/core/icons.js
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────────────
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
STATIC_DIR = REPO_ROOT / "static"
|
||||
ICONS_JS = STATIC_DIR / "js" / "core" / "icons.js"
|
||||
FA_SVG_DIR = REPO_ROOT / "tmp" / "Font-Awesome" / "svgs" / "solid"
|
||||
|
||||
DRY_RUN = "--dry-run" in sys.argv
|
||||
CHECK_ONLY = "--check-only" in sys.argv
|
||||
|
||||
# ── 0. Ensure Font-Awesome source is available ────────────────────────────────
|
||||
# Skipped in --check-only mode — that path stops after the diff (step 3)
|
||||
# so it never needs to resolve SVG sources. This keeps CI runs offline,
|
||||
# fast, and free of clone side effects in the checkout dir.
|
||||
if not CHECK_ONLY:
|
||||
TMP_DIR = REPO_ROOT / "tmp"
|
||||
if not TMP_DIR.exists():
|
||||
print(f"Creating {TMP_DIR.relative_to(REPO_ROOT)}/")
|
||||
TMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
FA_REPO = TMP_DIR / "Font-Awesome"
|
||||
if not FA_REPO.exists():
|
||||
print(f"Font-Awesome not found at {FA_REPO.relative_to(REPO_ROOT)} — cloning …")
|
||||
result = subprocess.run(
|
||||
["git", "clone", "https://github.com/FortAwesome/Font-Awesome.git", str(FA_REPO)],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print("✗ git clone failed — cannot continue without Font-Awesome source.")
|
||||
sys.exit(1)
|
||||
print("✓ Font-Awesome cloned successfully.\n")
|
||||
|
||||
# ── 1. Scan static/ for all fas fa-<name> occurrences ───────────────────────
|
||||
FA_RE = re.compile(r'\bfas fa-([\w-]+)')
|
||||
|
||||
used_icons: dict[str, list[str]] = {} # name → [file, …]
|
||||
|
||||
SKIP_DIRS = {".git", "node_modules"}
|
||||
|
||||
for path in sorted(STATIC_DIR.rglob("*")):
|
||||
if any(part in SKIP_DIRS for part in path.parts):
|
||||
continue
|
||||
if not path.is_file():
|
||||
continue
|
||||
# Only scan text files we care about
|
||||
if path.suffix not in {".html", ".js", ".css"}:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
continue
|
||||
for m in FA_RE.finditer(text):
|
||||
name = m.group(1)
|
||||
rel = str(path.relative_to(REPO_ROOT))
|
||||
used_icons.setdefault(name, []).append(rel)
|
||||
|
||||
print(f"Found {len(used_icons)} distinct FA icon(s) referenced in static/")
|
||||
|
||||
# ── 2. Parse existing OxiIcons keys from icons.js ───────────────────────────────
|
||||
icons_src = ICONS_JS.read_text(encoding="utf-8")
|
||||
|
||||
# Extract only the OxiIcons object body so we never accidentally match keys from
|
||||
# other objects or functions elsewhere in the file.
|
||||
_ICONS_BLOCK_RE = re.compile(r'const OxiIcons\s*=\s*\{(.*?)^};', re.DOTALL | re.MULTILINE)
|
||||
block_match = _ICONS_BLOCK_RE.search(icons_src)
|
||||
if not block_match:
|
||||
print("✗ Could not locate 'const OxiIcons = { … };' in icons.js — aborting.")
|
||||
sys.exit(1)
|
||||
icons_block = block_match.group(1)
|
||||
|
||||
# Now parse keys only within that block.
|
||||
# Keys may be quoted ('bars', "bars") or bare (bars) — make the quotes optional.
|
||||
# Hyphenated names like 'arrow-left' must be quoted in JS; bare keys are word-only.
|
||||
ICON_KEY_RE = re.compile(r"""['"]?([\w-]+)['"]?\s*:\s*\[""")
|
||||
registered: set[str] = {m.group(1) for m in ICON_KEY_RE.finditer(icons_block)}
|
||||
|
||||
print(f"Registry has {len(registered)} icon(s) in OxiIcons")
|
||||
|
||||
# ── 3. Find missing icons ─────────────────────────────────────────────────────
|
||||
missing = {name: files for name, files in used_icons.items() if name not in registered}
|
||||
|
||||
if not missing:
|
||||
print("✓ All used icons are present in the registry — nothing to do.")
|
||||
sys.exit(0)
|
||||
|
||||
print(f"\n{len(missing)} missing icon(s):")
|
||||
|
||||
# ── 3b. CI gate ───────────────────────────────────────────────────────────────
|
||||
# In --check-only mode we report the missing names and stop here. The
|
||||
# default mode continues into the SVG-resolve + patch path below.
|
||||
if CHECK_ONLY:
|
||||
for name, files in sorted(missing.items()):
|
||||
print(f" • {name:30s} used in: {', '.join(files)}")
|
||||
print(
|
||||
f"\n✗ {len(missing)} icon(s) referenced in static/ are absent from "
|
||||
f"OxiIcons in {ICONS_JS.relative_to(REPO_ROOT)}."
|
||||
)
|
||||
print(
|
||||
" Run `python3 tools/check-icons.py` locally (without "
|
||||
"--check-only) to auto-add them from Font-Awesome."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# ── 4. Resolve each missing icon from FA SVG files ────────────────────────────
|
||||
VIEWBOX_RE = re.compile(r'viewBox="0 0 (\d+) (\d+)"')
|
||||
PATH_D_RE = re.compile(r'<path[^>]+\bd="([^"]+)"')
|
||||
|
||||
new_entries: list[tuple[str, int, str]] = [] # (name, width, d)
|
||||
not_found: list[str] = []
|
||||
|
||||
for name, files in sorted(missing.items()):
|
||||
svg_path = FA_SVG_DIR / f"{name}.svg"
|
||||
print(f" • {name:30s} used in: {', '.join(files)}", end="")
|
||||
|
||||
if not svg_path.exists():
|
||||
print(f" ✗ SVG not found: {svg_path.relative_to(REPO_ROOT)}")
|
||||
not_found.append(name)
|
||||
continue
|
||||
|
||||
svg_text = svg_path.read_text(encoding="utf-8")
|
||||
|
||||
vb = VIEWBOX_RE.search(svg_text)
|
||||
pd = PATH_D_RE.search(svg_text)
|
||||
|
||||
if not vb or not pd:
|
||||
print(f" ✗ Could not parse SVG (viewBox={bool(vb)}, path={bool(pd)})")
|
||||
not_found.append(name)
|
||||
continue
|
||||
|
||||
width = int(vb.group(1))
|
||||
d = pd.group(1)
|
||||
print(f" ✓ viewBox=0 0 {width} 512")
|
||||
new_entries.append((name, width, d))
|
||||
|
||||
# ── 5. Patch icons.js ─────────────────────────────────────────────────────────
|
||||
if not new_entries:
|
||||
if not_found:
|
||||
print(f"\n✗ {len(not_found)} icon(s) could not be resolved — no changes written.")
|
||||
sys.exit(1 if not_found else 0)
|
||||
|
||||
# Build the text block to insert, sorted alphabetically for readability
|
||||
new_entries.sort(key=lambda x: x[0])
|
||||
|
||||
insert_lines = []
|
||||
for name, width, d in new_entries:
|
||||
insert_lines.append(f" '{name}': [\n {width},\n '{d}'\n ],")
|
||||
|
||||
insert_block = "\n".join(insert_lines) + "\n"
|
||||
|
||||
# Insert just before the closing "};" of OxiIcons (line 396 area)
|
||||
# Anchor: the line that is exactly "};"
|
||||
ICONS_END_RE = re.compile(r'^};$', re.MULTILINE)
|
||||
m = ICONS_END_RE.search(icons_src)
|
||||
if not m:
|
||||
print("\n✗ Could not locate the closing '}; ' of OxiIcons in icons.js — aborting.")
|
||||
sys.exit(1)
|
||||
|
||||
# Ensure the last existing entry has a trailing comma before we append.
|
||||
before = icons_src[: m.start()]
|
||||
if before.rstrip()[-1:] != ',':
|
||||
# Insert comma right after the last non-whitespace character
|
||||
rstripped = before.rstrip()
|
||||
trailing_ws = before[len(rstripped):]
|
||||
before = rstripped + ',\n' + trailing_ws
|
||||
|
||||
new_src = before + insert_block + icons_src[m.start() :]
|
||||
|
||||
if DRY_RUN:
|
||||
print(f"\n-- DRY RUN: would insert into icons.js --\n{insert_block}")
|
||||
else:
|
||||
ICONS_JS.write_text(new_src, encoding="utf-8")
|
||||
print(f"\n✓ Added {len(new_entries)} icon(s) to {ICONS_JS.relative_to(REPO_ROOT)}")
|
||||
|
||||
if not_found:
|
||||
print(f"\n⚠ {len(not_found)} icon(s) still missing (no SVG source found):")
|
||||
for n in not_found:
|
||||
print(f" - {n}")
|
||||
sys.exit(1)
|
||||
@@ -1,247 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
check-missing-translations.py — Audit locale files against en.json.
|
||||
|
||||
Treats static/locales/en.json as the canonical key set. For every other
|
||||
JSON file in static/locales/, reports keys that are missing (present in
|
||||
en.json, absent here) and optionally keys that are extra (present here,
|
||||
absent in en.json — usually drift from a removed feature).
|
||||
|
||||
Exit code:
|
||||
0 — every non-English locale has every English key
|
||||
1 — at least one locale is missing one or more keys
|
||||
|
||||
Usage:
|
||||
python3 tools/check-missing-translations.py [options]
|
||||
|
||||
Options:
|
||||
--check-only CI-friendly: print per-locale counts only
|
||||
(no per-key list). Exit code is unchanged —
|
||||
the script always returns 1 on any miss,
|
||||
this flag just keeps the CI log terse.
|
||||
Mirrors `tools/check-icons.py --check-only`.
|
||||
--no-extras Suppress the "extra keys" section.
|
||||
--values Show the English source value next to each
|
||||
missing key (truncated to 80 chars).
|
||||
--locale CODE [CODE…] Audit only the listed locale(s) (e.g. fr de).
|
||||
Default: every non-English file in the dir.
|
||||
|
||||
Examples:
|
||||
# Verbose audit of every locale, including extras
|
||||
python3 tools/check-missing-translations.py
|
||||
|
||||
# CI mode — terse output, exit 1 on any miss
|
||||
python3 tools/check-missing-translations.py --check-only
|
||||
|
||||
# Just French and Spanish, with English values to help translators
|
||||
python3 tools/check-missing-translations.py --locale fr es --values
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
LOCALES_DIR = REPO_ROOT / "static" / "locales"
|
||||
SOURCE_LOCALE = "en"
|
||||
|
||||
# Truncation length for the English source value shown by --values.
|
||||
VALUE_PREVIEW_LEN = 80
|
||||
|
||||
|
||||
def flatten(obj: dict[str, Any], prefix: str = "") -> dict[str, Any]:
|
||||
"""Walk a nested JSON object and produce a flat {"dotted.key": value}
|
||||
dict. Non-dict leaves (strings, numbers, booleans, arrays) are kept
|
||||
as-is; only nested dicts are expanded into the key path."""
|
||||
out: dict[str, Any] = {}
|
||||
for key, value in obj.items():
|
||||
full = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict):
|
||||
out.update(flatten(value, full))
|
||||
else:
|
||||
out[full] = value
|
||||
return out
|
||||
|
||||
|
||||
def truncate(text: str, max_len: int) -> str:
|
||||
"""Visual truncation for terminal output. Newlines normalised to
|
||||
spaces so a multi-line email body still fits on one row."""
|
||||
one_line = text.replace("\n", " ").replace("\r", " ")
|
||||
if len(one_line) <= max_len:
|
||||
return one_line
|
||||
return one_line[: max_len - 1] + "…"
|
||||
|
||||
|
||||
def load_locale(path: Path) -> dict[str, Any]:
|
||||
"""Parse one locale file. Returns the flattened key set."""
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
print(f"✗ {path.name}: could not parse ({exc})", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
if not isinstance(data, dict):
|
||||
print(f"✗ {path.name}: root must be an object", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return flatten(data)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Audit static/locales/*.json against en.json. Reports keys "
|
||||
"missing from each non-English locale and (optionally) keys "
|
||||
"that exist in non-English but not in English."
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-only",
|
||||
action="store_true",
|
||||
help=(
|
||||
"CI mode: print per-locale counts only, not the full key "
|
||||
"list. Exit code is unchanged (1 on any miss). Mirrors "
|
||||
"`tools/check-icons.py --check-only`."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-extras",
|
||||
action="store_true",
|
||||
help="Suppress the 'extra keys' section (default: report them).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--values",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Print the English source value next to each missing key. "
|
||||
"Helpful for translators; ignored under --check-only."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--locale",
|
||||
nargs="+",
|
||||
metavar="CODE",
|
||||
help=(
|
||||
"Audit only the given locale code(s) (e.g. 'fr', 'zh-TW'). "
|
||||
"Default: every *.json in static/locales/ except en.json."
|
||||
),
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
|
||||
if not LOCALES_DIR.is_dir():
|
||||
print(f"✗ Locales directory not found: {LOCALES_DIR}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
source_path = LOCALES_DIR / f"{SOURCE_LOCALE}.json"
|
||||
if not source_path.exists():
|
||||
print(f"✗ Source locale not found: {source_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
en = load_locale(source_path)
|
||||
en_keys = set(en.keys())
|
||||
|
||||
# Decide which locales to audit. --locale narrows the set; otherwise
|
||||
# we audit everything except en.json.
|
||||
if args.locale:
|
||||
locale_paths = []
|
||||
for code in args.locale:
|
||||
p = LOCALES_DIR / f"{code}.json"
|
||||
if not p.exists():
|
||||
print(f"✗ Locale file not found: {p}", file=sys.stderr)
|
||||
return 1
|
||||
if code == SOURCE_LOCALE:
|
||||
print(
|
||||
f"⚠ Skipping --locale {code}: that's the source locale.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
locale_paths.append(p)
|
||||
else:
|
||||
locale_paths = sorted(
|
||||
p
|
||||
for p in LOCALES_DIR.glob("*.json")
|
||||
if p.stem != SOURCE_LOCALE
|
||||
)
|
||||
|
||||
if not locale_paths:
|
||||
print("Nothing to check.")
|
||||
return 0
|
||||
|
||||
print(f"Source: {source_path.relative_to(REPO_ROOT)} ({len(en_keys)} keys)\n")
|
||||
|
||||
total_missing = 0
|
||||
total_extra = 0
|
||||
locale_with_missing: list[str] = []
|
||||
|
||||
for path in locale_paths:
|
||||
loc = load_locale(path)
|
||||
loc_keys = set(loc.keys())
|
||||
missing = sorted(en_keys - loc_keys)
|
||||
extra = sorted(loc_keys - en_keys)
|
||||
total_missing += len(missing)
|
||||
total_extra += len(extra)
|
||||
if missing:
|
||||
locale_with_missing.append(path.stem)
|
||||
|
||||
mark = "✓" if not missing else "✗"
|
||||
suffix = ""
|
||||
if missing or extra:
|
||||
parts = []
|
||||
if missing:
|
||||
parts.append(f"missing={len(missing)}")
|
||||
if extra:
|
||||
parts.append(f"extra={len(extra)}")
|
||||
suffix = " " + " ".join(parts)
|
||||
print(f" {mark} {path.name:14} total={len(loc_keys)}{suffix}")
|
||||
|
||||
if args.check_only:
|
||||
continue
|
||||
|
||||
# Per-key listing (suppressed under --check-only).
|
||||
if missing:
|
||||
print(f" missing ({len(missing)}):")
|
||||
for key in missing:
|
||||
if args.values:
|
||||
val = en.get(key, "")
|
||||
if not isinstance(val, str):
|
||||
val = json.dumps(val, ensure_ascii=False)
|
||||
preview = truncate(val, VALUE_PREVIEW_LEN)
|
||||
print(f" - {key} :: {preview}")
|
||||
else:
|
||||
print(f" - {key}")
|
||||
if extra and not args.no_extras:
|
||||
print(f" extra ({len(extra)}):")
|
||||
for key in extra:
|
||||
print(f" + {key}")
|
||||
|
||||
# ── Trailer ────────────────────────────────────────────────────────
|
||||
print()
|
||||
if total_missing == 0:
|
||||
print(f"✓ Every locale is at parity with {SOURCE_LOCALE}.json.")
|
||||
if total_extra and not args.no_extras:
|
||||
print(
|
||||
f" (Note: {total_extra} extra key(s) across locales — "
|
||||
f"they don't fail the check but may indicate drift.)"
|
||||
)
|
||||
return 0
|
||||
|
||||
print(
|
||||
f"✗ {total_missing} missing translation(s) across "
|
||||
f"{len(locale_with_missing)} locale(s): "
|
||||
f"{', '.join(locale_with_missing)}"
|
||||
)
|
||||
if total_extra and not args.no_extras:
|
||||
print(f" Plus {total_extra} extra key(s); see per-locale output above.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user