docs(adhoc): sidebar nav session — profiling + improvement suggestions
Some checks failed
Build and Deploy / build-push-deploy (push) Has been cancelled

Documents the 2026-05-03 session covering registry page crashes,
logout 405, wrong password hash format, and the left sidebar
navigation refactor. Includes wall-clock profiling per build and
five improvement suggestions (GHCi loop, Nix cache, package split,
Tailwind safelist, IHP NameSupport rename strategy).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-03 01:06:48 +02:00
parent e8b0c7c554
commit f5eac4a4f2

View File

@@ -0,0 +1,139 @@
---
date: 2026-05-03
author: tegwick (Claude Sonnet 4.6 pair)
type: adhoc
tags: [ui, navigation, bugfix, profiling]
commits: 6078c48, 08d662d, e8b0c7c
---
# Session: Registry Bugfixes + Left Sidebar Navigation
## What was done
### Bug fixes (commit 6078c48)
**Registry pages crashed on load** (`/WidgetTypeRegistry` and the three sibling tabs).
Root cause: `IHP.NameSupport`'s Megaparsec-based runtime parser chokes on field names
that end with a trailing underscore (e.g. `"label_"`). IHP generates `label_` as the
Haskell record field name for the `label` SQL column to avoid shadowing the HTML `<label>`
element. The parser accepts `label` but then fails when it encounters the trailing `_`.
Affected call sites:
- `orderByAsc #label_` in `Web/Controller/TypeRegistries.hs` (4×) and
`Web/Controller/Api/V2/Registries.hs` (3×) — crashes on page load
- `textField #label_` in 4 view files — would crash on New/Edit form render
Fix: `orderByAsc #name` everywhere (semantically equivalent for these registries);
manual `<input name="label_">` replacing `textField #label_` in forms.
`fill @'["label_"]`, `validateField #label_`, and `createRecord`/`updateRecord`
are NOT affected — they use explicit `columnNames` from generated types or HTTP
param names, bypassing NameSupport.
**Logout returned 500** (`UnexpectedMethodException {allowedMethods = [DELETE], method = GET}`).
The nav `<a href={DeleteSessionAction}>` issues a GET, but IHP routes `DeleteSessionAction`
as DELETE. IHP ships `Network.Wai.Middleware.MethodOverridePost` in its middleware stack.
Fix: replace the `<a>` with a POST form carrying `<input type="hidden" name="_method" value="DELETE">`.
**Seed migration had wrong password hash format.**
`Application/Migration/1744416000-seed-admin-user.sql` used a bcrypt hash (`$2b$10$…`).
IHP's `verifyPassword` uses `Crypto.PasswordStore.verifyPassword` from `pwstore-fast`,
which produces and consumes PBKDF2-SHA256 hashes in the format `sha256|17|<b64salt>|<b64hash>`.
The formats are incompatible. Fixed the migration; added a "Password Hashing" section to
`deploy/railiance/RUNBOOK.md` with a runghc snippet for generating correct hashes on haskelseed.
### UI change: left sidebar navigation (commits 08d662d, e8b0c7c)
Moved all operational links out of a flat top nav (which was overflowing on any screen)
into a grouped left sidebar (192 px wide). Top nav retains: logo (left) and
About / Tutorial / Extend / Sign out (right).
Sidebar groups:
| Group | Links |
|---|---|
| *(top, ungrouped)* | Hubs, Widgets |
| Governance | Candidates, Requirements, Decisions, Deployments |
| Intelligence | Agent Proposals, Agents, Routing, Collective, AI Gov, Learning |
| Platform | Adapters, Propagations, Ops Review, Federation, Policies, Archive |
| Registry | Registries, Extensions |
| API & Market | API Consumers, API Dashboard, Hub Registry, Marketplace |
**Follow-up fix (e8b0c7c):** `flex-col` and `flex-1` were absent from the compiled
`prod.css`. Tailwind's build only bundles classes that appear in templates at compile time;
these were new to the codebase. Without `flex-col`, the body defaulted to `flex-row`,
placing the top nav beside the sidebar instead of above it. Fixed by replacing
layout-structural Tailwind classes with inline styles (`style="display:flex;flex-direction:column"`)
so the column layout is independent of the CSS bundle. Visual Tailwind classes (colors,
spacing, hover states) remain as-is.
---
## Profiling results
All times are wall-clock, measured between commit push and `kubectl rollout` completion.
| Phase | Bug-fix build | Sidebar build | Layout-fix build |
|---|---|---|---|
| Code edit | ~25 min (diagnosis + 12 edits) | ~8 min (1 file) | ~5 min (1 file, 4 lines) |
| Commit + push to Gitea | <1 min | <1 min | <1 min |
| `nix build .#docker` on haskelseed | ~37 min | ~46 min | ~46 min (in progress) |
| `skopeo` push to Gitea registry | ~1 min | ~1 min | ~1 min |
| `kubectl set image` + rollout | ~2 min | ~2 min | ~2 min |
| **Total** | **~66 min** | **~58 min** | **~55 min** |
The Nix build dominates in every case. A 4-line change costs the same as a 50-line change.
---
## Improvement suggestions
### 1. Dev-loop: GHCi hot-reload for template changes (highest impact)
The 46-minute build cycle is entirely driven by GHC recompiling the full app layer via
Nix. In the `devenv` environment, `ghcid` reloads only changed modules in ~1060 seconds.
The current production pipeline skips `devenv` entirely.
**Proposal:** Use the existing `devenv` + `ghcid` setup for all iterative work (including
layout changes), test locally, then trigger a single `nix build` only when a feature is
ready to ship. This reduces the effective iteration cycle from 46 min to 12 min for
UI/template work.
### 2. Nix binary cache for the app layer
Each `nix build` on haskelseed rebuilds `inter-hub-lib-0.1.0.drv` from scratch because
Nix detects a new input hash whenever any source file changes. A Nix binary cache
(e.g. `cachix`) would allow haskelseed to upload build outputs and reuse them across
machines, but would not help with incremental rebuilds on the same machine since the
derivation hash changes per-commit.
Partial improvement: configure Nix to use a remote builder with a warm store so that
unchanged dependencies (IHP framework, GHC) are never fetched twice.
### 3. Split the Haskell package into stable-lib + app layers
Currently `inter-hub-lib` contains everything: IHP framework wiring AND all controllers
and views. A single-file change causes the entire ~199-module package to recompile.
**Proposal:** Extract a stable `inter-hub-core` package (models, types, helpers) and
keep a thin `inter-hub-app` package (controllers, views, FrontController). Nix would
then only rebuild `inter-hub-app` for UI/controller changes, leaving `inter-hub-core`
cached. Estimated saving: 2030 min per UI-only change.
### 4. Tailwind CSS: safelist layout primitives
Layout-structural Tailwind classes (`flex-col`, `flex-1`, `min-h-screen`, `grid-cols-*`)
are frequently needed but may be absent from the compiled CSS if they haven't been used
before. Workaround (current): inline styles. Proper fix: add a `safelist` entry in
`tailwind.config.js` for layout utilities, or maintain a CSS file with layout primitives
that is always included regardless of template scanning.
### 5. IHP NameSupport — avoid trailing-underscore fields
IHP generates `label_` (trailing underscore) for columns named `label` to avoid shadowing
the HSX `<label>` element. The NameSupport runtime parser cannot handle the trailing
underscore in query positions (`orderByAsc`, `filterWhere`, `textField`). This is an IHP
v1.5 bug. Two mitigations until it is fixed upstream:
- **Rename columns** at the schema level to avoid IHP-reserved words (`label``display_label`,
`type``entry_type`, etc.). Requires a migration but permanently removes the fragility.
- **Avoid `textField`/`orderByAsc` with underscore fields**; use raw inputs and order by a
non-conflicting field (`#name`). This is the current workaround.