Skip to content

feat(hermiq): rail-based agent detail, fuller dashboard, and the CSS that made either visible - #162

Merged
rubenvdlinde merged 8 commits into
developmentfrom
feat/hermiq-page-redesign
Aug 7, 2026
Merged

feat(hermiq): rail-based agent detail, fuller dashboard, and the CSS that made either visible#162
rubenvdlinde merged 8 commits into
developmentfrom
feat/hermiq-page-redesign

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Work that had been sitting uncommitted-adjacent on this machine — one commit, now brought up to date with development.

What it is

Rail-based agent detail, a fuller dashboard, and the CSS without which neither rendered.

Two things repaired while landing it

The dependency pin was pointing at a version that no longer exists. The working tree had @conduction/nextcloud-vue edited to 3.0.0-vue3.6 — one of the six 3.0.0-vue3.* builds unpublished when that line was withdrawn and renumbered onto 2.x. npm ci could not have resolved it. Merging development brings the correct 2.2.0-vue3.3; the stale edit is stashed rather than discarded (git stash list, top entry) in case anything else in it was wanted.

Brought current with development — the branch was 12 behind, now 0. That was the conflict risk worth removing.

Not included, deliberately

An untracked src/Chat.vue sits in the working tree. It is not the file the app uses: src/registry.js imports ./views/Chat.vue, which is tracked and is strictly better — it carries the data-testid="chat-conversation-row" the e2e suite selects on, and a prefers-reduced-motion block (WCAG 2.3.3) that replaces the infinite typing-indicator bounce with a static opacity. The stray copy has neither, so it is an older duplicate. Left in place rather than deleted, since it is untracked and git could not recover it.

…that made either visible

The layout the manifest already described was never reaching the screen. Hermiq
shipped no GridStack stylesheet: nc-vue imports it as a side-effect import from
an aliased package and webpack tree-shakes those away, and it is absent from
dist/nextcloud-vue.css too (0 occurrences of its base rules, while leaflet's 174
ARE there). GridStack >= 11 sets left/top INLINE but takes `position: absolute`
on items and `position: relative` on the container from that sheet, so every
widget fell into normal flow: correct widths, no horizontal placement, all 11
cells stacked in one column down the left third of a 1600px viewport. Measured
before the fix: gs-x="3" carried `left: calc(3 * var(--gs-column-width))`
resolving to 25% while `position` computed to `static`, and every item reported
left=371px. procest (same renderer, gridstack 10) positioned correctly, which is
what localised it to hermiq.

The stylesheet is fixed IN THE LIBRARY, not here — @conduction/nextcloud-vue's
src/css/index.css now @imports it so postcss-import folds it into the extracted
sheet, covering both the published-dist and aliased-to-src resolution paths. An
app-level import would duplicate those rules in every consumer. main.js carries
only a note recording why it is deliberately absent.

Layout, on top of that:
- AgentDetail adopts procest CaseDetail's 8/4 rail — an 8-column main column
  (Configuration, Run operations, Memory) beside a persistent 4-column rail (the
  four KPI tiles stacked, Skills, Eval baseline), both closing exactly at row 16.
  33 rows -> 28, zero unfilled cells, and zero widgets whose scrollHeight exceeds
  their cell (ADR-062), all re-measured in a browser.
- Tool governance halves from gridHeight 14 to 8: its two surfaces are peers over
  one capability, so they are now tabbed rather than stacked, with a real tablist
  (roving tabindex, aria-selected/aria-controls, arrow-key nav). It draws its own
  heading and sets showTitle:false, because with the chrome title on that title is
  a SIBLING of the widget root inside a display:block grid item, so height:100%
  resolved against the full box and ignored it — a permanent 6px overflow no
  gridHeight could remove.
- Dashboard closes its grid (the bottom row left 6 of 12 columns empty) and gains
  a runs-by-agent chart, an agents table, header actions and a KPI drilldown.
  12 rows, zero unfilled cells.

AnalyticsService enriches perAgent with `name`: the aggregate carried only
agentId, and a chart labelled with UUIDs is unreadable. This instance has zero
runs, so the chart was verified by a positive control — the endpoint was
intercepted with a synthetic payload and it rendered 6 bars across 3
name-labelled categories.

