VitePress-based personal site with eight fully client-side apps and one content page:
/idef0— IDEF0 diagram editor (SVG + Vue 3, FIPS 183)/journal— private encrypted daily journal (WebCrypto AES-GCM, IndexedDB, 500-words/day, file-based sync)/piano— interactive MIDI piano teacher (Web MIDI API, VexFlow notation, IndexedDB progress)/openpose— OpenPose pose editor (MediaPipe BlazePose WASM, drag-edit skeletons, ControlNet PNG + JSON export)/planner— encrypted project/task planner (WebCrypto AES-GCM, IndexedDB, kanban + list, encrypted.plannerexport/import)/decision-journal— encrypted decision journal with calibration (Brier score + confidence-bucket table)/finance— encrypted personal finance tracker (WebCrypto AES-GCM, IndexedDB, expenses/accounts/RU-market investment portfolio, deposit top-ups, on-demand MOEX ISS price refresh, local investment recommendations, encrypted.financeexport/import)/casual-games— casual puzzle collection (Queens, Tango, Zip, Klondike solitaire; seeded level generation, hints, scoring, local IndexedDB persistence)/music— music page: Alterfo albums (Яндекс.Музыка), lazy embed player
Plus two external apps served as subpaths (not in-tree client apps, copied into dist/<subpath>/ at deploy, mirroring each other):
/ar— Аудио-реактив (ar-engine/, SEO hand-maintained inar-engine/web/index.html)/vacuum-rogues— browser game (private repoalterfo/vacuum-rogues, Vite + PixiJS + onnxruntime-web; vendored as a git submodule atvacuum-rogues/, built in CI gated on theVACUUM_ROGUES_DEPLOY_KEYsecret, placeholder until then). Its entry point is the ship-ranger logo in the center of the LifeCircle donut on the home page (<a href="/vacuum-rogues/" target="_self">).
| Path | Role |
|---|---|
.vitepress/config.mts |
VitePress config, SEO hooks, sitemap |
.vitepress/theme/index.mts |
Theme entry: global component registration |
.vitepress/theme/components/crypto.js |
Shared WebCrypto substrate (PBKDF2 → AES-GCM); reused by journal, planner, decisions |
.vitepress/theme/components/IDEF0Editor.vue |
IDEF0 root (<ClientOnly>, defineAsyncComponent); modules in IDEF0Editor/ |
.vitepress/theme/components/Journal.vue |
Journal root (<ClientOnly>, defineAsyncComponent); modules in Journal/ |
.vitepress/theme/components/Piano.vue |
Piano root (defineAsyncComponent); modules in Piano/ |
.vitepress/theme/components/OpenPoseEditor.vue |
OpenPose root (defineAsyncComponent); modules in OpenPose/ |
.vitepress/theme/components/PlannerEditor.vue |
Planner root (defineAsyncComponent); modules in Planner/ |
.vitepress/theme/components/DecisionJournal.vue |
Decisions root (defineAsyncComponent); modules in Decisions/ |
.vitepress/theme/components/FinanceApp.vue |
Finance root (defineAsyncComponent); modules in Finance/ |
.vitepress/theme/components/CasualGames.vue |
Casual games root (defineAsyncComponent); modules in CasualGames/ |
.vitepress/theme/components/MusicAlbums.vue |
Music page component |
.vitepress/theme/components/spectrum.js |
Design system JS mirror (palette, CANVAS_PALETTE, PROJECT_COLORS) |
posts.data.ts |
VitePress data loader: reads posts/*.md, parses frontmatter, extracts excerpt |
blog.md |
Blog listing page at /blog — uses <BlogList :posts="posts" /> |
All eight app roots are registered in .vitepress/theme/index.mts via defineAsyncComponent(() => import('...')) — global component names (IDEF0Editor, Journal, Piano, OpenPoseEditor, PlannerEditor, DecisionJournal, FinanceApp, CasualGames) stay the same, but the editor source/deps are split into their own lazy chunk and never ship in the entry/app chunk (incl. on the home page, which never renders them). New app roots should follow the same pattern.
For detailed docs on each app, see CLAUDE.md in the relevant module subfolder.
For design system, SEO, typography, and VitePress gotchas, see .vitepress/CLAUDE.md.
# Dev server (use npm, not yarn — yarn is broken)
npm run dev
# Build
npm run build
# Unit tests
node --test .vitepress/theme/components/crypto.test.mjs
node --test .vitepress/theme/components/Journal/*.test.mjs
node --test .vitepress/theme/components/IDEF0Editor/model.test.mjs
node --test .vitepress/theme/components/Piano/*.test.mjs .vitepress/theme/components/Piano/importer/*.test.mjs
node --test .vitepress/theme/components/OpenPose/*.test.mjs
node --test .vitepress/theme/components/Planner/store.test.mjs
node --test .vitepress/theme/components/Decisions/vault.test.mjs .vitepress/theme/components/Decisions/stats.test.mjs
node --test .vitepress/theme/components/Finance/vault.test.mjs .vitepress/theme/components/Finance/stats.test.mjs .vitepress/theme/components/Finance/prices.test.mjs .vitepress/theme/components/Finance/components.render.test.mjs
node --test .vitepress/theme/components/CasualGames/*.test.mjs
node --test .vitepress/theme/components/music.test.mjs
node --test .vitepress/theme/components/spectrum.test.mjs .vitepress/theme/components/ConnectingParticles.test.mjs .vitepress/theme/components/countdown.test.mjs .vitepress/theme/components/lifecircle.test.mjs .vitepress/theme/components/lifecircle-mirrors.test.mjs .vitepress/theme/components/headerLifecycle.test.mjs
node --test .vitepress/seo.test.mjs
node --test .vitepress/theme/components/onboarding.test.mjs
vitepress preview404s new hashed assets after rebuild — restart preview after every build.- CDP debugging:
chrome --headless=new --remote-debugging-port=N --user-data-dir=/tmp/x about:blank; create tab via/json/new?url=about:blankthenPage.navigate; subscribe toRuntime.exceptionThrown; WebGPU canvases only viaPage.captureScreenshot. - Plan files:
docs/plans/(git-ignored, local only). - MOEX ISS runtime-fetch exception: every app in this repo makes zero runtime calls to external hosts, except
/finance'sFinance/prices.js, which callsiss.moex.com(official Moscow Exchange ISS API) to fetch current prices for held RU-market tickers. This is a deliberate, narrow, user-approved exception — one host, no API key, user-initiated only (a "refresh prices" button, never automatic/polling), only the public ticker symbol sent. Now queries at most two boards per ticker (shares first, bonds as fallback onunknown-ticker) — still the same one host, no new inputs. SeeFinance/CLAUDE.mdfor detail. Not an accidental CDN/runtime-fetch violation. - Known deferred advisories (4 GHSA IDs across 2 packages, GitHub-flagged: 1 high, 3 moderate;
vitepress/vite/esbuildaredevDependenciesonly — none of this ships in the built static site, all dev-server/dev-tooling only):- GHSA-67mh-4wv8-2f99 — esbuild ≤0.24.2 dev-server CORS (moderate)
- GHSA-fx2h-pf6j-xcff — vite ≤6.4.2
server.fs.denybypass on Windows alternate paths (high) - GHSA-4w7w-66w2-5vf9 — vite path traversal in optimized-deps
.maphandling (moderate) - GHSA-v6wh-96g9-6wx3 —
launch-editor(vite's click-to-open-in-editor) NTLMv2 hash disclosure via UNC path on Windows (moderate) - No fix available for any of them (
npm audit fixconfirms) — all are transitively pinned byvitepress@1.6.4'svite@5.4.21/esbuild@0.21.5.vitepress@2.0.0is still alpha (alpha.18as of 2026-07-12), not stable — re-evaluate once it ships stable and bumps the vite major. - 2026-08-20:
npm audit fixcleared 5 additional transitive advisories (js-yaml, nanoid, postcss, tar — incl. one critical DoS, undici) that had crept in since this list was last written — all patch/minor bumps within existing semver ranges (package.jsonuntouched, onlypackage-lock.json), verified via the full test suite +npm run build. GitHub's Dependabot count can drift ahead of this list between sessions (it flagged 15 open alerts vs. the 4 recorded here before this pass) — ifgit pushreports a vulnerability count that doesn't match this section, re-runnpm auditbefore assuming it's just these 4.
Repo is GitNexus-indexed; prefer mcp__gitnexus__* over grep. Before editing a symbol: impact({target, direction:"upstream"}). Before committing: detect_changes({scope:"compare", base_ref:"master"}). Explore with query/context; rename via rename (not find-replace). Refresh: node .gitnexus/run.cjs analyze. Full rules: .claude/skills/gitnexus/*/SKILL.md and AGENTS.md (Claude Code does not load AGENTS.md, so the detail there is token-free in-session).