Skip to content

Release: merge development into beta - #519

Open
github-actions[bot] wants to merge 31 commits into
betafrom
development
Open

Release: merge development into beta#519
github-actions[bot] wants to merge 31 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

github-actions Bot and others added 3 commits August 31, 2026 13:31
The 1.0.24-beta.20260831132856 release bumped the version on beta. Without this,
development stays behind beta and the next development -> beta promotion
conflicts on the version file.

Version files resolve to development's side, which is the higher line,
so this never moves a version backwards.
…0.24-beta.20260831132856

chore(release): sync beta back into development
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/launchpad @ 371184e

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 ✅ 526/526
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 14:41 UTC

Download the full PDF report from the workflow artifacts.

Dependabot cannot update these packages. Its npm updater fails with:

  npm error code EOVERRIDE
  npm error Override for <pkg> conflicts with direct dependency

The package is listed BOTH as a direct dependency and as a literal overrides
entry pinned to the same range. Dependabot bumps the direct dependency and
does not move the override, npm rejects the mismatched pair, and the update
is abandoned -- so these packages can never be updated, security advisories
included.

npm's self-reference is the idiom for exactly this: $<name> tells the
override to follow whatever the direct dependency resolves to, instead of a
range that has to be kept in lockstep by hand. This repo already uses it (see
the vue entry under @vue/test-utils).

Transitive consumers still get pinned to one version -- the direct
dependency's -- which is what the override was there to do. What changes is
that the pin tracks the dependency instead of duplicating it.

Verified in a clean container: bumping the direct dependency with the literal
override reproduces the EOVERRIDE above; with the self-reference the same
bump resolves.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/launchpad @ bc098af

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 ✅ 526/526
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 16:37 UTC

Download the full PDF report from the workflow artifacts.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/launchpad @ 677a370

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 ✅ 526/526
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-31 20:43 UTC

Download the full PDF report from the workflow artifacts.

`npm run lint` was `eslint src`, so two whole trees were never linted. Across
the fleet that hid roughly 3,900 errors, none of which any CI leg had shown.

Most of it was the config, not the code, and the same two defects were in
every app.

**scripts/ had no config block at all.** These are CommonJS Node CLI checkers,
and flat config defaults every `.js` to ESM with browser-ish globals, so eslint
read the CommonJS wrapper itself as undefined identifiers: `require`,
`process`, `__dirname`, `__filename`, `module`. A `scripts/**` block now
declares the environment. Declaring beats suppressing here: `no-undef` is the
rule that catches a genuinely misspelled identifier, and dozens of fake
findings would bury a real one. A second block covers `scripts/**/*.mjs`, which
is ESM and needs Node's globals without the CommonJS wrapper.

**The tests block applied a non-TypeScript-aware rule to TypeScript.** It named
`tests/**/*.ts` while setting the CORE `no-unused-vars`, which v9 deliberately
turns off for `.ts` in favour of the `@typescript-eslint` version. The core
rule reads the parameter names inside a function TYPE as bindings, so

    t?: (app: string, key: string) => string

reports `app` and `key` as unused variables, and every unused `catch (e)` in a
`.ts` spec reports twice. The block is split now: `.js`/`.mjs` on the core
rule, `.ts`/`.tsx` on the TypeScript one, same patterns on both.