Also fixes a pre-existing 500: AgentsController::stats bound PHP's (string)false
— the empty string — against a boolean column, so every call died with
SQLSTATE[22P02] and the agent counters had never worked. Booleans are now
normalised to 'true'/'false'. Verified against DB ground truth with a
deliberately-inactive probe agent (18/17/1), since with 17/17 active a broken
filter and a working one are indistinguishable.

Security postures declared rather than inherited (hydra gates 5/7/9/30):
- SettingsController::create, SetupController::saveConfig and ::runAction were
  admin-only by Nextcloud's default for an un-attributed method, which is silently
  lost the moment anyone adds #[NoAdminRequired]. Now
  #[AuthorizedAdminSetting(AdminSettings::class)]. runAction matters most:
  test-llm makes the server issue an outbound request, so unauthenticated it is
  an SSRF primitive.
- MetricsController::index stays admin-authed per ADR-006 ("/api/metrics
  Prometheus text, admin auth" vs "/api/health JSON, public") and gains
  #[NoCSRFRequired], which is what a scraper actually needs.
- Four endpoints taking no caller-supplied object id carry reason-bearing
  @no-admin-idor-exempt tags. create() was checked first: `owner` and
  `organisation` are in PROTECTED_KEYS and assigned server-side.

Accessibility: scope="col" on every column header across four components, a
visually-hidden label on a bare <th />, and prefers-reduced-motion fallbacks
(WCAG 2.2 AA 2.3.3). The chat typing indicator is an infinite bounce — the
pattern that triggers vestibular symptoms — and is replaced with a static
opacity rather than removed, so "assistant is typing" survives the motion.

The e2e login no longer waits for 'networkidle', which never settles on
Nextcloud (ADR-074 rule 4): the wait always ran to its timeout and only worked
because .catch() swallowed it — a disguised fixed delay, not a readiness signal.
It now waits for the submit control to be present and enabled.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ a76ff49

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-06 08:02 UTC

Download the full PDF report from the workflow artifacts.

testStatsUsesPaginatedTotals still matched `$query['active'] === true`, the
contract from BEFORE countAgents() started normalising booleans. The branch
sends the string 'true', so that arm never matched, the callback fell through to
the inactive branch, and the assertion read:

    total 10, active 4, inactive 4

counts that do not add up — which is the tell that the mock, not the code, was
wrong.

The normalisation is the fix, not the defect. A bool bound as a query parameter
casts to '1' for true and the EMPTY STRING for false, and Postgres rejects '' on
a boolean column with SQLSTATE[22P02] — so stats() was a hard 500 on every call
and the dashboard's agent counters showed nothing.

The callback now asserts the value is a string rather than only matching one, so
a regression to raw booleans fails here with a readable message instead of
quietly returning the wrong count.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ d2cd438

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 08:15 UTC

Download the full PDF report from the workflow artifacts.

…hors, exception translation, icons

All four ran for the first time on this repo when hydra-gates-ref moved
v1.3.0 -> v1.5.0, so this is newly-VISIBLE debt rather than newly-created.

gate-40 form-label-association — two raw <textarea> elements in Chat.vue (the
feedback box and the message composer) carried only a :placeholder. A
placeholder is not an accessible name: it disappears the moment the user types,
so anyone on a screen reader loses the field's description mid-entry (WCAG
3.3.2, 4.1.2). Both now carry an aria-label; the placeholder stays as the hint
it always was.

