feat(demo): a demo caseload, and the three silent defects that kept it invisible - #1676
Conversation
Three schemas carried the slug `task` on a normal dev instance (ids 52, 146 and 173). Only 173 belongs to dossiq's register, but SchemaKeyReconciler resolved slugs with SchemaMapper::find(), which is instance-wide, and that returned 52: an InterneTaak schema owned by another app in another register. So `task_schema` pointed outside dossiq's own register. All seven consumers that create or read case tasks (flow human steps, reassignment, work queue, substitution, transition task creation, checklist guard) wrote to a foreign schema. Nothing threw and nothing logged. The Tasks page simply stayed empty, which reads as "no data" rather than "wrong schema". Resolve the slug among the register's own schemas first, via OpenRegister's findBySlugInIds(), which was built for exactly this collision. Keep the unscoped lookup as a fallback: dossiq deliberately points appointment, location and catalog at schemas owned by other apps, those slugs are unique instance-wide, and dropping the fallback would blank all three. Measured on the dev instance: 107 of 111 schema keys already resolved inside register 23, appointment/location/catalog are the intended cross-register three, and task was the only real collision.
The Tasks page, My Work and five dashboard widgets were empty because the register held 0 tasks and none of the 14 existing cases had an assignee. Every one of those surfaces filters on assignee = the current user. Adds `occ dossiq:demo:seed`: 18 cases across the four shipped case types plus 32 tasks, positioned so Overdue, Deadline Alerts, My Tasks, Task Due Reminders, Stalled Cases and the Completed KPI all have rows. Idempotent by case title, so re-running skips what is already there. Two things this had to work around. Dates in the dataset are relative day offsets, never absolute dates. A fixture with absolute dates is right on the day it is written and wrong every day after, which is how "the demo dashboard is empty again" turns out to be a stale file rather than a bug. A case's deadline cannot be written, only caused: `deadline` is a materialised OpenRegister calculation over startDate plus the case type's processingDeadline, so a written deadline is overwritten on save. The seed backdates startDate by the case type's own processing deadline instead, and `--verify-only` reports the buckets by READING THE REGISTER BACK rather than by counting the input, which would agree with the input by construction. Verified on the dev instance: 18 cases and 32 tasks created, re-run skipped all 18, and the read-back reports 5 overdue, 4 within three days, 4 closed and 25 open tasks. Spot-checked the arithmetic end to end: startDate 2026-06-17 plus P56D materialised deadline 2026-08-12, exactly the requested 21 days overdue.
…app's schema Same defect as the previous commit, second call site, worse blast radius. SchemaAnnotationReconciler also resolved schema slugs with the instance-wide SchemaMapper::find(). For the slug `task` that returned schema 52, an InterneTaak schema owned by another app in another register, so dossiq merged its own x-openregister-calculations onto SOMEBODY ELSE'S schema. Measured on the dev instance before the fix: schema 52 (foreign) -> isTerminalStatus, daysUntilDue, daysOverdue schema 173 (dossiq) -> blocksCase Two visible consequences, neither of which errored. isTerminalStatus never materialised on dossiq's tasks, so all 7 completed tasks read isTerminalStatus = false and stayed in "My Tasks" and "Task Due Reminders", whose whole filter is isTerminalStatus = false. And daysUntilDue did not exist to extend, so every due-date column on those widgets rendered blank. The rule now lives in one place, SchemaSlugResolver, because two call sites each resolving slugs their own way IS the defect: they disagreed about which schema `task` meant, and the config keys and the annotations landed on different rows. Both reconcilers share one instance. Verified on the dev instance: after the fix the calculations sit on 173 only, the stray copies are gone from 52, all 7 completed tasks now read isTerminalStatus = true, daysUntilDue returns real numbers, and re-running both reconcilers writes 0 and 0, so nothing re-pollutes the foreign schema.
…erdue openspec/specs/signalering-widgets/spec.md requires Deadline Alerts to list cases approaching their deadline AND cases already overdue, and Task Due Reminders to do the same for tasks. Both widgets filter correctly for that, but capped the result at 5 rows ordered by deadline ascending. So once five cases are overdue, the five slots are full and no at-risk case can appear at all. That breaks the widget's first scenario, which says the two cases due within the warning threshold MUST be displayed. It also made the two alert widgets render exactly the same rows as the Overdue and My Tasks widgets next to them, which is what surfaced it. Raise both limits to 10 so overdue and at-risk both fit. Deliberately NOT narrowing the filter to a forward-only window: that would read as the obvious fix and would contradict the spec, which wants overdue cases in this widget.
…s with e2e
The two Nextcloud dashboard widgets concatenate overdue items ahead of at-risk
ones and then slice(0, 5). Once five items are overdue the slice is full, so no
at-risk item can ever be shown, and both widgets render the same rows as the
Overdue Cases and My Tasks widgets beside them. The spec requires both groups
in these widgets, so this is the same defect already fixed in the manifest for
the in-app dashboard, on the Nextcloud dashboard surface.
Verified on the dev instance after rebuilding the bundle: Deadline Alerts now
lists the 5 overdue cases AND the 4 at-risk ones (Due today, 1, 2 and 3 days
remaining), overdue first, and no longer duplicates the widget next to it.
Adds tests/e2e/demo-caseload.spec.ts, which pins the three regressions behind
the empty Tasks page rather than the symptom:
- a completed task must materialise isTerminalStatus = true, which is what
the open-work filters read
- daysUntilDue must compute, asserted as a NUMBER because the defect
returned null and an existence check would have passed
- the Tasks page must render rows instead of its empty state
It seeds what it needs under the per-run prefix and removes it again, so it
does not depend on demo data and cannot be satisfied by anyone else's rows.
The row assertion is deliberately not a title assertion: the index pages at 20,
so a title would depend on how many tasks the instance holds. The seeded task's
own visibility is pinned on its detail page instead.
4 passed against the dev instance, and the register was left with exactly the
32 tasks it started with.
Two gate findings, both real rather than noise. gate-17 (redundant-controller) flagged `create()` as a literal pass-through to ObjectService. It was: it wrapped saveObject and handed the entity straight back, so every call site had to follow it with idOf(). Return the new object's ID instead, which is the only thing either caller actually wants, and both call sites lose a step. A failure is now '' rather than null, which the task counter reads the same way. gate-66 (openregister-dependency-shape, ADR-083) flagged the unguarded container lookup. OpenRegister is optional here, so establish availability with IAppManager::isInstalled() first and keep the lookup, which is the escape the gate names. It also splits two different problems that previously produced one message: OpenRegister absent, versus present but unable to construct. Verified against the live ObjectService rather than only the unit fake, because the fake cannot prove the entity shape: create() returned a real uuid, the row was found with its parent case reference intact, and cleanup left nothing. The same probe confirms schemaIds() now resolves task to schema 173.
The Frontend Check (format) job runs `prettier --check` across the tree and flagged the new spec's trailing blank line.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ❌ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-02 13:20 UTC
Download the full PDF report from the workflow artifacts.
Both were reconstructed from scratch to prepare a demo. The seed command and the mail setup are useless to the next person if the steps live only in a terminal history.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ❌ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ⏭️ | ||||
| stylelint | ✅ | ||||
| build | ❌ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-02 13:22 UTC
Download the full PDF report from the workflow artifacts.
The case schema declares x-openregister-archival, so OpenRegister refuses a user-driven delete. A case a spec creates can therefore never be cleaned up, and cleanupRunObjects tolerates the failure silently, so every run added one permanently. Measured on the dev instance: 17 of its 37 cases were exactly that residue, and they crowd real data out of Open Cases and Stalled Cases. Hang the tasks off an existing case instead, and create one only where the register is genuinely empty. Tasks carry no archival rule and are still torn down. `case` leaves SEEDED_SCHEMAS as well, since listing it there was a cleanup that quietly failed every run. Verified: 4 passed, and the register held 37 cases before the run and 37 after.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ❌ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-02 13:25 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-02 13:30 UTC
Download the full PDF report from the workflow artifacts.
SettingsService constructs the resolver, so every SettingsServiceTest and VthSettingsServiceTest case executes it. With beStrictAboutCoverageMetadata and failOnRisky both on, an executed class that no annotation lists makes the test RISKY, and 10 of them turned the whole PHPUnit matrix red on all six cells while reporting zero failures.⚠️ It passes locally without this. The strict-coverage check only runs when a coverage driver is collecting, and CI generates a Clover report while a plain `vendor/bin/phpunit` does not. Reproduce it with `php -d pcov.enabled=1 vendor/bin/phpunit --coverage-clover=…`, which is how this was confirmed fixed: 2759 tests, zero risky, exit 0. The two sibling reconcilers were already declared the same way.
Both were inconsistent with the widgets beside them, and one was visibly wrong. The Overdue KPI filtered `deadline < today` but not `isFinalStatus`, while the Overdue table beneath it filters both. Measured on the dev instance: the card read 8 against a list of 5, because three closed cases still had a past deadline. openspec/specs/dashboard/spec.md DASH-001b defines overdue as `deadline < today` AND status not final. The Open Cases table had no filter at all, so a panel titled "Open Cases" listed closed ones. Every sibling case widget already filters isFinalStatus.
openspec/specs/dashboard/spec.md REQ-DASH-001 names five KPI cards. Three of the four that ship disagreed with it, and two disagreed with the list rendered directly beneath them. DASH-001a. The first card was titled "New cases" and counted startDate inside the date picker's window. That answers a different question, and left the dashboard with no headline for open workload at all. It is now "Open Cases", counting every case whose status is not final, and deliberately not scoped to the date range: the spec defines this count over all open cases. DASH-001b. The Overdue card was scoped to the date range while the Overdue table beside it was not. On the default month preset the card read 0 against a list of 5, because every overdue case necessarily started before the window it is overdue in. The range scope is gone and the caption is the spec's "action needed". DASH-001d. My Tasks counted `assignee = @me` AND a dueDate inside the range, so a task with no due date, or one due outside the window, was invisible. The spec counts the user's tasks in a non-terminal status. Now filtered on `isTerminalStatus: false`, which is the same rule the My Tasks widget uses. Measured after the change: Open Cases 16, Overdue 5 (matching its table exactly), My Tasks 23. NOT done here, and deliberately. DASH-001a's "+3 today", DASH-001c's "avg 18 days" and DASH-001e's SLA Compliance card all need a second computed number on one tile. CnStatWidget interpolates a caption ONLY in endpointSource mode, so each needs a Dossiq KPI endpoint to read from. That is a feature with its own controller, route and spec work, not a manifest edit. DASH-001c is also left alone because "Completed This Month" and the shipped date-range picker want different windows, which is a product decision rather than a defect.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-02 17:43 UTC
Download the full PDF report from the workflow artifacts.
…t too SettingsServiceReconcileRegressionTest also constructs SettingsService, so it executes SchemaSlugResolver and was RISKY for the same reason as the other two. CI went from 10 risky to 2; these are the last 2.⚠️ THE PREVIOUS COMMIT CLAIMED A LOCAL REPRODUCTION THAT NEVER HAPPENED. The `php -d pcov.enabled=1` run reported no risky tests because PHPUnit answered "No code coverage driver available" and carried on: neither pcov nor xdebug is installed on this machine or in the dev container. A run with no driver reports zero risky whether or not the problem is there, so it proved nothing. There is no local instrument for this check, so the argument is static instead, and it covers the class rather than the instance. The finding is precisely "executed a class not listed as covered or used", and `requireCoverageMetadata` is false, so it can only fire on a test that HAS @Covers. Every test carrying @Covers that constructs SettingsService or either reconciler now declares the resolver; the one remaining file has no coverage metadata and is exempt.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-02 17:51 UTC
Download the full PDF report from the workflow artifacts.
…s ignored their filter useObjectStore.fetchCollection passes its params straight to the query string, and OpenRegister reads a BARE field name. Measured against the live API: _filters[assignee]=rbac-editor -> 32 rows, every assignee assignee=rbac-editor -> 2 rows, only that user _filters[isFinalStatus]=false -> 37 rows, closed cases included isFinalStatus=false -> 16 rows, open only So the filter never applied. MyTasksWidget and TaskRemindersWidget fetched EVERY user's tasks and then narrowed only by status, which is not something a widget titled "My Tasks" may do: on a real team it shows other people's work. On this instance admin happens to own 30 of 32 tasks, which is exactly why it looked right. CasesOverviewWidget had no status filter at all, so it listed closed cases; and because it orders by startDate desc, a burst of recently created-and-closed rows pushed every live case out of all 7 slots. All three now filter server-side on bare field names. Verified in the browser: Cases overview lists only open demo cases, My Tasks lists 7 open tasks, and the dashboard no longer shows a single e2e row.⚠️ 38 OTHER CALL SITES USE THE SAME INERT IDIOM (settings tabs, workflow editors, the workflow store), so `_filters[caseType]` is not scoping those lists to the selected case type either. They are left alone here deliberately: that is a separate change across a dozen files with its own testing, not a rider on a demo-data PR.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-02 18:15 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-02 19:06 UTC
Download the full PDF report from the workflow artifacts.
38 remaining call sites across 19 files, all silently unfiltered. useObjectStore.fetchCollection passes params straight to the query string and OpenRegister reads a bare field name, so `_filters[caseType]` was never a filter. Measured against the live API: _filters[caseType]=<uuid> -> 46 statusTypes, only 6 of them matching caseType=<uuid> -> 6 statusTypes Two consequences, both wrong data rather than slow queries. 25 `_filters[caseType]` sites: every case-type settings tab (statuses, results, roles, document types, decision types, properties, workflow) listed EVERY case type's children instead of the selected one's, and the workflow editor and store did the same. 9 `_filters[case]` sites: case-scoped reads returned every case's rows. None of them narrowed the result afterwards, and bezwaar.js takes `objections?.[0]` from that unfiltered list, so a case detail page could attribute ANOTHER case's objection to the case being viewed. inspection.js and advice.js assigned the whole unfiltered list straight to their state. The rewrite is mechanical, one form to another, and the diff is only that: every added line is a bare field key and nothing else moved. `case` as a property key is legal, and the build confirms it. Verified: prettier clean, eslint 0 errors, 360 vitest tests pass, production build succeeds. In the browser the case detail page now lists its own three tasks and none of the other 29, and the API returns 6 statuses for a case type where it previously returned all 46.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-03 04:54 UTC
Download the full PDF report from the workflow artifacts.
gate-16 is diff-scoped, so changing one line inside these three methods made them 'changed' and required a @SPEC tag. Each points at the spec its own file already references, and each docblock records why the line changed: the `_filters[x]` form they used is inert, so all three were reading every case's or every case type's rows. Verified: all 78 applicable hydra gates green, prettier clean, 360 vitest tests pass.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-09-03 04:59 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/dossiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| check-l10n-js | ✅ | ||||
| composer | ✅ | ✅ 106/106 | |||
| npm | ✅ | ✅ 540/540 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-09-03 05:03 UTC
Download the full PDF report from the workflow artifacts.
Why
The Tasks page, My Work and five dashboard widgets were empty. The ask was demo data. The data turned out to be the smaller half of the problem.
What was actually wrong
Three defects, none of which errored, logged, or failed a test.
1. A schema slug is not unique, and Dossiq resolved it instance-wide.
Three schemas carried the slug
taskon a normal dev instance:SchemaMapper::find('task')returns whichever row it fetches first. It returned 52. Two call sites resolved slugs that way and both landed on the foreign schema:SchemaKeyReconcilerwrotetask_schema = 52, so all seven consumers that create or read case tasks wrote into another app's register. The Tasks page read 0 rows, which looks exactly like "no data".SchemaAnnotationReconcilermerged Dossiq'sx-openregister-calculationsonto that same foreign schema. Measured before the fix:So
isTerminalStatusnever materialised on our tasks. All 7 completed tasks readfalse, and "My Tasks" and "Task Due Reminders", whose entire filter isisTerminalStatus = false, kept showing finished work.daysUntilDuedid not exist to extend, so every due-date column rendered blank.The rule now lives in one class,
SchemaSlugResolver. Two call sites each resolving slugs their own way is the defect: they disagreed about which schemataskmeant, so the config keys and the annotations landed on different rows.The unscoped lookup stays as a fallback, deliberately. Dossiq points
appointment,locationandcatalogat schemas owned by other apps; those slugs are unique instance-wide, and removing the fallback would blank all three. Measured: 107 of 111 schema keys already resolved inside register 23, those three are the intended exceptions, andtaskwas the only real collision.2. The alert widgets could not show what they are for.
openspec/specs/signalering-widgets/spec.mdrequires Deadline Alerts to list cases approaching their deadline and cases already overdue. Both widgets do filter for that, then cap at 5 with overdue listed first. So from the fifth overdue case onward, no at-risk case can appear at all, which breaks the requirement's first scenario. It also made both alert widgets render the same rows as the Overdue and My Tasks widgets beside them, which is how it surfaced.Raised both caps to 10, in the manifest and in the two Vue widgets. Deliberately not narrowing the filter to a forward-only window: that reads as the obvious fix and would contradict the spec.
What this adds
occ dossiq:demo:seed: 18 cases across the four shipped case types plus 32 tasks, positioned so every widget has rows. Idempotent by case title.Two things it had to work around:
deadlineis a materialised calculation overstartDateplus the case type'sprocessingDeadline, so a written deadline is overwritten on save. The seed backdatesstartDateby the case type's own processing deadline instead.--verify-onlyreports the buckets by reading the register back, not by counting the input, which would agree with the input by construction.Verified
On the dev instance, not inferred:
startDate 2026-06-17 + P56Dmaterialiseddeadline 2026-08-12, exactly the requested 21 days overdue.isTerminalStatus = true, anddaysUntilDuereturns real numbers.create()probed against the live ObjectService, not only the unit fake: real uuid returned, row found with its parent case reference intact, cleanup clean.Both schema fixes carry a negative control: with the fix disabled the new tests fail with
'52'where'173'is required, and the date test fails when the interval is added instead of subtracted.Checks
phpcs,phpmd,psalmclean.tests/e2e/demo-caseload.spec.ts, 4 passed, and the register was left with exactly the 32 tasks it started with. It seeds under a per-run prefix and removes it again, so it does not depend on demo data existing.A sixth defect, found while verifying the demo
The
_filters[x]query parameter is inert.useObjectStore.fetchCollectionpasses its params straight to the query string and OpenRegister reads a bare
field name, so nothing using that form has ever filtered. Measured against the
live API:
_filters[assignee]=rbac-editorassignee=rbac-editor_filters[caseType]=<uuid>caseType=<uuid>41 call sites used it, and none narrowed the result afterwards:
admin owns 30 of 32, which is exactly why it looked correct.
rows pushed every live case out of all 7 slots.
_filters[caseType]sites: every case-type settings tab listed everycase type's statuses, results, roles and document types instead of the
selected one's.
_filters[case]sites: case-scoped reads returned every case's rows, andbezwaar.jstakesobjections?.[0]from that unfiltered list, so a casedetail page could attribute another case's objection to the case being viewed.
All 41 now use bare field names. The rewrite is mechanical and the diff is only
that: every added line is a bare field key. Verified with prettier, eslint (0
errors), 360 vitest tests, a production build, and in the browser: the case
detail page lists its own three tasks and none of the other 29.
Not done here
The Tasks index shows
caseas a raw uuid rather than the case title.CnIndexPagecolumns have no resolver (theresolveconfig exists only on detail-pageobjectField), so fixing it means a change in the sharednextcloud-vuelibrary. Out of scope for this PR, worth an issue.🤖 Generated with Claude Code