Also: stale `eslint-disable` comments naming plugins eslint 10 no longer
registers, which are themselves errors ("Definition for rule ... was not
found"), and a rule that must not parse shell scripts.

The genuinely real findings were the useful part: dead locals, unused imports,
dead helper functions, unused `catch` bindings, extensionless relative imports,
and a handful of `== null` comparisons spelled out so they still match null AND
undefined.

Verified per app: `npm run lint` 0 errors over src + tests + scripts,
`prettier --check` clean, and the unit suite still green.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/launchpad @ 68224bf

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 ✅ 526/526
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-01 10:53 UTC

Download the full PDF report from the workflow artifacts.

… inside (#526)

its own card.

⚠️ THE CAUSE IS AN INSET, NOT A COMPONENT. A card widget renders `flush` and
then has its padding put BACK by the card-fit rule (`padding: 8px 14px`), so
the KPI sits inside the wrapper while the WRAPPER draws the border, radius
and background the user reads as "the card". The library's
`--clickable:hover` rule then drew a 2px border and a drop shadow on the KPI
itself, 8-14px in from the edge being hovered.

Measured live on dossiq, while genuinely hovered: the tile carried a 2px
rgb(0,103,158) border and its own shadow, inset 9px from the wrapper's top
and 15px from its left. A non-card-fit tile sits at 1px, so its border lands
on the wrapper's own edge and reads as one card. That inset is the whole
difference, which is why it looked app-specific and was not.

nextcloud-vue#932 moves the affordance to the wrapper. No layout change:
nothing moves, one card outlines. Verified in the browser with a real pointer
hover:

  before   inner 2px rgb(0,103,158) + shadow   wrapper grey, no shadow
  after    inner transparent, no shadow        wrapper rgb(0,103,158) + shadow

Every app with clickable stat, gauge or delta tiles had this. This bump is
one of the fleet sweep that clears it.

Dependency change only: package.json and the nextcloud-vue entry in
package-lock.json.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/launchpad @ 94b8265

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 ✅ 526/526
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-02 19:06 UTC

Download the full PDF report from the workflow artifacts.

Conduction Release Bot and others added 3 commits September 3, 2026 11:43
Schema property descriptions render as help text under form fields, so
whatever is written there is product copy. Two things in it were not
meant for a reader.

Requirement ids: 8 strings carried a trailing "(REQ-DASH-011)" or
similar. That is our traceability, and it means nothing to the person
filling in the field.

Em-dashes: 11 strings used one. Voice rule 8 bans them outright, so each
is rewritten with a comma, a colon or a full stop rather than swapped
for a different dash.

One description also lost a paragraph explaining that a union type is
rejected by the schema importer, and why that once prevented the schema
being created. True, useful, and addressed to whoever maintains the
register rather than to the person filling in the form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…text

docs(schemas): keep spec ids and em-dashes out of text a user reads
* feat(nav): give launchpad the Documentation and roadmap chrome

ADR-114 Decision 1: seven items belong at the bottom-left of every Conduction
app. CnAppNav draws Personal settings and Admin settings itself; the other five
are the app's own. launchpad shipped one of those five.

Adds Documentation (footer, order 90, the app's own docs site) and Features &
roadmap (footer, order 100) with its type:"roadmap" page, which ADR-018 has
required of every app since it was written.

Both icons had to be registered in src/icons.js. An unregistered name renders
NO glyph, not a fallback, so shipping the entries alone would have put two
blank rows in the footer. gate-60 caught it.

* fix(l10n): carry the roadmap label in the English catalogue too

The nl catalogue had it and en did not, so the two disagreed about a string
this app now ships.

* fix(l10n): translate the new chrome labels into every required locale

tests/l10n/check-l10n-parity.js requires all 36 European locales to carry every
English source key, in BOTH the .json catalogues and the .js ones. A new
user-facing label is therefore 36 translations, not one.

Measured against origin/development before and after: 0 missing keys on the base,
so these are the strings this branch introduced and nothing else.

launchpad has no scripts/build-l10n-js.js, so the .js catalogues are maintained
by hand and each got the key in the file's own style.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/launchpad @ 134a207

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 ✅ 526/526
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 10:24 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Quality Report — ConductionNL/launchpad @ 4354856

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 ✅ 526/526
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-09-03 10:56 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 6 commits September 3, 2026 14:52
Every push to development fired Code Quality TWICE on the same commit: once
for the push event, once for the pull_request event of the permanently-open
"Release: merge development into beta", whose head_ref IS development.

The concurrency block is not the bug and must not be changed: it suffixes the
group by event name deliberately so the push run is NOT cancelled by the PR
run, because the push run is the only carrier of Coverage Baseline Check, SBOM
and Features Extract. The consequence is that both lanes run to completion, so
the duplicate has to be dropped at the job.

The PR run is the one to drop: its head sha IS development's, which the push
run already decided, and it runs strictly fewer jobs.

Not done by removing `beta` from pull_request.branches, though that would also
stop it: the release/v* and sync/main-to-beta PRs target beta too and
genuinely need their run. Only the head_ref:development PR is redundant. A
development -> main promotion PR is skipped by the same clause, for the same
reason and just as correctly.

Proven on openregister#3370 before rolling out: run 33757014196 (pull_request,
sha 4f3ef5e9) completed as `skipped` with ZERO jobs in 72s, while the push run
for the same sha ran the full suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR-114 puts Documentation, Reports and Features & roadmap in the footer
in that order. LaunchPad had two of the three, with room at 95.

One card, deliberately. LaunchPad's register holds a single schema —
dashboard — so a second report would either repeat this one or invent a
reading the data cannot support. The `widgets` array holds widget
descriptors rather than references, so grouping by it would bucket
objects and render noise; it is left out rather than shipped as a chart
that looks like an answer. The e2e asserts the count, so a schema added
later without a report is something a test notices.

The report is a declarative type:dashboard page over the app's own
register. No bespoke Vue component and no per-app controller. Every
filter is scalar equality, the only kind OpenRegister's aggregation
endpoint can evaluate.

Ten strings across all 36 locales. Parity passes on development and still
passes here.
feat(chrome): give launchpad a Reports page with one honest report
Three defects, all found by actually RUNNING the specs rather than
collecting them. Until now they had only ever been listed, which proves a
spec parses and nothing else.

1. The cn-nav-* testids are on the <li> WRAPPER. The clickable element and
   the href both live on the <a class="app-navigation-entry-link"> inside
   it, so clicking the li resolved the locator and then never became
   actionable — a 30s timeout that reads like the navigation is broken —
   and reading href off the li gave null.

2. On a FRESH instance the setup wizard modal opens over the app and
   intercepts pointer events. Every nav click failed while every URL
   navigation passed, which is exactly the shape that hides this: only the
   click-through tests break, and only on a clean install.

3. A bare getByText('Open') matched the SVG <title>Opens in a new tab</title>
   on an external-link icon — attached, hidden, unrelated. Single-word
   probes are now scoped to the page body.

Verified against a throwaway Nextcloud (not the shared :8080, and not the
neighbouring sessions' containers): planninq 6/6 and keepiq 5/5 green.
fix(e2e): make the chrome specs pass in a real browser
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.9 to 2.2.12.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dependabot Bot and others added 15 commits September 4, 2026 09:17
)

Bumps [conduction/hydra-gates](https://github.com/ConductionNL/.github) from 1.10.0 to 1.11.0.
- [Commits](ConductionNL/.github@v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: conduction/hydra-gates
  dependency-version: 1.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#532)

Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.68.0 to 8.69.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ADR-114 puts Documentation, Store, Reports and Features & roadmap in the
footer in that order. This app had three of the four; gate-107 goes from
4 of 5 to 5 of 5.

The page writes no backend. Openregister hosts the store plane, so
adopting it is a type:"store" page plus a top-level `store` block and
nothing else. With no registry configured the page renders the app's own
items and makes no network call.

There is deliberately no `installable` allowlist, following decidiq's
precedent: a configuration set exists to introduce schemas the instance
does not have yet, so listing schemas this app already owns would refuse
exactly the sets worth installing. An EMPTY `installable` would mean
install NOTHING rather than install anything, so omitting the key and
declaring an empty list are different things.

StoreOutline is registered in src/icons.js. An unregistered name renders
no glyph at all, with no fallback and no console error.

nextcloud-vue moves ^2.31.1 -> ^2.36.1, the version that ships
CnStorePage; without it the store page renders nothing. The published
tarball was unpacked to confirm the component is in it rather than
trusting the version number.

The chrome spec's footer test goes from three items to four, and a new
test opens the Store surface and asserts it renders on a plain instance
with no registry configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(chrome): give launchpad a Store
…are Run button (#543)

The demo-data step was a Run button under a paragraph. The paragraph said the
data was safe to load and safe to delete. Neither said what was about to land
in the operator's register, and there was no way to say no.

## Declining was unsayable, and that reopened the wizard for ever

This app implements a `skip-demo-data` action. No manifest step could reach it:
the only step was the run-action that INSTALLS. So an operator who did not want
example data had no way to record that, `demo-data` stayed `done: false`, and
CnAppRoot reopens the wizard while any optional step is outstanding. On every
fresh browser profile, over every page, until they imported data they did not
want.

Measured across the fleet on 2026-09-05: fifteen apps implement that action and
not one declares a step that can post it.

## What the step asks now

Two cards, read from the server: "None, I will set this up myself" and the
dataset this app ships, with its object count. Picking one is an answer, and
`none` closes both steps without importing anything.

The list comes from `GET /api/setup/status` as `datasets` — the step declares
`optionsSource` and carries no options of its own, so nothing in the manifest
can disagree with what will actually be imported. The count is read from the
descriptor file, so the card promises the number that lands.

The card's description carries NO number, deliberately: the wizard translates a
description by literal lookup, so an interpolated count would leave a Dutch
operator reading English. The count travels as `objectCount` and the card
renders it as a stat with a label the library translates.

## Compatibility

`install-demo-data` still works and still means "the dataset this app ships",
so a runbook or script that posts it keeps working. `skip-demo-data` now writes
both keys rather than only the decision flag.

## Verification

21 PHPUnit tests pass, 9 of them new. PHPCS clean, ESLint clean, prettier
clean. All 75 applicable hydra gates green.

Requires @conduction/nextcloud-vue with CnChoiceCards and hydra-gates >=
v1.15.0 for manifest schema 2.33.0.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…545)

The demo-data step is a choice followed by a load step now, and the CI
seed settles the optional steps by posting `skip-demo-data` — which
records `demo_dataset = none`. The load action then correctly imports
nothing and answers "No example data was loaded", so `installing the
demo data reports HOW MUCH landed` asserted against a decision it never
made. It went red on every app whose seed reaches that line.

It picks the shipped dataset first now, and it reads the id from
`/api/setup/status` rather than writing a literal: the choice step reads
its options from exactly that list, so a hardcoded id can pass while the
list an operator sees is empty.

The `api()` helper gained a body, which it needed to post the choice at
all.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
LaunchPad does not render CnAppNav, and that is deliberate: it does not
root on CnAppRoot/NcContent, and App.vue says so where it writes its own
`.workspace-shell` — org navigation rail, slide-in sidebar, branded
DashboardFooter, its own skip link.

Five tests asserted `[data-testid="cn-nav"]` and
`.cn-app-nav__footer-list` anyway, from "feat(chrome): give launchpad a
Store" (2026-09-04) onward. The E2E leg has been red on every push since:
a beforeEach waiting 30 s for a nav that cannot appear, reported as five
broken features. The manifest got the Store entry; the shell never got
the nav.

The chrome IS declared — Documentation, Store, Reports and Features &
roadmap, in the manifest's footer section — and its destinations do work.
So the tests check that instead: the four entries in order, each with an
icon, Documentation as an external href, and each of the other three
opening the page this app hosts. Read from the manifest rather than
restated, so a renamed entry fails rather than going stale. Reports, the
dashboards report, Store and Flows are reached by route, which is how
they are reachable here.

A new first test asserts the absence of CnAppNav. It is the premise the
others rest on, and it is the tripwire: if this app ever adopts
CnAppRoot, that fails where the reason is written down instead of leaving
four tests passing against chrome that moved.

The settings-foldout test drops its personal-settings assertion, because
that entry is a CnAppNav widget with no equivalent in this shell. What
survives is the part about LaunchPad rather than about the component: the
admin settings section and the Flows page open.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ure (#549)

LaunchPad declares nine pages in its manifest and serves ONE. /store,
/reports and /flows each redirect to /dashboard, and /reports/dashboards
renders the workspace grid.

That is not a bug in these tests. It is the app: LaunchPad has no
vue-router at all — createRouter appears nowhere in src/ — because
navigation is Pinia state that never touches the URL, and it is the only
app in the fleet that does not root on CnAppRoot. main.js has called
this Tier 1 since manifest adoption landed and named the change that
would fix it. That change was never filed, so it is filed here:
openspec/changes/launchpad-manifest-tier-3/.

The four route-dependent assertions are marked test.fail() against it.
Not skipped: a skipped test proves nothing and stops being read, while
Playwright FAILS the run when a test.fail() test passes — so landing
routing turns these red until the markers come off, in the change that
earns it.

What stays a live assertion is the half that is true today and worth
guarding: the manifest declares four footer destinations in ADR-114's
order, each with an icon, Documentation as an external href, and each of
the other three naming a page the manifest hosts. The admin settings
section splits out of the old foldout test and keeps passing, because it
is a Nextcloud settings route rather than one of this app's own.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(routing): serve the pages the manifest declares

LaunchPad declared nine pages and served ONE. /store, /reports and
/flows each redirected to /dashboard, and /reports/dashboards rendered
the workspace grid — because there was no vue-router at all.
`createRouter` appeared nowhere in src/: navigation was Pinia state that
never touched the URL, and this was the only app in the fleet that did
not root on CnAppRoot.

main.js has named this change since Tier 1 adoption landed. Its own file
header already described the intent — "App.vue watches it and passes the
live value down to CnAppRoot" — and the implementation stopped short.

WHAT CHANGED

App.vue roots on CnAppRoot. The route table is derived from
manifest.pages, so a declared page is a routed page by construction.
`/` and `/dashboards/:id` render WorkspaceApp; the rest render their
declared page types.

The org navigation rail stays exactly where it is. It is NOT an app
menu — it renders an org-wide tree from GET /api/admin/org-navigation,
group-filtered, with its own position setting — so it coexists with
CnAppNav rather than folding into it, and it renders only when an admin
has configured a tree.

The bespoke skip link is gone. It existed BECAUSE this app did not root
on NcContent; CnAppRoot renders it, and two bypass links are worse than
one. #launchpad-main-content keeps tabindex="-1" for the quick-search
Esc contract, which needs it independently.

A dashboard has an address now. /dashboards/:id was declared all along
and routed nowhere; WorkspaceApp watches the param and switches. An
unknown id is left alone deliberately — the store already resolves a
sensible active dashboard, and overriding would make a bad link empty
the page instead of falling back.

TWO PAGES WERE NOT PAGES

admin-settings named AdminSettingsPage, which has never existed, and
admin-templates-index named TemplatesPage, which is a TAB inside the
Nextcloud admin section. Routing them would have rendered half an admin
surface beside the real one. Both resolve to AdminSettingsRedirect,
which sends the operator to /settings/admin/launchpad and renders a real
anchor as well as navigating.

TESTS

The four test.fail() markers come off — this is the change that earns
them. The tripwire is INVERTED rather than deleted: it asserted the
absence of CnAppNav, and now asserts the shared chrome renders with the
workspace still inside it. A new test asserts a dashboard has an
address, asking the app which dashboards exist rather than seeding an id.

Verified: check:manifest, format, check:schema-l10n and lint pass;
webpack compiles (exit 0, the 3 warnings are the pre-existing bundle-size
ones); vitest WorkspaceApp 14 passed; playwright --list collects 9.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(chrome): give CnAppNav a slot, and answer the two gates

Three findings, all real.

🔴 REQ-SHELL-001 SUPPRESSED THE NAVIGATION SLOT. PageController passed
`id-app-navigation => null` so Nextcloud allocated no left panel — the
app rendered its own slide-in sidebar and nothing else. CnAppRoot renders
NcContent, and NcContent allocates that panel for CnAppNav, so leaving
the suppression in place would have left the shared chrome with nowhere
to render: an empty rail, not an error. Every other app in the fleet
passes no slot ids at all. Found by reading the spec this change
contradicts, not by CI.

gate-16 wanted @SPEC on three changed methods. The change had no spec to
point at, so one is written — `specs/manifest-routing/spec.md`, six
requirements including REQ-ROUTE-004, which records that it supersedes
REQ-SHELL-001's chrome-slot clause. Every anchor was checked to resolve;
a tag that merely exists is not evidence.

gate-26 wanted a visual baseline for two new page components.
AdminSettingsRedirect gets a real test: both admin routes are opened and
must either reach /settings/admin/launchpad or render the notice, since
the redirect deliberately does both. WorkspaceApp gets `@visual exclude`
with the reason — it is not a new screen, it is this app's oldest and
only one, and a baseline captured now would be a baseline of the screen
that already shipped.

Verified: all 78 applicable hydra gates pass locally against
origin/development; check:manifest, format and lint green; phpcs clean on
PageController; playwright --list collects 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(e2e): settle the support note once, for the whole run

The tier-3 push run came back 62 failed / 85 passed. Every one of the 62
is a click that never became actionable, across a dozen unrelated spec
files, and the call log names the same thing each time:

    <div data-testid-modal="cn-support-dialog" class="dialog__modal modal-mask">
    subtree intercepts pointer events

CnAppRoot mounts CnSupportDialog on first open by default. This app
rooted on CnAppRoot in launchpad-manifest-tier-3, and inherited it. The
chrome spec was among the survivors only because it happens to dismiss
[data-testid="cn-modal"] in its own beforeEach — which is why the tier-3
PR went green on every check and the regression only appeared on the
push run, where the E2E leg actually runs.

Dismissal persists per USER through the app's preferences endpoint, so
recording it once in globalSetup settles it for every spec — the same
shape as dismissFirstRunWizard beside it, and the same reasoning the
walkthrough sentinel already documents two paragraphs down: the marker is
per user, so without it the suite is order-dependent and whichever spec
runs first wears the dialog.

The localStorage key goes with it, because the composable falls back to
it when the server read fails and a flaky preferences call would
otherwise put the mask back.

⚠️ NOT `:support-dialog="false"` in the app. That would take a real
feature away from real users to make the tests quiet.

Verified: format, lint and check:manifest pass. The standalone `tsc`
TS5097 on this file is pre-existing — identical on origin/development
without this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(e2e): the report and store pages are not the workspace

Two of the 62 were not the modal mask. They are my own assertions, and
they were stale the moment routing landed.

`the dashboards report renders real numbers` and `Store opens the hosted
store surface` both asserted `.workspace-shell` on their page. That
passed before `launchpad-manifest-tier-3` for the wrong reason: every URL
fell back to the dashboard, so the workspace WAS on every page. It routes
now, and those pages are the report and the store — the workspace being
absent is the change working.

They assert the shared chrome instead, which is what those pages do have.

The rest of that spec is the direct evidence tier 3 works: `each declared
chrome destination opens`, `Reports lists the one report this app can
honestly offer`, `the Flows page opens` and `the admin manifest pages
redirect` all PASSED on the same run — every route that used to fall
through to /dashboard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t behind it (#555)

#551 adopted the shared shell, and the dashboard switcher has been unreachable
ever since. 58 e2e tests across eleven specs fail as `locator.click` timing out,
and all 240 interception lines in that run name the same element:
`[data-testid="cn-nav"] .app-navigation`. The panel slides out, it is visible,
and every click on it lands on the navigation instead.

This is a stacking context, not a z-index. The sidebar is `position: fixed`,
which escapes the scroll flow but not the stacking context. Until #551 this view
WAS the page, so there was no context to escape and nothing else claimed the
left 280px. Now it renders inside `#app-content`, a sibling of
`.app-navigation`, and NcAppNavigation carries `z-index: 1400` on itself. A
sibling with a z-index paints above the whole of a sibling subtree whose own
z-index is auto, so the sidebar's 1500 could never win: it was competing inside
a box that had already lost. Raising the number again would have changed
nothing, which is worth saying because that is the obvious first move.

The sidebar and its backdrop are therefore teleported to `body`, making them
siblings of the navigation rather than descendants of the content, which is the
only place their z-index means what it says.

The backdrop goes from 999 to 1450 while it is there. At 999 the navigation
painted over it even once teleported, so a click meant to dismiss the sidebar
navigated instead. 1450 puts it above the navigation and below the sidebar it
sits behind.

Verified locally: 682 unit tests pass, webpack build clean, eslint and prettier
clean.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ve (#557)

* fix(shell): the dashboard switcher opens above the app navigation, not behind it

#551 adopted the shared shell, and the dashboard switcher has been unreachable
ever since. 58 e2e tests across eleven specs fail as `locator.click` timing out,
and all 240 interception lines in that run name the same element:
`[data-testid="cn-nav"] .app-navigation`. The panel slides out, it is visible,
and every click on it lands on the navigation instead.

This is a stacking context, not a z-index. The sidebar is `position: fixed`,
which escapes the scroll flow but not the stacking context. Until #551 this view
WAS the page, so there was no context to escape and nothing else claimed the
left 280px. Now it renders inside `#app-content`, a sibling of
`.app-navigation`, and NcAppNavigation carries `z-index: 1400` on itself. A
sibling with a z-index paints above the whole of a sibling subtree whose own
z-index is auto, so the sidebar's 1500 could never win: it was competing inside
a box that had already lost. Raising the number again would have changed
nothing, which is worth saying because that is the obvious first move.

The sidebar and its backdrop are therefore teleported to `body`, making them
siblings of the navigation rather than descendants of the content, which is the
only place their z-index means what it says.

The backdrop goes from 999 to 1450 while it is there. At 999 the navigation
painted over it even once teleported, so a click meant to dismiss the sidebar
navigated instead. 1450 puts it above the navigation and below the sidebar it
sits behind.

Verified locally: 682 unit tests pass, webpack build clean, eslint and prettier
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(shell): the floating controls move with the backdrop they sit above

#555 teleported the sidebar and its backdrop to `body` and raised the backdrop
to 1450. It should have moved the floating controls at the same time, and the
e2e said so within one run.

Those controls are `position: fixed` with `z-index: 1000`, and that number was
not arbitrary: it sat just above the backdrop's original 999 so that "click the
hamburger again to close the sidebar" works — the sidebar is open, the backdrop
is up, and the toggle has to stay reachable through it. Teleporting one half of
that pairing broke it. The backdrop became a body-level sibling while the
controls stayed inside `#app-content`, whose own z-index is auto, so the
backdrop painted over them whatever number they carried.

`runtime-shell-canEdit` names it exactly: the backdrop intercepting pointer
events over a button the same log calls visible, enabled and stable.

So both halves move, and the controls go to 1460 — above the backdrop, below the
sidebar they never overlap. The comment says which number it is above and why
the Teleport is what makes it mean anything, because the next person to see 1460
will otherwise try raising it.

Verified locally: 682 unit tests pass, webpack build clean, eslint and prettier
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…'s own (#559)

* fix(shell): compose the runtime manifest instead of replacing the app's own

Reproduced on a clean instance rather than reasoned about, and the finding is
worse than the 13 failing tests suggested: with tier 3, launchpad rendered
NOTHING on a fresh install. `#workspace-vue` held 7 bytes, an empty comment,
against 43,564 for the same instance on the commit before tier 3.

`GET /api/manifest` is not this app's manifest. ManifestController says so in
its own docblock: it assembles a document from the user's OpenRegister dashboard
objects, one page and one menu entry per dashboard, and "when the user has no
dashboards the manifest returns empty pages/menu".

App.vue read `runtimeManifest?.value ?? this.manifest`. An empty object is
truthy, so a user with no dashboards got a manifest with no pages and no menu,
and tier 3 builds both the router and the nav from the manifest. A user WITH
dashboards was no better off, only less obviously: the runtime document still
replaced the nine declared pages, the ADR-114 footer and the walkthrough, which
is what the chrome and routing specs have been failing on.

So the two are composed, which is what each is for. The bundled manifest owns
the declared surfaces; the runtime one contributes the dashboards only the
server knows about, and the live `runtime` block.

THE SETUP WIZARD CAME WITH CnAppRoot, and nothing accounted for it. CnAppRoot
opens CnSetupWizard while ANY step is outstanding, optional or not
(nextcloud-vue#806) — and `completed` is not `settled`: the status endpoint
answered `"completed": true` while still reporting `demo-data` and
`load-demo-data` as `done: false`. `cn-wizard-dialog` is an aria-modal that
intercepts pointer events, so every click landed on it. Measured locally: with
the wizard up, dashboard-sharing failed 4 of 4; with the choice recorded,
runtime-shell-canEdit and add-widget-modal ran 10 passed, 0 failed. globalSetup
now records the choice once, because the marker is per user and settling it per
spec makes the suite order-dependent.

Three manifest references could never resolve. The `dashboards` menu entry and
two walkthrough steps pointed at `dashboard-detail`, whose path is
`/dashboards/:id`, so vue-router threw `Missing required param "id"` on every
render. They were inert until tier 3 gave the manifest a real router. A third
step named the `Flows` PAGE where a menu-entry id belongs.

Six labels were raw i18n keys. `launchpad.menu.dashboards` and five others
render as the key itself in all 36 locales but Dutch, which is the only one
carrying them. They are plain English now, like every other entry in this menu
already was. Dutch loses a translation it only ever had for these six; that is
the same position `Flows`, `Store` and `Reports` are already in, and worth a
follow-up for all of them together rather than a special case for these.

682 unit tests pass. The development push run is the E2E proof, since that leg
does not run on a pull request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(nav): the admin entry is not called Settings, because the foldout already is

gate-63 caught the label I chose. ADR-079 reserves `settings`, `appsettings` and
`app settings` for a settings-foldout entry, because NcAppNavigation's foldout is
ITSELF labelled "Settings" — an entry by that name renders as Settings >
Settings, which is the duplication ADR-079 D1 removed a whole menu to stop.

Replacing the raw `launchpad.menu.adminSettings` key with "Settings" walked
straight into it. "Admin settings" says where the entry goes — it redirects to
/settings/admin/launchpad — and is not reserved.

⚠️ FOLLOW-UP, NOT SILENTLY FIXED HERE: this entry now carries the same label as
the one nc-vue's shared chrome already renders, and both lead to the same admin
page. ADR-079 D1's precedent says an in-app duplicate of the Nextcloud admin
settings page should go, not be renamed. Removing a menu surface is a product
decision rather than a gate fix, so it is named here instead of taken.

Verified locally: gate-63 reports `checked 2 manifest(s): 0 failure(s)`, 682
unit tests pass, webpack build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(l10n): the new admin label has a Dutch key, so it does not render English

gate-102 caught what my last commit deferred, and it was right to: a manifest
string with no `l10n/nl.json` key renders its English source to a Dutch user, and
nothing else reports it. `check:l10n-js` compares nl.json to nl.js, and a string
absent from BOTH is in sync.

⚠️ CORRECTION TO THAT COMMIT MESSAGE. It said Dutch loses a translation it only
ever had for these six labels. That was wrong, and the gate is what measured it:
five of the six English strings I introduced — Dashboards, Templates, Settings,
Dashboard — were ALREADY keys in nl.json, so replacing the dotted i18n keys cost
Dutch nothing there. Only "Admin settings" was new, because only that label had
no prior English form to have been catalogued under.

So there is no follow-up owed for the other five, and the one that was genuinely
missing is added rather than noted: "Beheerinstellingen", matching the file's own
"Instellingen > Beheer" and "beheerder".

Verified locally: gate-102 `checked 51 manifest string(s)` with no findings,
gate-63 `0 failure(s)`, prettier clean, 682 unit tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
All eleven of launchpad's remaining e2e failures, across two specs, were one
defect wearing a misleading name — and a second, smaller one behind it.

NcSelect defaults `appendToBody` to true, so vue-select moves the open menu to
`<body>` and positions it with inline styles. Its z-index reads
`var(--vs-dropdown-z-index)`, which nc-vue declares on `body` as 9999. NcModal
does not use a fixed z-index at all: it assigns one at RUNTIME, counting up as
modals open — measured 10005 for the first and 10010 for the second on one
page. Every one of those is above 9999, so the modal always wins and the menu
is drawn underneath it.

The failure never names the select. `elementFromPoint()` at the option's own
centre returns whatever modal content happens to sit over it, so Playwright
reported `<p class="dashboard-config__hint"> intercepts pointer events` and a
reader goes looking at the hint. That is why this survived three rounds of
fixes: the interceptor named in the log is innocent.

⚠️ NOT THE SAME BUG as `.dashboard-config__field--select` in
DashboardConfigModal.vue, and that rule cannot help. It raises the FIELD so a
menu rendered INSIDE the modal clears the fields below it. This menu is not
inside the modal at all.

⚠️ TWO OBVIOUS FIXES BOTH LOSE, MEASURED RATHER THAN ASSUMED. A plain
`.vs__dropdown-menu { z-index }` is overridden by nc-vue's own
same-specificity rule in `_plugin-vue_export-helper-*.chunk.css`, which loads
after this file: the rule shipped in the bundle and the computed value stayed
9999. Redeclaring the variable on `:root` loses to nc-vue's declaration on
`body`, which is more specific for everything inside it. Setting the variable
on the MENU beats an inherited value whatever the source order, so this needs
no `!important`.

THE SUPPORT NOTE WAS SETTLED FOR ONE USER OF THE TWO. `global-setup.ts` records
it through the logged-in admin's own preference and the marker is per user, so
the suite settled it for exactly one of the people it logs in as. With the
z-index fixed, "recipient sees the shared dashboard in their switcher" was the
last failure standing, and the call log named `cn-support-dialog` intercepting a
toggle it also called visible, enabled and stable. seed.sh now settles it for
every seeded account, with occ, because occ is the only thing there that can
write another user's preference — the app's own endpoint writes the caller's.
That mirrors the firstrunwizard block already above it, which documents this
same shape for the same test.

Verified against a real instance, not reasoned about: conditional-visibility-
editor 13 passed of 13, dashboard-sharing 4 passed of 4. Both were 0 of their
CI-failing subsets before.

The z-index collision belongs upstream in nextcloud-vue, where the modal
counter and the dropdown constant should be related rather than coincidental.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d alone (#562)

Correcting my own description in #561. It said nc-vue declares
`--vs-dropdown-z-index` on `body` as 9999. That is true of nc-vue's chunk
stylesheet, and it is ALSO true of this app's own `css/launchpad.css`, which
re-declares it there deliberately under ConductionNL/nextcloud-vue#864 because
nc-vue 9.10 moved the declaration onto the select root where an appended menu
cannot inherit it. Leaving that out made the new rule look like it replaced the
old one.

It does not. They fix different halves and removing either brings a different
failure back:

  css/launchpad.css        restores INHERITANCE, so the menu receives a value.
  src/styles/workspace.css raises the VALUE above NcModal, which assigns itself
                           a runtime z-index counting up as modals open —
                           measured 10005 for the first and 10010 for the
                           second. Every one beats 9999, so the #864 workaround
                           alone still leaves the menu under any modal.

That matters because #864 is open and ends with "delete both once upstream
fixes the scoping". Whoever closes it will read this file, and now finds a line
saying the 10100 rule must stay until nc-vue relates the two numbers rather than
choosing them independently. Both comments now point at each other.

Comment-only. No rule changes.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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