gate-46 spec-anchor-existence — three unresolved targets:
  - MetricsController pointed at openspec/changes/example-change/tasks.md, a
    directory that does not exist. Repointed to the canonical
    observability spec's REQ-OBS-001, which is exactly what the controller
    implements (Prometheus metrics, admin only).
  - AgentsController and its test used #task-4-1, which is a LIST ITEM, not a
    heading, so it never resolved. The file's own convention is number +
    kebab-cased heading (#1-port-the-chat-engine resolves today), so these
    become #4-mirror-the-routes.
Also fixed HealthController's three example-change refs while here — same
broken placeholder, and it is outside this diff only by accident of which files
this PR touched. REQ-OBS-002 is the health endpoint.

gate-49 controller-exception-translation — SettingsController::index/create/load
had no try/catch and no @throws. An uncaught throwable there becomes a framework
500 with a stack trace: no use to the caller, and index() is #[NoAdminRequired],
so it leaks internals to a non-admin. All three now translate to a JSON error
and log why. This needed a logger on the constructor, so the hand-built
controller in SettingsControllerTest gains a mock — checked for, rather than
discovered by a red build.

gate-55 detail-page-discipline — six widget icons named MDI glyphs the shared
registry cannot render, so each drew the "?" fallback (ADR-062 rule 8). Mapped
to registry entries by MEANING, not by nearest spelling: PackageVariantClosed ->
Package, CogOutline -> Cog, ChartTimelineVariant -> Timeline ("Total runs
recorded"), SchoolOutline -> School ("Learnings"), PlayCircleOutline ->
RocketLaunch (the run-operations card — the registry has no play glyph).

Verified by running check_detail_page_discipline.py locally: 6 findings -> 0,
and the manifest still passes Ajv validation.
'Feedback details' and 'Message' are now the accessible names of the two chat
textareas, so they are user-facing strings and need translating like any other.
Dutch supplied — an aria-label left in English is exactly the string a Dutch
screen-reader user hears.
@rubenvdlinde
rubenvdlinde requested a review from Rem-Dam as a code owner August 7, 2026 08:32
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 56984bc

Check PHP Vue Security License Tests
lint ⏭️
phpcs ⏭️
phpmd ⏭️
psalm ⏭️
phpstan ⏭️
phpmetrics ⏭️
eslint ⏭️
stylelint
build ⏭️
composer ⏭️ ⏭️
npm ⏭️ ⏭️
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 08:33 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 9999098

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 08:41 UTC

Download the full PDF report from the workflow artifacts.

…swept in

src/Chat.vue is an older copy of src/views/Chat.vue. I had looked at it earlier,
established it is NOT the file the app loads (src/registry.js imports
./views/Chat.vue) and is strictly worse — it lacks the data-testid the e2e suite
selects on and the prefers-reduced-motion block — and decided to leave it
untracked.

Then I staged the gate fixes with 'git add -A src' and committed it anyway. Its
relative imports are written for src/views/, so from src/ none of the seven
resolve, and eslint failed with exactly those seven import/no-unresolved errors.

Untracked again and added to .gitignore, so the same accident cannot repeat. The
file stays on disk — it is untracked, so git could not give it back.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ cbd01ac

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 08:53 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ cbd01ac

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 08:58 UTC

Download the full PDF report from the workflow artifacts.

…gged

The coverage guard failed the PR: 24 statements added, coverage 68.25% -> 68.20%.
The added statements were the try/catch blocks from the gate-49 fix, and nothing
exercised them.

That is the right complaint. An untested catch block is indistinguishable from
no catch at all — it is the 'a check that never runs looks exactly like one that
passed' shape, and the whole point of the block is what happens on the path
nobody normally takes.

Each test now asserts the TRANSLATED response, not merely that the method
survives:

  index()   500 + an 'error' key, and the internal exception message is NOT
            echoed back — that leak is what the translation exists to stop, and
            index() is #[NoAdminRequired] so it would reach a non-admin
  create()  success:false, the shape the UI already branches on
  load()    says nothing was changed, which is the one fact the admin who
            clicked reload needs
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 977f296

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 09:17 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 977f296

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 09:22 UTC

Download the full PDF report from the workflow artifacts.

…s it

[hydra-gate-controller-exception-translation exclude] SettingsController::index/create/load each catch \Throwable, return a translated JSON error and log the cause; the gate matches only nine named domain exceptions and does not recognise \Throwable, which is strictly broader and covers all nine. Upstream: ConductionNL/.github#204.

The same text is in the PR body, but a re-run did not pick it up there — the
commit-message path is the one that reliably reaches the gate.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 230e6da

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
check-manifest
test-l10n
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-07 09:39 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant