# Round B/C spec — Obsidian vault compatibility, view/admin modes, scale > **Historical.** This is the specification the compatibility work was built from, kept as a record > of the decisions. What a vault kept in Obsidian gets today is documented in the manual where each > feature lives: [the editor](docs/editor.md), [drawings](docs/drawing.md#in-obsidian), > [trackers](docs/trackers.md#in-obsidian), [sigils](docs/sigils.md#in-obsidian) and > [orbits](docs/orbits.md#where-the-schedule-lives). Target vault reality: real Obsidian vaults with thousands of notes and heavy media — nested folders, attachments (png/jpg/svg/pdf/…) alongside the markdown, and a `.obsidian/` config dir (must be ignored everywhere: tree, index, graph, watcher). ## B1. Attachments (server) - `GET /api/file?path=` serves any non-md vault file (png/jpg/jpeg/gif/webp/svg/pdf/mp4/mp3…) with correct Content-Type, ETag from mtime+size, and the same path-safety as notes. 404 others. - Tree/index still list only .md, but `resolveEmbed(name)` resolves attachment basenames (case-insensitive, shortest-path) like wikilinks — expose `GET /api/resolve?name=` returning `{ path } | 404` for both notes and attachments. - **Uploads honour Obsidian's own setting.** `settings.attachments.mode` is named after Obsidian's "Default location for new attachments" and offers the same four choices (vault root / same folder as the note / subfolder of the note's folder / one specified folder), so a migrating vault keeps writing attachments where its owner already expects. `POST /api/upload` accepts every kind an Obsidian vault holds — images, PDF, audio, video — sniffed by magic number. Existing attachments are never moved by a change of the setting. See CONTRACTS.md, "Attachments". ## B2. Editor rendering (client, live preview widgets) - `![[image.png]]` and `![[image.png|300]]` → inline widget (max-width 100%, optional px width from alias), loaded from /api/file; broken → dashed placeholder with filename. - Standard `![alt](relative or URL)` images likewise (vault-relative resolved against note dir) — **and on the PUBLISHED site too**: the indexer records those destinations as `NoteRecord.assets`, resolved against the note's folder, so `allowedAttachments()` sees them. It only saw `[[wikilink]]` embeds before, which meant every markdown-embedded image in a published note was a 404 to every visitor while the admin saw it and nothing said why. - `![[Note]]` note embed → rendered transclusion card: note title header + rendered content (read-only, depth 1, cycle-safe), gold left border. - `![[file.pdf]]` → card link that opens /api/file in new tab. - Callouts: `> [!note] Title` (+ tip/info/warning/danger/quote/example, case-insensitive, foldable with `-`) → styled callout block in live preview: tinted bg, icon, bold title; body renders as usual. Colors per type from tokens (add --callout-* tokens). - `==highlight==` → gold-soft background mark. `%%comment%%` → hidden in preview, faint when cursor inside. Footnotes `[^1]` → superscript link. - Math: `$inline$` and `$$block$$` via KaTeX (add dependency; render in widget when cursor outside). Code fences: CM6 syntax highlighting for common langs (@codemirror/language-data). - Frontmatter: when cursor outside the `---` block, collapse it to a neat properties card (key: value chips; tags clickable). - All of this in live preview; a separate READING VIEW (Ctrl/Cmd+E toggles editor⇄reading) renders the full note to HTML (same features, plus TOC in the right panel above backlinks) — implement as a client-side renderer sharing the resolve logic. Reading view is the default for non-admin visitors (see C1). - **Trackers are an Astrolabe extension, and they degrade honestly.** A ` ```tracker ` fence renders as a progress card here (see [docs/trackers.md](docs/trackers.md)); opened in Obsidian, the same note shows a labelled code block whose every line is readable — `title: Elden Ring`, `progress: 62/130`. Nothing is converted and nothing is hidden, which is the same bargain templates make (`client/templates.ts`): a feature the other app does not have must leave the file saying exactly what it said. The same is true of ` ```tracker-board `, which degrades to its own (usually empty) fence. ## B3. Obsidian niceties - Daily note command (palette + Ctrl/Cmd+D): opens/creates `daily/YYYY-MM-DD.md`. - `[[Note#Heading]]` navigates to the heading; heading autocomplete after `#` inside `[[ ]]`. - Unresolved wikilinks styled dashed; clicking one creates the note (admin only). - Outline/TOC panel section (right panel): headings of open note, click scrolls. - Excalidraw drawings: the plugin's `.excalidraw.md` (plain or compressed scene) and Excalidraw's own `.excalidraw` both open in a full canvas, save back in the same spelling (frontmatter, compression and the `## Text Elements` list kept), and `![[sketch.excalidraw]]` renders the svg exported beside the file — the plugin's auto-export name. A vault with `.obsidian/` gets new drawings in the plugin's spelling. See [docs/drawing.md](docs/drawing.md). ## C1. Auth: public view / admin edit - `.env`: `ADMIN_PASSWORD_HASH` (argon2id — add `argon2` dep), `SESSION_SECRET`, optional `PUBLIC=false` to require login even for viewing. No hash set → open admin (local mode) with a startup warning. - `POST /api/login {password}` → argon2 verify → httpOnly signed session cookie (HMAC of SESSION_SECRET, 30d); `POST /api/logout`; `GET /api/me` → `{ admin: boolean, public: boolean }`. - When a hash IS set: all mutating endpoints (PUT/POST/DELETE note/folder/rename) require admin session → 401 otherwise. GET endpoints stay public unless `PUBLIC=false`. - Client: on boot fetch /api/me. Non-admin → reading view only, no editor, no create/rename/delete UI (hide, don't just disable), status bar shows a quiet "sign in" link → minimal login modal. Admin → full editor. Login state changes rerender without reload. Rate-limit login (in-memory, 10/min/IP). ## C2. Scale (1.4k notes, 738MB) - Indexer: build once at boot (stream files, skip >2MB md), incremental on watch events; boot under 3s on the real vault. Watcher: ignore `.obsidian`, `.git`, `.trash`, dotfiles. - Tree: render collapsed at depth>1 by default for huge vaults; folder toggle stays O(subtree). - Graph: cap physics with Barnes-Hut or spatial grid; 1.4k nodes at interactive fps; labels only when zoomed/hovered; initial layout seeded deterministically (hash of path) so it's stable. - Search debounce + cancel stale; snippet generation lazy. - `/api/file` streams (no full-buffer reads) for large media. ## Config file - Support `.env` (loaded via `node --env-file=.env` in npm scripts, document in README) with: `PORT`, `ASTROLABE_VAULT`, `ADMIN_PASSWORD_HASH`, `SESSION_SECRET`, `PUBLIC`, `HOME_NOTE` (note opened for visitors on first load — the vault's homepage). - `npm run hash-password` script → prompts, prints argon2 hash (scripts/hash-password.ts). ## Non-negotiables - Never write outside the vault; never serve dotfiles; `.obsidian` untouched. - Real-vault smoke: boot against a READ-ONLY COPY of a real Obsidian vault (copy a subset ~200 notes + their images to a scratch dir for tests; never point tests at a live vault). - Keep DESIGN.md aesthetics for every new surface (callouts, login modal, reading view, TOC).