vitrine ======= A tiny read-only webserver for putting local source trees on display. Point it at some directories via a small config file, and it serves them: markdown renders to HTML, directories become listings, and `?fmt=raw` on any URL serves the file bytes directly. No javascript required, no uploads, no auth, no dynamic anything beyond rendering. (URL prefixes can also forward to other local services; see Forwarding below.) Config ------ vitrine takes exactly one config file, via the required `-config` flag (there is no default location and no search). The format is YAML: ```yaml exhibits: moonshot-1: path: "%" meta/research/some-topic: path: ../elsewhere/some-topic some-repo: path: ../src/some-repo expose-git: true ``` - `exhibits` maps URL prefixes to directories. A prefix may contain slashes; the intermediate URL levels then serve synthetic listings that reveal only the mounted subtrees. - `path` (required) is the directory to serve. The placeholder `"%"` means "same as the exhibit name" -- the common case where the URL space mirrors the directory layout. Relative paths resolve against the config file's directory, which makes a config portable alongside the trees it describes. Absolute paths also work. Trailing slashes on names and paths are accepted and normalized away. - `expose-git` (optional, default false): see below. - `raw` (optional, default false): see below. A bad config (parse error, unknown key, missing path, or nothing configured at all) is a startup error: vitrine refuses to run rather than serve the wrong thing. An exhibit whose directory is absent is not a bad config, though: that exhibit is skipped with a warning and the rest serve normally, so one config can describe trees that only some hosts happen to have. Config changes apply live: vitrine watches the config file (inotify for immediate pickup, plus a 5-second stat poll as backstop, so a change lands within ~5s worst case, usually instantly). Edit freely; a typo mid-edit logs a rejection and leaves the running server untouched. Forwarding ---------- Besides serving file trees, vitrine can reverse-proxy URL prefixes to other services, so one publicly routable port can carry both the docs and, say, a game server: ```yaml exhibits: some-repo: path: "%" forwards: /: to: http://localhost:8866 tools/dashboard: to: http://localhost:9090 ``` - `forwards` maps URL prefixes to backend base URLs, sibling to `exhibits`. Names share the URL namespace with exhibits (a collision is a config error) and appear in listings like any other mount. - The name `/` is the root forward: it catches every request that no exhibit, forward, or listing claims. Exhibits keep winning at their own prefixes -- dispatch is longest-prefix-match -- so a backend at the root simply never sees URLs that collide with an exhibit prefix, its ancestors, or the reserved `/-/` prefix. - `to` must be an absolute `http://` or `https://` URL; the scheme is mandatory. Websocket backends are also `http://`: a websocket connection begins as an ordinary http request with an `Upgrade` header, which the proxy passes through, so websockets and SSE streams work with no extra configuration. - A path on the `to` URL becomes a prefix on every proxied request. - The mount prefix is stripped before proxying: a backend behind `tools/dashboard` sees `/x`, not `/tools/dashboard/x`. It should therefore use relative URLs in its pages (or be told its public prefix by its own means; vitrine does not rewrite response bodies). - An unreachable backend answers 502 per-request. Backend liveness is deliberately not part of config validation: backends come and go while vitrine runs. Forwarding is one of two deliberate exceptions to "read-only, no dynamic anything" (the other is `expose-git`); vitrine adds no auth, no TLS, and no rewriting -- the forward is a pane of glass to another local port. Theming ------- Each host wears its own accent color, derived from its hostname, so when several vitrines from several hosts are open in tabs, each is recognizable at a glance. The hostname's first dot-segment (e.g. `langoustine` from `langoustine.exe.xyz`) hashes to a hue; that name is also shown next to the site name in the nav, so the color never has to carry the meaning alone. The derivation is deterministic: the same hostname always yields the same color, on every build and every host. The `-theme-seed` flag substitutes any string for the hostname segment -- useful for testing, or if you dislike the color your hostname landed on. Pages follow the browser's `prefers-color-scheme`, light or dark, with no in-page toggle (vitrine ships no JavaScript, so there is nowhere to store one). Both palettes are picked to clear WCAG AA contrast (4.5:1) against their background, and server-rendered diagrams (see Diagrams below) invert to match. Callouts -------- Markdown can carry callout boxes (notes, warnings, tips, and the like) using colon fences: ``` :::warning Optional title text body content, which is **full markdown** ::: ``` - A line of three-or-more colons followed by a non-empty remainder opens a callout. The first word of the remainder is the callout *type*; anything after it is an optional *title*. - A line of three-or-more colons alone closes it. - The body is ordinary markdown: lists, emphasis, links, and code all work inside. The rendered box is `
` (matching the markdown-it-container / VuePress convention, so docs authored for those tools render as intended). The styled types are `info`, `note`, `tip`, `success`, `todo`, `warning`, `danger`, and `reject`, each with its own accent color and icon; `info` and `note` ride the per-host accent. `warn` is accepted as an alias for `warning`, and the GitHub alert types `caution` and `important` map onto `warning` and `note`. An unknown type still renders as a plain neutral box. Callouts do not nest, by design, and an unterminated callout simply runs to the end of the document rather than breaking it. Diagrams -------- A fenced code block tagged `dot` (or its alias `graphviz`) renders server-side to inline SVG, so diagrams need no JavaScript in the page. Write the DOT source in a normal fenced block: ```` ```dot digraph { rankdir=LR; a -> b -> c; b [shape=box]; } ``` ```` - The source language is Graphviz's DOT. Rendering runs entirely in-process via an embedded Graphviz WebAssembly build, so there is no external `dot` binary to install and no browser involved. - A diagram whose DOT fails to compile does not break the page: it falls back to showing its own source in a code block, flagged with the parse error, so the author sees exactly what did not render. - Any other code language (or none) renders as an ordinary code block, unchanged. raw --- By default, a file served without `?fmt=raw` gets the rendered view: markdown becomes HTML, and anything else is framed in an iframe. Setting `raw: true` on an exhibit drops that frame entirely: every file serves as its raw bytes, as if `?fmt=raw` were always present (markdown included -- it serves as `text/plain` like any other source file). This turns an exhibit into a plain static file server, which is what you want for a tree of assets meant to be consumed directly (by tools, by ``/`` references, or by download) rather than read through vitrine's chrome. Directories are unaffected: they still render the normal listing. expose-git ---------- By default, `.git` directories are invisible: hidden from listings and unreachable by direct URL. Setting `expose-git: true` on an exhibit flips that: `.git` lists and serves like any other directory, which makes `git clone http://host/exhibit/.git` work over git's dumb HTTP transport. Two caveats, read them before enabling: - **vitrine writes into that exhibit's `.git`.** Dumb-HTTP clone requires `.git/info/refs` and `.git/objects/info/packs`, and ordinary commits never maintain them, so vitrine runs `git update-server-info` on the exhibit at startup and at every successful config reload. This is a deliberate exception to vitrine's otherwise strictly read-only stance. - **Dumb HTTP has dumb semantics.** Clones reflect the refs as of the last `update-server-info` run, so commits made after the last startup or config reload are invisible to clones until the next one (touching the config file suffices to trigger a reload). A stale clone succeeds silently at the old tip; there is no warning. Concurrent `git gc` in the exhibit can also break an in-flight clone (loudly; retry works). Deployment ---------- Run vitrine as your own user, under a systemd user unit. No root anywhere. 1. Build and install the binary somewhere on your user's path: ``` go build -o ~/bin/vitrine . ``` 2. Write your config, e.g. `~/fusion/.vitrine.cfg`, with paths relative to that directory (see Config above). 3. Install the unit file from this repo as a *user* unit: ``` mkdir -p ~/.config/systemd/user cp vitrine.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now vitrine ``` Adjust `ExecStart` in the unit if your binary or config live elsewhere. 4. Keep it running after logout (user units otherwise stop with your session): ``` loginctl enable-linger $USER ``` Check on it with `systemctl --user status vitrine` and `journalctl --user -u vitrine -f`.