feat: admin page with dynamic converter loading

Implements full admin interface for managing units and groups.
Migrates converter from static imports to server-side data loading.

- Switch adapter-static → adapter-node
- Add bcryptjs session auth with HMAC-signed cookies
- Add data/units.json and data/config.json data layer
- Add atomic file writes via temp-file rename
- Add public GET /api/units endpoint
- Add auth-gated admin CRUD API for units and groups
- Add two-panel admin UI with group tree and edit forms
- Add YOLO mode toggle for cross-group conversions
- Add visual group dividers in converter results grid
- Update ResultItem type for nullable convertedValue and isYolo flag
- Group deletion supports reassign/orphan with toBase recalculation

Rollback point: 067fd44

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Falkan
2026-03-17 13:55:11 -04:00
parent 067fd445ef
commit d1f51c141e
27 changed files with 1915 additions and 81 deletions

40
src/lib/server/data.ts Normal file
View File

@@ -0,0 +1,40 @@
import { readFileSync, writeFileSync, renameSync } from 'fs';
import { resolve } from 'path';
import type { UnitsData } from '$lib/types';
// Path is relative to project root (where the server runs from)
const DATA_DIR = resolve('data');
const UNITS_PATH = resolve(DATA_DIR, 'units.json');
const CONFIG_PATH = resolve(DATA_DIR, 'config.json');
export interface AppConfig {
adminPath: string;
passwordHash: string;
sessionSecret: string;
}
/** Read units.json synchronously. Returns parsed UnitsData. */
export function loadData(): UnitsData {
const raw = readFileSync(UNITS_PATH, 'utf8');
return JSON.parse(raw) as UnitsData;
}
/** Write full UnitsData atomically (write to temp, rename). */
export function saveData(data: UnitsData): void {
const tmp = UNITS_PATH + '.tmp';
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf8');
renameSync(tmp, UNITS_PATH);
}
/** Read config.json synchronously. */
export function loadConfig(): AppConfig {
const raw = readFileSync(CONFIG_PATH, 'utf8');
return JSON.parse(raw) as AppConfig;
}
/** Write config.json atomically. */
export function saveConfig(config: AppConfig): void {
const tmp = CONFIG_PATH + '.tmp';
writeFileSync(tmp, JSON.stringify(config, null, 2) + '\n', 'utf8');
renameSync(tmp, CONFIG_PATH);
}