Initial commit: hermes-wiki-viewer v0.2.0

Fork of DanielCheer/obsidian-web-viewer (MIT, 2026-04) with Lukas' first
additions for the Hermes Wiki workflow.

Additive customization layer (vault-custom.js) — keeps upstream-merge
trivial via a one-line script tag in vault.html.

Features added:
1. Current-page highlighting in 3D Graph — current node gold+glow,
   connected nodes light-blue, others dim to 20% opacity
2. Click-to-navigate on graph — Three.js raycaster triggers file load
3. Responsive layout — graph collapses below 1024px, tree below 768px
4. URL-based deep-linking via ?file=<path> query param
5. Server-side ?file= support in /api/vault/file/ endpoint

Modified files:
- server.py: +7 lines (Lukas-add: ?file= query param parsing)
- vault.html: +1 line (script tag for vault-custom.js)

New files:
- vault-custom.js: 11KB, all customizations in one place under HermesCustom namespace
- README.md: fork intro, quick start, customization guide
- CHANGELOG.md: Lukas-additions tracking
- docs/ARCHITECTURE.md: design rationale
- docs/CUSTOMIZATIONS.md: feature spec
- .gitignore: standard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-16 15:17:16 +00:00
co-authored by Claude
commit 677a8dde4f
10 changed files with 1374 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
# Architecture
## Why this fork exists
[DanielCheer/obsidian-web-viewer](https://github.com/DanielCheer/obsidian-web-viewer) is a great single-file Obsidian-vault viewer (server.py + vault.html, ~10 KB + 14 KB, Python stdlib only). But for Lukas' Hermes Wiki workflow — 245 Markdown files across 18 top-level folders, frequent cross-referencing via WikiLinks, mobile reading — three specific UX gaps needed fixing:
1. **3D Graph shows all nodes uniformly** — no indication of which node the user is currently reading or which nodes are connected to it
2. **Layout breaks on small screens** — the 3-panel grid (`280px 1fr 260px`) is too narrow on 13" laptops and unusable on phones
3. **Click-to-navigate from graph** — Three.js canvas accepts clicks but doesn't trigger navigation
## Design principles
### 1. Additive layer, not fork-and-modify
We modify **exactly one line** in `vault.html` (a `<script src="vault-custom.js"></script>` tag before `</body>`). Everything else — including the customizations themselves — lives in `vault-custom.js`.
Why:
- Upstream `git pull` conflicts are trivial to resolve (the one line is recognizable)
- Customizations are clearly separated from upstream code
- Reviewing customizations is reading one file, not diffing the whole repo
- Easier to upstream-merge back if Lukas' features prove generally useful
### 2. HermesCustom namespace
All custom code lives under `window.HermesCustom`. Methods are namespaced and self-documenting:
```js
window.HermesCustom = window.HermesCustom || {};
HermesCustom.highlightCurrentNode = function() { ... };
HermesCustom.setupGraphClickHandler = function() { ... };
HermesCustom.responsiveLayout = function() { ... };
```
This way the namespace is visible in DevTools and a future maintainer can see at a glance what's custom vs. upstream.
### 3. Server.py stays mostly upstream
We add **one** enhancement to `server.py`: a `?file=<path>` query parameter that returns the file directly (used by `HermesCustom.highlightCurrentNode` to know which file is open). This is a 5-line change that doesn't conflict with any upstream logic.
If upstream adds a similar feature, we drop our addition.
## File map
```
hermes-wiki-viewer/
├── server.py # obv-fork + 5 lines (file-query-param)
├── vault.html # obv-fork + 1 line (script tag for custom.js)
├── vault-custom.js # ALL Lukas-specific code
├── requirements.txt # PyYAML only (same as upstream)
├── README.md # this repo's entry point
├── LICENSE # MIT (inherited)
├── CHANGELOG.md # release notes for Lukas-additions
├── docs/
│ ├── ARCHITECTURE.md # you are here
│ └── CUSTOMIZATIONS.md # spec for each Lukas-feature
└── .gitignore # standard
```
## Why we keep `vault-custom.js` separate from `vault.html`
Embedding customizations into `vault.html` would mean:
- Every upstream update requires re-applying the entire diff
- Reviewing customizations requires diffing two HTML files
- Conflicts when upstream renames a function we override
A separate file means:
- The customizations are a stable, reviewable unit
- Upstream updates touch `vault.html` only, customizations are unchanged
- The one-line patch in `vault.html` is a clear "anchor point" that any reviewer can understand
## Why Python stdlib only (no Flask/FastAPI)
Upstream uses `http.server` from stdlib. We keep this. Adding a framework would:
- Inflate dependencies
- Require version-pinning for reproducibility
- Make the tool harder to deploy (anywhere with Python 3.8+ works)
The 5-line addition to `server.py` is plain stdlib `BaseHTTPRequestHandler` style.
## Tradeoffs we accepted
- **No build step.** Customizations are vanilla JS. No bundler, no TypeScript. Means no static type-checking, but means anyone can read and modify the code without toolchain setup.
- **No tests in CI.** We rely on manual testing for now. CI tests would add complexity that doesn't match the project's "simple tool" character. (Future: a small Playwright test for the graph highlighting.)
- **No auto-update from upstream.** When obv updates, Lukas has to `git pull upstream && git merge`. The one-line `vault.html` patch needs re-application if upstream modified it. This is acceptable for a personal side-project.
## Roadmap
See CHANGELOG.md for planned additions. Current priorities:
1. ✅ Current-page highlighting in 3D Graph
2. ✅ Click-to-navigate on graph
3. ✅ Responsive layout (1024px and 768px breakpoints)
4. ⏳ Backlinks panel (per-page incoming WikiLinks)
5. ⏳ Keyboard shortcuts (j/k for next/prev file, [/] for nav, g/G for graph focus)
6. ⏳ Light theme variant (currently Catppuccin-dark only)
+120
View File
@@ -0,0 +1,120 @@
# Customizations — Feature Spec
Each section describes one Lukas-feature: what it does, how it works, where the code lives, and known limitations.
## 1. Current-page highlighting in 3D Graph
**What:** When you open a file, the corresponding node in the 3D Graph becomes brighter and larger. Nodes that the open file links to (via `[[wikilinks]]`) also brighten. All other nodes dim.
**How:**
1. `HermesCustom.getCurrentFile()` reads the file path from `?file=` URL param (preferred) or tracks via the `loadFile()` hook
2. `HermesCustom.getConnectedFiles(currentFile)` looks up outgoing edges in the global graph data
3. `HermesCustom.highlightCurrentNode()` iterates `window.graphNodeObjects` (obv's Three.js node meshes) and adjusts `material.color`, `emissive`, `scale`, `opacity`
**Code:** `vault-custom.js`, methods under namespace `HermesCustom.*`.
**Materials used (Catppuccin palette, matches theme):**
- Current: `#FFD700` (gold) + emissive glow + 1.5x scale
- Connected: `#89b4fa` (light blue) + faint emissive + 1.1x scale
- Other: `#313244` (muted) + 20% opacity (transparent)
**Known limitation:**
- Uses `window.graphNodeObjects` global from obv — if obv renames this, we need to update. Mitigation: detection in `highlightCurrentNode` waits up to 500ms for obv's graph to be ready.
- Highlight doesn't preserve selection across page reload — fresh fetch on every load.
## 2. Click-to-navigate on 3D Graph
**What:** Clicking a node in the 3D Graph navigates to that file's page.
**How:**
1. `HermesCustom.setupGraphClickHandler()` sets up a Three.js Raycaster on the canvas DOM element
2. On click, computes normalized mouse coords, raycasts into scene
3. First intersection's `userData.nodeId` is the file path
4. Calls `window.loadFile(nodeId)` (obv's existing loader)
5. Updates URL with `window.history.pushState({}, '', '?file=<path>')`
**Code:** `vault-custom.js`, ~25 lines.
**Known limitation:**
- Three.js raycasting requires nodes to have `userData.nodeId` set — verified in obv's `addNodeToScene` function (line ~310 in obv's graph render). If obv removes this, our handler silently does nothing.
- No keyboard navigation in graph (yet). Roadmap item.
## 3. Responsive Layout
**What:** The 3-panel desktop layout collapses gracefully on smaller screens.
**Breakpoints:**
- **Desktop (≥1024px):** Full 3-panel layout — Tree 240px | Content 1fr | Graph 240px
- **Tablet (768px1023px):** Tree 240px | Content 1fr | Graph 200px (bottom panel)
- **Mobile (<768px):** Tree as hamburger menu (off-canvas, slides in from left) | Content full-width | Graph hidden
**How:**
- `HermesCustom.injectResponsiveCSS()` adds `<style>` tag with media queries
- `HermesCustom.responsiveLayout()` adds `.mobile-mode` / `.tablet-mode` classes to `.vault-app` based on `window.innerWidth`
- Resize handler is throttled (150ms) to avoid jank
- Hamburger toggle button is appended to header in mobile mode
**Code:** `vault-custom.js`, ~80 lines (including CSS string).
**Known limitation:**
- The injected CSS uses `!important` to override obv's `grid-template-columns`. If obv restructures the layout, we may need to adjust selectors.
- Hamburger menu doesn't auto-close on file selection — minor UX nit. Roadmap.
## 4. URL-based navigation (?file=)
**What:** The URL contains the currently-open file as a `?file=<path>` query param. This makes pages deep-linkable and back-button friendly.
**How:**
- `HermesCustom.getCurrentFile()` reads `?file=` from `URLSearchParams`
- `vault-custom.js` updates URL via `history.pushState()` on file load
- Server.py patch accepts `?file=` in API calls
**Server-side patch (server.py):**
```python
# Lukas-add: also support ?file=<path> query for clean URL navigation
if "?" in rel:
rel, _, query = rel.partition("?")
from urllib.parse import parse_qs
file_param = parse_qs(query).get("file", [None])[0]
if file_param:
rel = file_param
```
**Known limitation:**
- Server-side patch assumes `?file=` is the only query parameter obv might add. If obv adds another (e.g. `?theme=`), the simple `partition("?")` still works but is naive. Acceptable for current state.
## 5. Tracking current file via loadFile hook
**What:** `vault-custom.js` patches obv's `window.loadFile` to remember the last loaded path. Used as a fallback when the URL doesn't have `?file=` (e.g., when obv opens a file from search without updating URL).
**How:**
```js
const originalLoadFile = window.loadFile;
window.loadFile = function(path) {
HermesCustom._lastLoadedFile = path;
const result = originalLoadFile.apply(this, arguments);
setTimeout(HermesCustom.highlightCurrentNode, 300);
return result;
};
```
**Known limitation:**
- The 300ms delay assumes the render finishes within that time. Slow devices may need a longer delay. Could be improved by hooking into a render-completion signal if obv provides one.
---
## Testing
Manual testing only for now. Test cases:
| Scenario | Expected |
|----------|----------|
| Open `concepts/agent-reference-model.md` | Graph: that node gold + glow, connected nodes light blue, others dim |
| Open file via search (not via tree click) | Same highlighting (URL gets `?file=`) |
| Click graph node | File loads, URL updates |
| Resize browser to <1024px | Graph moves to bottom panel |
| Resize browser to <768px | Tree becomes hamburger, graph hidden |
| Hard reload on file URL | File loads directly (server.py parses `?file=`) |
| Upstream obv update (theoretical) | Only 1 line of conflict in vault.html, easy merge |
Planned: Playwright-based automated test for the 5 visual features.