Repository navigation
fix(dashboards): ship a dashboard, and let the API resolve the one the shell shows - #361
Merged
rubenvdlinde merged 3 commits intoAug 28, 2026
Merged
Conversation
…e shell shows
A fresh install of launchpad had no dashboard at all. `/apps/launchpad/`
rendered "No dashboards available, contact your administrator",
`GET /api/dashboards` answered `{"items": []}` with HTTP 200, and
`oc_launchpad_dashboards` was empty. Two separate defects, and the first
one hides the second.
1. NOTHING EVER CREATED A DASHBOARD. `allowUserDashboards` defaults to
`false` on purpose (REQ-ASET-003: an admin opts in before users may
create personal dashboards). That default closes the only branch of
`tryCreateFromTemplate()` that WRITES, so on a brand-new instance no
dashboard was created by the app or by the user, and every rung of
`resolveActiveDashboard()` returned null.
The default widget bundle (the Conduction, Sendent and Nextcloud
tiles plus a Files widget) had shipped and had unit tests. Nothing
ever called it, because nothing ever created the dashboard it seeds.
`SeedDefaultDashboard` now provisions it on install and after every
upgrade, idempotently.
THE SHAPE IS LOAD-BEARING. This app has two resolution chains and
they do not agree. `getEffectiveDashboard()` consults admin
templates; the page shell calls `resolveActiveDashboard()`, which
reads only `getVisibleToUser()` and never looks at a template. A
default ADMIN TEMPLATE therefore seeds four placements the workspace
still will not render. Measured, not reasoned: the first cut of this
fix seeded exactly that, and the app kept showing its empty state.
The shape the shell resolves is a `group_shared` row on the reserved
`default` group (REQ-DASH-012) with `isDefault = 1`, created
`published` because `filterByPublicationState()` hides drafts from
every non-owner and this row has none.
2. THE TWO CHAINS DISAGREED, SILENTLY. With the row seeded, the sidebar
listed "DEFAULT / Dashboard" and the grid drew its empty state on top
of it: `GET /api/dashboard` answered `{"error": "No dashboard
available"}` for the same user the shell had just resolved.
`getEffectiveDashboard()` had no group-shared rung at all. With no
preference set it went straight from owned dashboards to shares and
templates. It now resolves the `default` sentinel the same way
`resolveActiveDashboard()` steps 3 and 5 do.
Only the sentinel. A per-group default needs the caller's primary
group, which this signature does not carry, and guessing one would
make the two chains disagree in a new way.
Verified on a running instance: `occ maintenance:repair` seeds the row
(`group_shared` / `default` / `isDefault=1` / `published`) and
`/apps/launchpad/` renders Conduction, Sendent, Nextcloud and Files.
1577 unit tests pass; phpcs, psalm and phpstan clean on the changed files.
rubenvdlinde
requested review from
WilcoLouwerse,
bbrands02 and
rjzondervan
as code owners
August 27, 2026 21:22
Contributor
Quality Report — ConductionNL/launchpad @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ❌ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 538/538 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-27 22:30 UTC
Download the full PDF report from the workflow artifacts.
`Guard coverage baseline` was red on the scoped ratchet, not on the suite —
PHPUnit itself passed (1577 tests, 4059 assertions):
Surviving code, head: 75.05% (716/954 statements)
Surviving code, base: 76.83% (693/902 statements)
FAIL: coverage of the code this change KEEPS or ADDS dropped by 1.78%.
The PR added 52 statements and covered 23 of them. `lib/Repair/SeedDefaultDashboard.php`
(125 lines) arrived with no test at all; DashboardService got one, the repair
step did not.
Five tests over all four paths: the step's name, a seed that provisions, a seed
that finds one already there, a seed that throws, and the container failing to
resolve the service at all.
The two failure tests are the ones worth having. A repair step that lets an
exception escape aborts `occ upgrade` for the WHOLE instance — a missing demo
dashboard is cosmetic, a failed upgrade is not — so the step swallows and warns,
and without a test that asserts the swallow a later refactor could turn the one
into the other silently.
Both info branches are asserted on their MESSAGE, not just their call count:
they both call $output->info(), so a count-only test would pass with the two
swapped.
Local: OK (5 tests, 17 assertions).
…method past phpmd
`PHP Quality (phpmd)` was red on one violation:
lib/Service/DashboardService.php:435 ExcessiveMethodLength
The method getEffectiveDashboard() has 104 lines of code.
Current threshold is set to 100.
The group-shared rung this PR added is what pushed it over, so that is what
comes out — into resolveDefaultGroupDashboard(), carrying its own explanation
of why the rung exists at all (the page shell and the API resolved differently,
silently, so the sidebar listed a dashboard the grid drew an empty state over).
Behaviour is unchanged: same resolveGroupSharedDashboard() call with the same
DEFAULT_GROUP_ID sentinel, in the same position in the chain, returning the same
built result. The early-return is inverted only to keep the extracted method
flat.
Verified locally: php -l clean, phpmd exit 0 on BOTH legs (the ruleset and the
unused-params ruleset, as CI runs them), PHPUnit OK (1582 tests, 4076
assertions).
Contributor
Quality Report — ConductionNL/launchpad @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| app:check-code | ⏭️ | ||||
| info.xml | ⏭️ | ||||
| REUSE | ⏭️ | ||||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-28 09:38 UTC
Download the full PDF report from the workflow artifacts.
Contributor
Quality Report — ConductionNL/launchpad @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 104/104 | |||
| npm | ✅ | ✅ 538/538 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | 🚨 NO VERDICT — enabled but never ran | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-28 10:22 UTC
Download the full PDF report from the workflow artifacts.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 28, 2026
) Three specs assert "No dashboards available". #361 (c9e5808) exists to make that message impossible: SeedDefaultDashboard provisions an INSTANCE-WIDE, group-shared dashboard on install and after every upgrade, because a fresh install greeting a user with an empty shell was the defect. So the specs are not wrong about the app; the app deliberately removed the state they describe. They have been red on development since that commit. Handled three different ways, on purpose: 1. active-dashboard-resolution.spec.ts — INVERTED, not deleted. The valuable assertion is the opposite of what it said: a brand-new account, which owns no dashboard of its own, must still resolve to the seeded one. That is exactly the guarantee #361 introduced, and nothing was testing it. 2. runtime-shell-canEdit.spec.ts x2 — excluded WITH a reason. These assert the presence/absence of a Create CTA *inside* the empty state, so inverting them is not possible: the container is gone. The rule underneath (REQ-ASET-003 — allowUserDashboards gates whether a user may create a dashboard) is still worth covering and now needs an assertion that does not route through the empty state. The reason says so, names the commit, and reaches the Playwright report — so the skip-discipline gate reads it as attributed rather than silent. 3. fixtures/secondary-user.ts — the docblock said a brand-new account has zero dashboards "personal, group, or default", and that this was how the empty-state scenarios reached their state. That premise is now false and would have sent the next person down the same path. Verified: npm ci rc=0, prettier clean, `playwright test --list` compiles 11 tests across both files. Refs #372. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
that referenced
this pull request
Aug 28, 2026
E2E fails on development: ADR-023: fresh install is usable by non-admins — the empty-state Create CTA it is offered actually works locator(.workspace-shell__empty-cta) — element(s) not found #361 (SeedDefaultDashboard) provisions an instance-wide, group-shared dashboard on install, so a brand-new non-admin resolves to THAT and never reaches the empty state this test clicked through. #373 handled the two REQ-SHELL-005 specs in this same file for exactly that reason and said the rule underneath now needs an assertion that does not route through the empty state. This third test in the same file was left behind. This is that assertion. The create affordance a non-admin is offered today is the switcher sidebars Add dashboard button, gated on the same allowUserDashboards the test already sets. The valuable part is unchanged and is why the test exists: the POST must not come back 401/403/404/5xx. The closing assertion changes with it. Asserting the empty state is gone would prove nothing now — it was never there — so it asserts the users own dashboard is listed in the switcher instead.
This was referenced Aug 28, 2026
rubenvdlinde
added a commit
that referenced
this pull request
Aug 28, 2026
… state (#380) The last red spec on launchpad's development. #362 fixme'd two empty-state tests when #361 (c9e5808) made that state unreachable; this third one used the same locator and was missed, so it kept failing: Error: expect(locator).toBeVisible() failed Locator: locator('.workspace-shell__empty-cta') Error: element(s) not found SeedDefaultDashboard now provisions an instance-wide, group-shared dashboard on install and after every upgrade, so no account resolves to zero dashboards and no user ever sees the empty-state CTA. The CTA was never the claim. The claim is REQ-ASET-003 — an ordinary user may create a dashboard when allowUserDashboards is on — and that is what actually regressed once: `dashboard.create` 403'd and the user was stranded on the empty state. The sibling fixmes say so themselves: "the underlying rule is still worth covering, but it needs an assertion that does not route through the empty state." This is that assertion. It POSTs to the endpoint as a throwaway non-admin and requires 200/201. Three details carried over from the file's own hard-won conventions, rather than reinvented: - Basic-Auth + OCS-APIRequest via the existing `userApi` helper. A cookie-authenticated POST with no `requesttoken` is rejected by Nextcloud's CSRF check before reaching the controller, which would fail this test for a reason unrelated to authorization. - `expect([200, 201]).toContain(status)`, NOT `not.toBe(403)`. The weaker form also passes on 401 and 404, neither of which proves an ordinary user may create anything. That exact false green is documented two tests above. - The route is `dashboardApi#create`, `POST /api/dashboard`, read from appinfo/routes.php. The same file records an earlier draft that used a made-up path, got 404, and would have passed a weaker check. `create()` takes only optional parameters and is `#[NoAdminRequired]`, so a bare POST reaches `denyCreate()` — which is precisely the gate under test. Verified: npm ci rc=0, npm run lint rc=0, prettier clean, and `playwright test --list` compiles all 8 tests in the file. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde
added a commit
that referenced
this pull request
Sep 12, 2026
…its compulsory flag Four defects, each measured on a live instance with PostgreSQL before the fix. 1. The seeding never ran: getEffectiveDashboard() resolved the instance-wide default group dashboard, seeded on install since #361, before it reached tryCreateFromTemplate(). A user in a group with layout defaults resolved to the shared dashboard and owned nothing. The role path is now tried first, and only for a user who actually has layout defaults. 2. The second user ever to auto-provision got HTTP 500 'Slug must be unique among siblings': root slugs share one namespace across owners and every auto-provisioned dashboard is named 'My Dashboard'. The slug now carries the owner. 3. The hardcoded tile/tile/tile/files set was created on top of the role layout, though the comment beside it called it a fallback. It is a fallback now. 4. isCompulsory was dropped while seeding, so a compulsory role widget arrived removable. REQ-RFP-002 names it.
rubenvdlinde
added a commit
that referenced
this pull request
Sep 14, 2026
…its compulsory flag (#634) * test(enterprise-security): one verdict per scenario, all never built The spec carried a single blanket exclusion over 15 scenarios. Each now carries its own reason. The widget id appears in no commit on any branch under src, lib, appinfo or templates, so every scenario is honestly unbuildable rather than untested, and each reason says what is missing. * fix(role-layout): seed a new user from their role layout, once, with its compulsory flag Four defects, each measured on a live instance with PostgreSQL before the fix. 1. The seeding never ran: getEffectiveDashboard() resolved the instance-wide default group dashboard, seeded on install since #361, before it reached tryCreateFromTemplate(). A user in a group with layout defaults resolved to the shared dashboard and owned nothing. The role path is now tried first, and only for a user who actually has layout defaults. 2. The second user ever to auto-provision got HTTP 500 'Slug must be unique among siblings': root slugs share one namespace across owners and every auto-provisioned dashboard is named 'My Dashboard'. The slug now carries the owner. 3. The hardcoded tile/tile/tile/files set was created on top of the role layout, though the comment beside it called it a fallback. It is a fallback now. 4. isCompulsory was dropped while seeding, so a compulsory role widget arrived removable. REQ-RFP-002 names it. * test(db): pin the role layout seeding against a real database A new user whose group carries RoleLayoutDefault rows is seeded from them, once, with the compulsory flag kept, and a second auto-provisioned user no longer collides on the root slug. Each assertion names one of the four defects fixed in 4782cf9. * fix(phpcs): give tryCreateFromTemplate() its docblock back uniqueRootSlugFor() was inserted between the docblock and the method it documents, which left tryCreateFromTemplate() undocumented and phpcs reporting an error in lib. * fix(role-layout): keep getEffectiveDashboard() under the NPath limit, and never return undefined placements Two findings from the full suite, both introduced on this branch. phpmd: the role layout step added two branches inline and took getEffectiveDashboard() to an NPath of 432 against a threshold of 200. It now lives in resolveRoleLayoutOrDefaultGroupDashboard(), which the method takes as the single step the default group lookup already was. phpstan: tryCreateFromTemplate() started $seeded at false, so with no RoleFeaturePermissionService neither the > 0 nor the === 0 branch ran and $placements was undefined. It starts at 0, the int the seeder returns, and $placements starts empty.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backlog item 12 from the 2026-08-27 new-env review: "launchpad: fresh install ships NO default dashboard. Should ship a default Nextcloud + Sendent + Conduction dashboard."
What Ruben saw
/apps/launchpad/rendered "No dashboards available — contact your administrator". Measured on the running instance:GET /api/dashboardsanswered{"items": []}with HTTP 200, andoc_launchpad_dashboardshad zero rows.Two separate defects, and the first one hides the second.
1. Nothing ever created a dashboard
allowUserDashboardsdefaults tofalseon purpose (REQ-ASET-003: an admin opts in before users may create personal dashboards). That default closes the only branch oftryCreateFromTemplate()that WRITES, so on a brand-new instance no dashboard was created by the app or by the user, and every rung ofresolveActiveDashboard()returned null.The default widget bundle — the Conduction, Sendent and Nextcloud tiles plus a Files widget — had shipped and had unit tests (
DashboardServiceCreateDefaultsTest). Nothing ever called it, because nothing ever created the dashboard it seeds.New repair step
SeedDefaultDashboardprovisions it oninstallandpost-migration, idempotently.The shape is load-bearing
This app has two resolution chains and they do not agree:
group_shared?PageController::index)resolveActiveDashboard()GET /api/dashboardgetEffectiveDashboard()So seeding a default admin template writes four placements the workspace still will not render. Measured, not reasoned: the first cut of this fix seeded exactly that, the row landed in the DB, and the app kept showing its empty state.
The shape the shell resolves is a
group_sharedrow on the reserveddefaultgroup (REQ-DASH-012) withisDefault = 1, createdpublishedbecausefilterByPublicationState()hides drafts from every non-owner and this row has none.2. The two chains disagreed, silently
With the row seeded, the sidebar listed "DEFAULT ▸ Dashboard" and the grid drew its empty state on top of it:
GET /api/dashboardanswered{"error": "No dashboard available"}for the same user the shell had just resolved.getEffectiveDashboard()had no group-shared rung at all — with no preference set it went straight from owned dashboards to shares and templates.It now resolves the
defaultsentinel the same wayresolveActiveDashboard()steps 3 and 5 do. Only the sentinel: a per-group default needs the caller's primary group, which this signature does not carry, and guessing one would make the two chains disagree in a new way.Verification
On the running dev instance:
/apps/launchpad/then rendersConduction | Sendent | Nextcloudacross the top row with the Files widget below.phpcs,psalm,phpstanclean on the changed fileslint:licensesclean;lint:spec-annotationsfailures are pre-existing and do not name any file in this PR