Skip to content

Commit ed5d9ca

Browse files
authored
feat(nav): give hermiq a Reports page, from the report it already had (#765)
ADR-114 Decision 3 puts Reports in every app. Nothing is invented here: AI oversight already existed, and it was an entry in the SETTINGS foldout. That was the wrong home. The foldout is where configuration lives (ADR-044 Decision 3), and oversight is not configuration — it is a reading of what the agents did, which is what a report is. It becomes a card on /reports. ⚠️ THE ROUTE HAD TO SURVIVE, AND NOT ONLY FOR DEEP LINKS. dossiq declares /apps/hermiq/ai-oversight as an ADR-110 integrations entry, so retiring the menu entry while dropping the route would 404 from ANOTHER app. The page is untouched, and an e2e test navigates to it directly. Guardrail policy, the algorithm register and Compliance stay in the foldout. Those ARE configuration, and the e2e asserts that asymmetry so a later sweep fails a test rather than passing review. Hermiq is one of only four apps that already had a Store, so its footer now carries all four declared chrome items and the spec asserts the full sequence Documentation, Store, Reports, Features & roadmap. ChartBoxOutline registered in src/icons.js. An unregistered icon name renders no glyph at all — not a fallback, not a console error. Verified: gate-22, gate-53, gate-60, gate-102, gate-104 and gate-107 clean (gate-107 now reports zero warnings for this app, since it has both Store and Reports); prettier clean; all five e2e tests collect.
1 parent cdd799b commit ed5d9ca

7 files changed

Lines changed: 179 additions & 10 deletions

File tree

l10n/en.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1307,7 +1307,10 @@ OC.L10N.register(
13071307
"Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.": "Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.",
13081308
"Where the automation lives": "Where the automation lives",
13091309
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.",
1310-
"Open Flows in the menu": "Open Flows in the menu"
1310+
"Open Flows in the menu": "Open Flows in the menu",
1311+
"Reports": "Reports",
1312+
"Pick a report to open it.": "Pick a report to open it.",
1313+
"What the agents did, and what a human still has to answer for.": "What the agents did, and what a human still has to answer for."
13111314
},
13121315
"nplurals=2; plural=(n != 1);"
13131316
)

l10n/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,10 @@
13181318
"Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.": "Where this agent’s dictated audio is transcribed. “On this instance” never leaves the server and is slower; “browser” is instant and, in most browsers, sends the audio to the browser vendor.",
13191319
"Where the automation lives": "Where the automation lives",
13201320
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.",
1321-
"Open Flows in the menu": "Open Flows in the menu"
1321+
"Open Flows in the menu": "Open Flows in the menu",
1322+
"Reports": "Reports",
1323+
"Pick a report to open it.": "Pick a report to open it.",
1324+
"What the agents did, and what a human still has to answer for.": "What the agents did, and what a human still has to answer for."
13221325
},
13231326
"plurals": "",
13241327
"pluralForm": "nplurals=2; plural=(n != 1);"

l10n/nl.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,10 @@ OC.L10N.register(
13881388
"Open the documentation to keep going": "Open de documentatie om verder te gaan",
13891389
"Where the automation lives": "Waar de automatisering zit",
13901390
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows zijn wat er gebeurt zonder dat iemand klikt: een agent die op een schema draait, een overdracht die afgaat zodra een taak klaar is. Hier leest en bewerkt u ze. U hoeft nu niets te bouwen.",
1391-
"Open Flows in the menu": "Open Flows in het menu"
1391+
"Open Flows in the menu": "Open Flows in het menu",
1392+
"Reports": "Rapportages",
1393+
"Pick a report to open it.": "Kies een rapport om het te openen.",
1394+
"What the agents did, and what a human still has to answer for.": "Wat de agents deden, en waar een mens nog verantwoording over aflegt."
13921395
},
13931396
"nplurals=2; plural=(n != 1);"
13941397
)

l10n/nl.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1411,7 +1411,10 @@
14111411
"Open the documentation to keep going": "Open de documentatie om verder te gaan",
14121412
"Where the automation lives": "Waar de automatisering zit",
14131413
"Flows are what happens without anyone clicking: an agent that runs on a schedule, a hand-off that fires when a task completes. This is where you read and edit them. Nothing to build now.": "Flows zijn wat er gebeurt zonder dat iemand klikt: een agent die op een schema draait, een overdracht die afgaat zodra een taak klaar is. Hier leest en bewerkt u ze. U hoeft nu niets te bouwen.",
1414-
"Open Flows in the menu": "Open Flows in het menu"
1414+
"Open Flows in the menu": "Open Flows in het menu",
1415+
"Reports": "Rapportages",
1416+
"Pick a report to open it.": "Kies een rapport om het te openen.",
1417+
"What the agents did, and what a human still has to answer for.": "Wat de agents deden, en waar een mens nog verantwoording over aflegt."
14151418
},
14161419
"plurals": "",
14171420
"pluralForm": "nplurals=2; plural=(n != 1);"

src/icons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import CalculatorVariantOutline from 'vue-material-design-icons/CalculatorVarian
2121
import Calendar from 'vue-material-design-icons/Calendar.vue'
2222
import CashMultiple from 'vue-material-design-icons/CashMultiple.vue'
2323
import ChartBar from 'vue-material-design-icons/ChartBar.vue'
24+
import ChartBoxOutline from 'vue-material-design-icons/ChartBoxOutline.vue'
2425
import ChartLine from 'vue-material-design-icons/ChartLine.vue'
2526
import ChartTimelineVariant from 'vue-material-design-icons/ChartTimelineVariant.vue'
2627
import ChatOutline from 'vue-material-design-icons/ChatOutline.vue'
@@ -86,6 +87,7 @@ export default {
8687
Calendar,
8788
CashMultiple,
8889
ChartBar,
90+
ChartBoxOutline,
8991
ChartLine,
9092
ChartTimelineVariant,
9193
ChatOutline,

src/manifest.json

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,12 @@
126126
"order": 96
127127
},
128128
{
129-
"id": "AiOversightMenu",
130-
"label": "AI oversight",
131-
"icon": "ShieldCheckOutline",
132-
"route": "AiOversight",
133-
"section": "settings",
134-
"order": 97
129+
"id": "ReportsMenu",
130+
"label": "Reports",
131+
"icon": "ChartBoxOutline",
132+
"route": "Reports",
133+
"section": "footer",
134+
"order": 95
135135
}
136136
],
137137
"pages": [
@@ -1280,6 +1280,25 @@
12801280
"app": "hermiq"
12811281
}
12821282
},
1283+
{
1284+
"id": "Reports",
1285+
"route": "/reports",
1286+
"type": "reports",
1287+
"title": "Reports",
1288+
"config": {
1289+
"description": "Pick a report to open it.",
1290+
"cards": [
1291+
{
1292+
"id": "AiOversight",
1293+
"label": "AI oversight",
1294+
"description": "What the agents did, and what a human still has to answer for.",
1295+
"icon": "ShieldCheckOutline",
1296+
"route": "AiOversight"
1297+
}
1298+
]
1299+
},
1300+
"_note": "ADR-112 / ADR-114 Decision 3. Nothing is invented: AI oversight already existed and was an entry in the SETTINGS foldout. That is where configuration lives (ADR-044 Decision 3), and oversight is not configuration — it is a reading of what the agents did, which is what a report is. The page is untouched and stays routable at /ai-oversight, and dossiq deep-links it as an ADR-110 integrations entry, so the route had to survive. Guardrail policy, the algorithm register and Compliance stay in the foldout: those ARE configuration."
1301+
},
12831302
{
12841303
"id": "AiOversight",
12851304
"route": "/ai-oversight",

tests/e2e/app-chrome.spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
3+
* SPDX-License-Identifier: EUPL-1.2
4+
*
5+
* The bottom-left app chrome, in a browser (ADR-114).
6+
*
7+
* gate-107 reads the manifest and can prove the entries are DECLARED. It
8+
* cannot prove they RENDER, and this programme has produced three defects of
9+
* exactly that shape: an unregistered icon name renders NO glyph (no fallback,
10+
* no console error — ChartBoxOutline had to be added to src/icons.js for this
11+
* very entry), an entry whose `route` names a page the app does not host
12+
* renders a row that goes nowhere, and `nav.includePersonalSettings: false`
13+
* silently removed the entry reaching the user's notification preferences in
14+
* two apps.
15+
*
16+
* ⚠️ SCOPE EVERY SELECTOR TO `[data-testid="cn-nav"]`. An unscoped selector
17+
* also matches Nextcloud's own user menu, which is attached-but-hidden:
18+
* `waitFor({state:'attached'})` passes on it and the click never becomes
19+
* actionable, so the spec fails with "Target page has been closed" — a timeout
20+
* wearing a crash's clothes.
21+
*
22+
* ⚠️ SETTINGS ENTRIES ARE ATTACHED, NOT VISIBLE, inside a collapsed foldout.
23+
*
24+
* The config supplies `use.storageState`, so specs start signed in.
25+
*/
26+
27+
import { expect, test } from '@playwright/test'
28+
29+
const APP_BASE = '/index.php/apps/hermiq'
30+
31+
test.describe('app chrome (ADR-114)', () => {
32+
test.beforeEach(async ({ page }) => {
33+
await page.goto(`${APP_BASE}/`, { waitUntil: 'domcontentloaded' })
34+
await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible({
35+
timeout: 30_000,
36+
})
37+
})
38+
39+
test('the footer reads Documentation, Store, Reports, Features & roadmap, each with a glyph', async ({
40+
page,
41+
}) => {
42+
const footer = page.locator(
43+
'[data-testid="cn-nav"] .cn-app-nav__footer-list',
44+
)
45+
await expect(footer).toBeAttached({ timeout: 15_000 })
46+
47+
const rows = footer.locator('li')
48+
const texts = (await rows.allInnerTexts())
49+
.map((t) => t.trim())
50+
.filter(Boolean)
51+
52+
// Hermiq is one of only four apps that had a Store before this
53+
// programme, so its footer carries all four declared chrome items.
54+
// ORDER is the rule, not the numbers.
55+
const seen = texts.filter((t) =>
56+
/Documentation|Store|Reports|roadmap/i.test(t),
57+
)
58+
expect(seen.length).toBe(4)
59+
expect(seen[0]).toMatch(/Documentation/i)
60+
expect(seen[1]).toMatch(/Store/i)
61+
expect(seen[2]).toMatch(/Reports/i)
62+
expect(seen[3]).toMatch(/roadmap/i)
63+
64+
for (const row of await rows.all()) {
65+
await expect(
66+
row.locator('svg, .material-design-icon').first(),
67+
).toBeAttached()
68+
}
69+
})
70+
71+
test('AI oversight is a card on Reports, not a settings entry', async ({
72+
page,
73+
}) => {
74+
const nav = page.locator('[data-testid="cn-nav"]')
75+
76+
// It sat in the SETTINGS foldout, which is where configuration belongs
77+
// (ADR-044 Decision 3). Oversight is not configuration — it is a reading
78+
// of what the agents did.
79+
await expect(
80+
nav.locator('[data-testid="cn-nav-entry-AiOversightMenu"]'),
81+
).toHaveCount(0)
82+
83+
await nav.locator('[data-testid="cn-nav-entry-ReportsMenu"]').click()
84+
await expect(page).toHaveURL(/\/apps\/hermiq\/reports(\?|$)/, {
85+
timeout: 15_000,
86+
})
87+
await expect(
88+
page.getByText('AI oversight', { exact: false }).first(),
89+
).toBeVisible({ timeout: 15_000 })
90+
})
91+
92+
test('the AI oversight page is still routable, because another app deep-links it', async ({
93+
page,
94+
}) => {
95+
// dossiq declares /apps/hermiq/ai-oversight as an ADR-110 integrations
96+
// entry. Retiring the menu entry must not take the ROUTE with it, or
97+
// that link 404s from another app (ADR-044 Decision 5).
98+
await page.goto(`${APP_BASE}/ai-oversight`)
99+
await expect(page).toHaveURL(/\/ai-oversight(\?|$)/, { timeout: 15_000 })
100+
await expect(page.locator('[data-testid="cn-nav"]')).toBeVisible()
101+
})
102+
103+
test('guardrails, the algorithm register and compliance stay in the foldout', async ({
104+
page,
105+
}) => {
106+
// Deliberate asymmetry with AI oversight: these ARE configuration, so
107+
// ADR-044 Decision 3 keeps them where they are. A later sweep that cards
108+
// them fails here rather than passing review.
109+
const nav = page.locator('[data-testid="cn-nav"]')
110+
for (const id of ['GuardrailPolicy', 'AlgorithmRegister', 'Compliance']) {
111+
await expect(
112+
nav.locator(`[data-testid="cn-nav-entry-${id}"]`),
113+
).toBeAttached({ timeout: 15_000 })
114+
}
115+
})
116+
117+
test('the settings foldout carries Personal settings, Admin settings and Flows', async ({
118+
page,
119+
}) => {
120+
const nav = page.locator('[data-testid="cn-nav"]')
121+
122+
await expect(nav.locator('[data-testid="cn-nav-settings"]')).toBeAttached({
123+
timeout: 15_000,
124+
})
125+
await expect(
126+
nav.locator('[data-testid="cn-nav-personal-settings"]'),
127+
).toBeAttached()
128+
await expect(
129+
nav.locator('[data-testid="cn-nav-entry-FlowIndex"]'),
130+
).toBeAttached()
131+
132+
const admin = nav.locator('[data-testid="cn-nav-admin-settings"]')
133+
await expect(admin).toBeAttached()
134+
await expect(admin).toHaveAttribute('href', /\/settings\/admin\/hermiq$/)
135+
})
136+
})

0 commit comments

Comments
 (0)