Skip to content

Release: merge development into beta - #71

Merged
rubenvdlinde merged 1557 commits into
betafrom
development
Aug 20, 2026
Merged

Release: merge development into beta#71
rubenvdlinde merged 1557 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

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit 1a6f970
Branch 71/merge
Event pull_request
Generated 2026-03-19 19:06 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23312024194

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (110 total)

Metric Count
Approved (allowlist) 110
Approved (override) 0
Denied 0

npm dependencies (266 total)

Metric Count
Approved (allowlist) 265
Approved (override) 1
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 10 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit cc14e0c
Branch 71/merge
Event pull_request
Generated 2026-03-19 21:38 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23318043060

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (110 total)

Metric Count
Approved (allowlist) 110
Approved (override) 0
Denied 0

npm dependencies (266 total)

Metric Count
Approved (allowlist) 265
Approved (override) 1
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 10 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit 7531e25
Branch 71/merge
Event pull_request
Generated 2026-03-22 21:13 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23412628554

Summary

Group Result
PHP Quality FAIL
Vue Quality FAIL
Security FAIL
License FAIL
PHPUnit FAIL
Newman FAIL

PHP Quality

Tool Result
lint FAIL
phpcs FAIL
phpmd FAIL
psalm FAIL
phpstan FAIL
phpmetrics FAIL

Vue Quality

Tool Result
eslint FAIL
stylelint FAIL

Security

Ecosystem Result
composer FAIL
npm FAIL

License Compliance

Ecosystem Result
composer FAIL
npm FAIL

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Result
FAIL

Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit 7f052ed
Branch 71/merge
Event pull_request
Generated 2026-03-22 21:15 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23412642650

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit FAIL
Newman SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (110 total)

Metric Count
Approved (allowlist) 110
Approved (override) 0
Denied 0

npm dependencies (266 total)

Metric Count
Approved (allowlist) 265
Approved (override) 1
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Integration Tests (Newman)

Newman integration tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor Author

Quality Report

Repository ConductionNL/docudesk
Commit 6c9e313
Branch 71/merge
Event pull_request
Generated 2026-03-22 21:20 UTC
Workflow Run https://github.com/ConductionNL/docudesk/actions/runs/23412678437

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (110 total)

Metric Count
Approved (allowlist) 110
Approved (override) 0
Denied 0

npm dependencies (266 total)

Metric Count
Approved (allowlist) 265
Approved (override) 1
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 10 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

rubenvdlinde and others added 17 commits July 29, 2026 17:12
…bject write (#350)

* fix(grondslag): guard string casts so array-valued OR properties stop flooding the log

GrondslagProposalService runs on the OpenRegister object-write path and cast
`$base['name']` / `$base['description']` with a bare (string) cast. An
OpenRegister property may legally be an array (multi-value / nested), and
casting one raises a PHP "Array to string conversion" warning per field per
object.

Measured 2026-07-28 on the dev instance: a SINGLE object create produced
6,240 of these warnings (6,623 log lines total), which is a material part of
why `POST /apps/openregister/api/objects/...` was unusable and why
nextcloud.log grows without bound (cf. the 163GB log that filled the Docker
disk and PANICked Postgres).

Adds a scalar-guarded asString() helper and routes both casts through it.

* perf(enrichment): stop building the admin-settings payload on every object write

EnrichmentRunner runs inside OpenRegister's object-created dispatch — i.e.
inside an unrelated app's save. To answer the boolean 'is enrichment enabled'
it called SettingsService::getAllSettings(), which assembles what the ADMIN
SETTINGS PAGE needs: every available register with its schemas, the
object-type configuration, OCR status and the grondslag selector data.

Measured on the dev instance 2026-07-29, one object create:

  SchemaMapper::find() calls from this path   1,471
  share of ALL schema reads during a create      96%

1,471 DISTINCT schemas, so OpenRegister's request cache could not help — the
caller was genuinely walking nearly every schema on the instance (1,917 of
them) to read three IAppConfig booleans.

Added SettingsService::getFeatureToggles() and pointed the listener at it.
Same three values, no register or schema touched.

Effect on an OpenRegister object create (median of 3, same payload):

  wall                     13.7s  ->  3.2s
  schema sequential scans  3,019  ->    58

Part of openregister/openspec/changes/object-write-sub-500ms task 1.
…est on the instance (#351)

* fix(grondslag): guard string casts so array-valued OR properties stop flooding the log

GrondslagProposalService runs on the OpenRegister object-write path and cast
`$base['name']` / `$base['description']` with a bare (string) cast. An
OpenRegister property may legally be an array (multi-value / nested), and
casting one raises a PHP "Array to string conversion" warning per field per
object.

Measured 2026-07-28 on the dev instance: a SINGLE object create produced
6,240 of these warnings (6,623 log lines total), which is a material part of
why `POST /apps/openregister/api/objects/...` was unusable and why
nextcloud.log grows without bound (cf. the 163GB log that filled the Docker
disk and PANICked Postgres).

Adds a scalar-guarded asString() helper and routes both casts through it.

* perf(enrichment): stop building the admin-settings payload on every object write

EnrichmentRunner runs inside OpenRegister's object-created dispatch — i.e.
inside an unrelated app's save. To answer the boolean 'is enrichment enabled'
it called SettingsService::getAllSettings(), which assembles what the ADMIN
SETTINGS PAGE needs: every available register with its schemas, the
object-type configuration, OCR status and the grondslag selector data.

Measured on the dev instance 2026-07-29, one object create:

  SchemaMapper::find() calls from this path   1,471
  share of ALL schema reads during a create      96%

1,471 DISTINCT schemas, so OpenRegister's request cache could not help — the
caller was genuinely walking nearly every schema on the instance (1,917 of
them) to read three IAppConfig booleans.

Added SettingsService::getFeatureToggles() and pointed the listener at it.
Same three values, no register or schema touched.

Effect on an OpenRegister object create (median of 3, same payload):

  wall                     13.7s  ->  3.2s
  schema sequential scans  3,019  ->    58

Part of openregister/openspec/changes/object-write-sub-500ms task 1.

* perf(settings): persist configuration_version so the import gate can close

SettingsInitializer::initialize() reads 'configuration_version' to decide
whether its OpenRegister configuration is already imported. Nothing ever wrote
that key — not this app, not OpenRegister — so $currentVersion stayed at its
'0.0.0' default forever, version_compare never short-circuited, and the full
importFromApp ran again on every call.

It is called from Application::boot(), so that is EVERY request to the whole
Nextcloud instance, not just DocuDesk's own pages.

Measured 2026-07-29 on the dev instance (OpenRegister object create, 10 runs):

  configuration_version absent   median 354ms
  configuration_version present  median 255ms

~100ms per request, ~28%. Confirmed by call-path attribution: the create's
schema lookups drop 23 -> 9, of which the ImportHandler contribution goes
14 -> 0.

DocuDesk is the only app in the fleet that runs a settings import from boot();
the others do it from cron or a repair step. Keeping the call but making the
gate work is the smaller change, and it now self-heals: the first request after
an upgrade imports once and records the version.

Second cost this class of pattern has produced today — see the sibling fix in
0495b9e, where an is-enrichment-enabled boolean reached getAllSettings() and
issued 1,471 schema reads per object create.
* fix(grondslag): guard string casts so array-valued OR properties stop flooding the log

GrondslagProposalService runs on the OpenRegister object-write path and cast
`$base['name']` / `$base['description']` with a bare (string) cast. An
OpenRegister property may legally be an array (multi-value / nested), and
casting one raises a PHP "Array to string conversion" warning per field per
object.

Measured 2026-07-28 on the dev instance: a SINGLE object create produced
6,240 of these warnings (6,623 log lines total), which is a material part of
why `POST /apps/openregister/api/objects/...` was unusable and why
nextcloud.log grows without bound (cf. the 163GB log that filled the Docker
disk and PANICked Postgres).

Adds a scalar-guarded asString() helper and routes both casts through it.

* perf(enrichment): stop building the admin-settings payload on every object write

EnrichmentRunner runs inside OpenRegister's object-created dispatch — i.e.
inside an unrelated app's save. To answer the boolean 'is enrichment enabled'
it called SettingsService::getAllSettings(), which assembles what the ADMIN
SETTINGS PAGE needs: every available register with its schemas, the
object-type configuration, OCR status and the grondslag selector data.

Measured on the dev instance 2026-07-29, one object create:

  SchemaMapper::find() calls from this path   1,471
  share of ALL schema reads during a create      96%

1,471 DISTINCT schemas, so OpenRegister's request cache could not help — the
caller was genuinely walking nearly every schema on the instance (1,917 of
them) to read three IAppConfig booleans.

Added SettingsService::getFeatureToggles() and pointed the listener at it.
Same three values, no register or schema touched.

Effect on an OpenRegister object create (median of 3, same payload):

  wall                     13.7s  ->  3.2s
  schema sequential scans  3,019  ->    58

Part of openregister/openspec/changes/object-write-sub-500ms task 1.

* perf(settings): persist configuration_version so the import gate can close

SettingsInitializer::initialize() reads 'configuration_version' to decide
whether its OpenRegister configuration is already imported. Nothing ever wrote
that key — not this app, not OpenRegister — so $currentVersion stayed at its
'0.0.0' default forever, version_compare never short-circuited, and the full
importFromApp ran again on every call.

It is called from Application::boot(), so that is EVERY request to the whole
Nextcloud instance, not just DocuDesk's own pages.

Measured 2026-07-29 on the dev instance (OpenRegister object create, 10 runs):

  configuration_version absent   median 354ms
  configuration_version present  median 255ms

~100ms per request, ~28%. Confirmed by call-path attribution: the create's
schema lookups drop 23 -> 9, of which the ImportHandler contribution goes
14 -> 0.

DocuDesk is the only app in the fleet that runs a settings import from boot();
the others do it from cron or a repair step. Keeping the call but making the
gate work is the smaller change, and it now self-heals: the first request after
an upgrade imports once and records the version.

Second cost this class of pattern has produced today — see the sibling fix in
0495b9e, where an is-enrichment-enabled boolean reached getAllSettings() and
issued 1,471 schema reads per object create.

* feat(icons): adopt the ADR-077 semantic icon vocabulary

Menu icons across the fleet had drifted into meaninglessness: a scan of 21
manifest-shipping apps found 120 distinct icons for 262 distinct labels, with
one glyph standing for as many as 18 unrelated concepts
(`icon-category-monitoring`) and the same concept drawn differently per app —
Store was `icon-category-integration` in one app and
`icon-category-organization` in another.

Moves this app's menu onto the shared vocabulary: MDI PascalCase names, one
concept to one icon. Tier A entries (Dashboard, Documentation, Settings, Store,
Features & roadmap) now match every other Conduction app, which is the whole
point — a glyph should mean the same thing wherever a user meets it.

Two defect classes are fixed along the way:

* Icon names that do not exist in vue-material-design-icons at all. They could
  never resolve — rendering a help-circle at best, nothing at all in the
  navigation.
* Menu entries that rendered with NO icon, because CnAppNav resolves an MDI name
  only through the registry `registerIcons()` populates, with no fallback for a
  name the app never registered. Apps that relied on legacy `icon-*` classes
  registered nothing at all and were fine until the first MDI name appeared.

src/icons.js is generated from the app's own manifests and register files, so
every name the app references is registered and the migration stands on its own
against the CURRENTLY RELEASED @conduction/nextcloud-vue — it does not wait on
the library-side vocabulary (ConductionNL/nextcloud-vue#563).

Verified: 0 menu entries render without an icon (was 51 fleet-wide), every icon
import resolves against the app's own node_modules, and hydra's gate-60
icon-vocabulary check passes with no failures or warnings.

Spec: ADR-077 (ConductionNL/hydra#408).
Fix Conduction → ConductionNL org reference (all workflows failed with
"workflow was not found") and rename the branch-protection caller job
`check` → `branch-protection` to satisfy the org ruleset's required check.
…rest (#354)

* perf(events): declare DossierCheckedOnListener's register/schema interest

DossierCheckedOnListener only ever acts on `docudesk`/`dossier` objects
(its own REGISTER / DOSSIER_SCHEMA constants), yet it was registered
globally: every object write anywhere on the instance constructed it and
its GrondslagenSummaryService dependency before the guard rejected the
object.

Route it through OpenRegister's ObjectEventSubscription, which records
the declared slugs at registration time and dispatches through one shared
proxy, so an uninterested write never reaches the listener. Guarded on
class_exists so an instance whose OpenRegister predates the mechanism
falls back to the exact global registration it replaced.

DocuDeskEventListener is deliberately left unfiltered: DocuDeskEventHandler
identifies its work by payload SHAPE (looksLikeDossier / detectPolicyShape)
and EnrichmentRunner enriches metadata on every object on the instance
regardless of register, so any slug declaration there would silently drop
work.

* fix(events): subscribe from boot() so the OpenRegister guard is order-independent

Declaring the filtered subscription in register() was boot-order sensitive:
Nextcloud enables each app's autoloader immediately before calling that app's
own register(), so OpenRegister's ObjectEventSubscription was only autoloadable
to apps registering after it. DocuDesk is app 21 of 92 and OpenRegister is 52,
so the class_exists() guard here was ALWAYS false and this app fell back to an
unfiltered registration that looked identical to a working narrowing — one of
seven fleet conversions that were inert while reading as successful.

boot() runs only after every app's register() has completed, so the guard
resolves regardless of this app's position. The fallback now logs a warning
naming the app and listener instead of degrading silently.

Verified live on the reference instance: with this change DocuDesk's
DossierCheckedOnListener appears in the subscription registry (count 3 -> 4)
and fires on a docudesk/dossier update (invoked=1, listenerUs=1103) while being
skipped on a larpingapp/character update of the same event class (invoked=0).
…roprietary vue3-apexcharts (#355)

* ci: point reusable workflows at ConductionNL/.github and enable the frontend suites

docudesk's CI has produced ZERO jobs for two months.

Every caller workflow referenced `Conduction/.github` — an org that does
not exist (`GET /repos/Conduction/.github` -> 404). Actions cannot resolve
the reference, so each run fails instantly with no jobs at all. The tell is
visible in `gh run list --json name`: docudesk's runs are named
`.github/workflows/code-quality.yml` (the raw path, i.e. never resolved)
while its one local workflow shows its real name, `l10n`. The most recent
Code Quality run on `development` reports `total_count: 0` jobs.

Introduced by 9279c8c (2026-06-01) "point reusable workflows at
Conduction/.github (org rename)" — the rename went the wrong way. Since
then no ESLint, stylelint, PHPCS, PHPMD, PHPStan, Psalm, licence scan,
security scan, SBOM or PHPUnit has run on this repo.

Fixed in all 8 caller workflows. Two of them (issue-triage, openspec-sync)
also pinned `@feature/openspec-project-sync`, a branch that does not exist
in ConductionNL/.github; both now use `@main`, where the same inputs
(`app-name`, `backlog-existing`, `PROJECT_TOKEN`) are still declared.
code-quality.yml additionally cloned `Conduction/openregister` through the
`additional-apps` input — a `uses:`-only grep would have missed that.

`.forgejo/workflows/` is deliberately untouched: on Codeberg the org
really is `Conduction`.

branch-protection.yml also adopts the job id and `permissions: {}` from
#353 so the check reports as `branch-protection / check-branch`, the
context name the org ruleset requires.

Separately: `frontend-checks` was never set, and it defaults to `"[]"`.
`enable-frontend: true` only buys eslint + stylelint, so the repo's own
77-test Jest suite, 45-test Vitest suite, manifest schema validation and
l10n key check had ALSO never executed on a pull request. All four are
green locally on the pinned node 20 / npm 10 and are now wired in.

* feat: migrate to Vue 3 + @conduction/nextcloud-vue 2.1.0-vue3.13

Vue 2.7 -> Vue 3.5, @nextcloud/vue 8 -> 9, vue-router 3 -> 4, pinia 2 -> 3,
@nextcloud/dialogs 6 -> 7, webpack-vue-config 6 -> 7, vue-loader 15 -> 17,
vue-template-compiler dropped.

@conduction/nextcloud-vue is pinned EXACTLY, no caret. A caret on a
prerelease DOES float: `^2.1.0-vue3.7` resolves all the way up to
vue3.13, and that is how the fleet drifted onto a proprietary dependency.
vue3-apexcharts stopped being open source at 1.9.0 (dual-licence, free
only under $2M revenue, and it forbids sublicensing under different
terms — incompatible with shipping inside an EUPL-1.2 app). nc-vue's
vue3.13 pins `~1.8.0`, the last MIT release. Verified from the LOCKFILE,
not package.json: exactly two apexcharts entries exist in the tree,
`vue3-apexcharts@1.8.0` (MIT) and `apexcharts@4.7.0` (MIT), with no
nested copies. No licence override was added — the core `apexcharts`
reports an identical `Custom: <url>` symptom at 5.x, so overriding by
symptom would have buried this.

Bootstrap — no lint rule covers any of this
  - createApp / createRouter / createWebHistory replace new Vue and
    VueRouter; PiniaVuePlugin dropped; Vue.mixin -> app.mixin.
  - vue-router 4 removed the bare `path: '*'`. It matches nothing and
    raises no error, so the shell renders with an empty <main>. Replaced
    with `/:pathMatch(.*)*`.
  - Mount host renamed `#content` -> `#docudesk-app`. Vue 2's `$mount()`
    REPLACED the matched element, so mounting on templates/index.php's
    duplicate `<div id="content">` quietly replaced core's own wrapper
    from layout.user.php and the duplication never showed. Vue 3 renders
    INSIDE the match, and with two `#content` elements it is undefined
    which one is matched.
  - registerBuiltinDashboardWidgets() is now called explicitly at
    bootstrap: nc-vue's `sideEffects` list lets webpack drop the bare
    imports that register the built-in widgets, which makes them render
    "Widget not available" with no error.
  - settings.js no longer reads `t`/`n` off window globals.

Renamed props that no lint rule can see — 96 bindings across 28 files
  - `<NcButton type="primary">`: v9 repurposed `type` as the NATIVE
    button type and moved styling to `variant`. 49 buttons were rendering
    `<button type="primary">` — not a valid native type, so it behaves as
    submit — while silently falling back to default styling.
    `native-type="submit"` becomes `type="submit"`.
  - NcTextField / NcSelect / NcCheckboxRadioSwitch dropped `value` and
    `checked` in favour of `modelValue`. 18 `:checked`, 18
    `@update:checked`, 5 NcSelect `:value` and 2 `@input` were dead.
    NcCheckboxRadioSwitch keeps a separate `value` prop for radio groups,
    so those were deliberately left alone.
  - ESLint's `.sync` -> `v-model:` autofix preserves the prop NAME, so it
    turned 45 `.sync` modifiers into `v-model:value` on components that
    no longer have a `value` prop — green lint, dead binding. Corrected
    to a bare `v-model`.

Lint
  - eslint.config.js layers `conductionVue3Fixes` from nc-vue LAST. The
    `@nextcloud` v8 base activates ZERO `vue/no-deprecated-*` rules, so
    Vue-2 idioms survived a green lint; 21 are now active (verified by
    severity, not by counting listed rule names).
  - `.eslintrc.js` removed. A marker probe — add a unique rule, re-run
    `eslint --print-config` — showed the marker never reached the
    resolved config while a control rule from eslint.config.js did. It
    was a dead second rule set that could only drift.
  - Errors 4 -> 0, including the 4 that were already red on the base
    branch. PdfViewer's unscoped `<style>` is now scoped via `:deep()`
    anchored on the template-owned `.pdf-viewer__page`; pdfjs builds the
    text layer with document.createElement, so those nodes can never
    carry a scope hash. It had been leaking a bare `::selection` rule
    page-wide.

webpack
  - @nextcloud/vue@9, @nextcloud/dialogs@7 and vue-router@5 ship an
    `exports` map with no `main` and no `module`, so a Vue-2-era alias to
    the package DIRECTORY resolves to nothing. Aliased to absolute files.
  - @nextcloud/vue@9 hard-depends on vue-router ^5.1.0 while the app is
    on 4, so npm installs a nested second copy. Two router instances mean
    two injection keys and navigation dies with no console error; a
    `vue-router$` alias forces the single copy.
  - path-browserify fallback for the bundled FilePicker chunk.
  - USE_LOCAL_LIB is opt-OUT and the shared sibling checkout sits on the
    Vue 2 line, so the default would silently compile Vue 2 library
    sources into this Vue 3 app. Added a major-version guard.
  - gridstack CSS imported: nc-vue lists gridstack as a peerDependency
    and deliberately does not bundle its stylesheet. Without it v12 sizes
    items from an undefined `--gs-column-width` and every dashboard item
    renders 0 px wide with nothing in the console.

e2e harness — was pointed at the SHARED dev container
  - playwright.config.ts resolved `baseURL` as
    `NEXTCLOUD_URL || 'http://localhost:8080'` while global-setup.ts used
    `PLAYWRIGHT_BASE_URL ?? NEXTCLOUD_URL ?? NC_BASE_URL ?? 8080`. The two
    disagree, so the documented
    `PLAYWRIGHT_BASE_URL=... npx playwright test` logged in against the
    intended instance and then ran every spec against :8080 — the shared
    container, which bind-mounts other people's working trees. Both now
    resolve through one strict `tests/e2e/base-url.ts` with NO localhost
    fallback. It accepts `BASE_URL` as well, because that is the name the
    shared quality workflow exports.

Tests
  - Vue 3 wraps anything written into reactive state in a Proxy, so
    `toBe(literal)` against a value the store holds no longer matches.
    Re-anchored on the object the store exposes — still strict identity,
    deliberately NOT relaxed to toEqual. Verified by injecting a
    copy-instead-of-reference bug into the getter: the assertions still
    go red.
  - jest 77/77 and vitest 45/45, identical to the base branch.

* chore(docs): regenerate features.json

The revived `quality / Features Check` job fails on the committed file:
"docs/features.json is out of date — run scripts/extract-features.py to
regenerate".

The drift is entirely pre-existing, not a side effect of the Vue 3 work:
running the shared extractor against unmodified `development` produces the
same 164-insertion / 145-deletion diff, and the two regenerated files are
byte-identical. It simply went unnoticed because the gate has produced
zero jobs since 2026-06-01.

* fix(deps): regenerate package-lock.json so `npm ci` accepts it under npm 10

The first Code Quality run on this PR — the first run this repo has
produced any jobs for in two months — failed 11 jobs at "Install
dependencies":

    npm error `npm ci` can only install packages when your package.json
    and package-lock.json are in sync.
    Invalid: lock file's @noble/hashes@1.4.0 does not satisfy @noble/hashes@2.2.0
    Missing: @noble/hashes@1.4.0 from lock file

`pkijs` depends on exactly `@noble/hashes@1.4.0`, while `@exodus/bytes`
(pulled in by whatwg-encoding) declares `@noble/hashes ^1.8.0 || ^2.0.0`
as an OPTIONAL peer. npm 10's `npm install` hoists pkijs's 1.4.0 to the
root to satisfy that optional peer, then `npm ci` validates the same tree
and rejects it. Install and ci disagree with each other.

This is NOT a Vue 3 regression. Deleting package-lock.json on unmodified
`development` and re-running `npm install` with the pinned npm 10
reproduces the identical error — the committed lockfile only survives
because it is frozen from before `@exodus/bytes` entered the tree in this
shape. Any regeneration hits it, and a Vue 3 migration has to regenerate.

Fixed by generating the lockfile with npm 11 (which nests correctly and
leaves the optional peer unmet, exactly like the pre-existing lockfile)
and then normalising it with `npm install --package-lock-only` under
npm 10, so the file is in npm-10 shape. Verified the only way that counts,
with the major `engines` pins: `npm ci` under npm 10.8.2 installs 1781
packages cleanly. An npm-11-written lockfile on its own is NOT enough —
npm 10 then fails it with EBADPLATFORM on @esbuild/netbsd-arm64.

No `overrides` entry was added. A scoped `pkijs` override and a root
`@noble/hashes` override were both tried and then reverted after a
control run showed the regeneration sequence alone is sufficient; keeping
them would have been inert config implying a constraint that is not doing
any work.

* style(php): fix the 4 phpcs errors the revived gate surfaced

`PHP Quality (phpcs)` was the last red job once CI could resolve its
reusable workflow again. All four errors are pre-existing PHP that has not
been linted since 2026-06-01, and all four are in one file:

  - GrondslagProposalService.php:331,339 — Conduction's standard requires
    named parameters for calls to internal code: `self::asString($x)` ->
    `self::asString(value: $x)`.
  - :546 — two blank lines after a function where one is expected.
  - :549 — `extractObjects()` had a docblock with @param/@return but no
    short description.

phpcs now reports 0 errors across 71 files. The 183 remaining warnings
(mostly the repo's own `@spec` tag sniff) are non-fatal by design —
phpcs.xml sets `ignore_warnings_on_exit=1` — and are left as pre-existing
documentation debt rather than papered over.

* chore(licenses): approve duck and pako — permissive strings the matcher cannot parse

`License (npm)` denied two packages once the gate could run again:

    Disallowed license: duck (0.1.12) uses 'BSD*'
    Disallowed license: pako (1.0.11) uses '(MIT AND Zlib)'

Neither is a real licence problem, and neither is new. Both sit at the
identical version in unmodified `development` (duck via mammoth -> lop,
pako via jszip and browserify-zlib) and were simply never surfaced,
because this job has produced no output since 2026-06-01.

`BSD*` is a legacy non-SPDX spelling and `(MIT AND Zlib)` is an SPDX
compound expression. The allowlist compares whole strings, so it cannot
match either even though BSD, MIT and Zlib are all on it.

This is deliberately NOT the same move as papering over a `Custom: <url>`
symptom: these two declare permissive licences unambiguously, they are
just spelled in forms the matcher does not evaluate.

While here, the pre-existing `apexcharts` override is annotated. It is
keyed on the symptom rather than the licence: apexcharts 4.x is MIT but
5.x is proprietary and emits the same `Custom: <url>` string, so the
override would silently approve 5.x. The `^4.7.0` range nc-vue pins cannot
reach it (4.0.0-4.7.0 verified MIT), but the trap is now written down.

* test(e2e): assert the real template columns instead of a Status column that never existed

The Vue 3 build made this spec fail, and the reason is worth writing down.

`templates table shows the expected column headers` asserted a "Status"
column. src/manifest.json declares the Templates page's columns as
`["name","category","format","namespace","description"]` — there has never
been a Status column, and CnIndexPage has never rendered one.

It passed anyway because the whole block sat behind

    if (await table.isVisible().catch(() => false)) { ... }

On the Vue 2 build the table did not render on this page, the condition was
false, and the test passed by asserting NOTHING. The Vue 3 build renders the
table — an improvement — so the guard stopped short-circuiting and the stale
expectation finally surfaced. The e2e delta on an identically seeded instance
was baseline 10 failed / 59 passed versus 11 failed / 58 passed, and this was
the single difference.

The fix strengthens the test rather than relaxing it:
  - the guard becomes an assertion, so a missing table is a failure, not a
    silent skip (playbook: no `if (...)` guards that pass by asserting
    nothing);
  - all five manifest-declared headers are asserted, not two of them;
  - the absence of a Status column is asserted explicitly, so the manifest
    and this spec cannot drift apart in either direction.

With this, the suite matches the base branch exactly: 10 failed / 59 passed,
same 10 specs, all pre-existing and all traceable to the unseeded
signingRequest / signerRecord schemas described in the PR body.
Psalm, phpstan and phpmd each ended '|| echo ... not installed, skipping...', so they
always exited 0 and the CI jobs invoking them could never fail. Replaced with
shillinq's 'if [ -f vendor/bin/<tool> ]' pattern, which still tolerates a
genuinely absent tool but propagates a real failure.

Measured before flipping (clean worktree at origin/development, PHP 8.3.32,
after asserting the OCP stub resolves):
psalm 1, phpstan 4, phpmd 11.

test:all deliberately left unchanged - its cost is unmeasured.
…#357)

Version alignment onto the current nc-vue vue3 release. Pinned exactly
(no caret) so the resolved version is the reviewed one.

Lockfile-verified: @conduction/nextcloud-vue -> 2.1.0-vue3.16 (single
entry); vue3-apexcharts -> 1.8.0, below the 1.9.0 line that turned
proprietary and forbids sublicensing in our EUPL-1.2 apps. No licence
override is used: core apexcharts is MIT and reports an identical
`Custom: <url>` symptom, so an override would mask the real signal.

Opt-ins vue3.16 makes possible were checked and do not apply: no
`@nextcloud/initial-state` overrides entry to drop, no local
`vue/no-multiple-template-root: off` to delete.

Gates: eslint clean, vitest 45/45, webpack production build clean. The
lint gate was proved live with a throwaway `.vue` probe that fired
vue/no-deprecated-slot-attribute, vue/no-deprecated-filter and
vue/no-deprecated-v-on-native-modifier (exit 1), then exited 0 once
removed. A `.js` probe would not exercise those rules at all.
test:all (and test:unit where present) ended in '|| echo Tests require
Nextcloud environment, skipping...', so phpunit's exit status was
discarded unconditionally and check:strict could never fail on a test
failure. This repo's suite runs standalone, so the message was untrue here.

Positive control, PHP 8.3.32 container, vendor/ freshly installed, adding
one deliberately failing test under tests/:
  old composer.json + failing test -> test:all never named, swallowed
  new composer.json + failing test -> check:strict fails NAMING test:all
  new composer.json, test removed  -> test:all passes

Also passes --no-coverage: this repo's phpunit.xml requests coverage, and
without a driver PHPUnit warns and exits non-zero, which would have made
the newly-live gate red for an environment reason rather than a code one.
shillinq and decidesk already do this.

Tooling only - no findings fixed here.
Adds the four pieces the shared `ConductionNL/.github` quality workflow
needs to run DocuDesk's 19-file Playwright suite on a fresh Nextcloud:

- `.github/workflows/code-quality.yml`: enable-playwright, test path and
  seed command; move the openregister additional-app from `beta` to
  `development`.
- `tests/e2e/playwright.config.ts`: CI-only config declaring ONLY the
  regression project, with report/output at the app root where the
  workflow's upload steps look. The workflow passes no `--project`, so the
  root config would also run `docs-capture` and `visual`.
- `tests/e2e/ci-seed.sh`: explicit register import through OpenRegister's
  admin importer + the app-config object-type bindings DocuDesk resolves
  every write through, both verified, plus a bundle gate.
- `tests/e2e/global-setup.ts`: a missing bundle is now a hard error on CI
  instead of a silent `npm run build` that would heal away the control.
…d log

The importer answers 200 "Import successful" while having silently skipped
individual schemas: ImportHandler catches a per-schema Exception, logs it and
continues. Run 30796295073 imported 14 of DocuDesk's 20 schemas that way, and
the only record of why lives in data/nextcloud.log.
OpenRegister's importer answers HTTP 200 "Import successful" while
skipping any schema that fails validation — ImportHandler catches the
per-schema exception, logs it, and continues. On a fresh install only
14 of DocuDesk's 20 schemas were written. Two causes:

1. `x-openregister-archival.retention` declared as a bare ISO-8601 STRING
   on correspondence, signingRequest, signerRecord, signingAuditEntry and
   batchCorrespondenceJob. ArchivalAnnotationValidator requires an object
   `{ default: <duration>, rules?: [] }` and rejects the string with
   "x-openregister-archival.retention is required and must be an object".
   anonymizationLink and financialExtraction already shipped the object
   form, which is why they imported and the other five did not.

2. publicationProhibition declared an authorization action `write`, which
   is not in OpenRegister's CRUD vocabulary — Schema::validateAuthorizationRules()
   accepts only create/read/update/delete. Expanded to create/update/delete,
   preserving the documented intent (read by the consent group, write by
   docudesk-policy-admins).

Impact: signing was entirely non-functional on a fresh install —
signingRequest, signerRecord and signingAuditEntry did not exist, so every
SigningService write targeted a schema that was not there. Found by the new
e2e seed gate, which verifies every slug the definition declares.

No property, slug or retention DURATION changed. Config version 7.6.2 ->
7.6.3 so the version-guarded boot import re-applies on existing installs.
37 of 66 specs failed on run 30797589151 with selector timeouts. The page
under test was not DocuDesk at all: it was PHP's own 404 —

    heading "Not Found"
    The requested resource /apps/docudesk was not found on this server.

The shared workflow serves Nextcloud with `php -S 0.0.0.0:8080` and no router
script, so nothing performs the .htaccess rewrite of /apps/* onto index.php.
The built-in server resolves the path against the document root, finds no
file, and answers its own error page.

That page is a near-perfect impostor for the checks these specs make: it has
a <body>, it is not /login, and it returns HTTP 404 which no spec inspects.
So the failures pointed at #header, at empty tables, and at
"CSRF request-token must be harvestable" — never at the URL. Worse, every
admin-settings spec PASSED against it, because those only assert a visible
<body> and a non-/login URL.

/index.php/apps/... is served correctly both with and without URL rewriting,
so it is the portable form — the same reasoning that already put index.php in
workflows/_fixtures.ts's API constant. The in-app router agrees: main.js uses
createWebHistory(generateUrl('/apps/docudesk')), and generateUrl emits the
index.php prefix whenever the front controller is inactive.

No assertion weakened: every toHaveURL(/\/apps\/docudesk…/) still matches.
… NOT NULL

`publicationConsent` listed `documentId` in the schema-level `required`
array, so OpenRegister created the backing column NOT NULL. A scope=entity
record — a standing consent — is document-less by definition, so:

  - all 12 seeded standing consents failed to insert, with
    'null value in column "document_id" of relation
    "oc_openregister_table_15_18" violates not-null constraint'
    (12 MagicMapper errors in run 30797589151's Nextcloud log, while the
    import still answered HTTP 200);
  - PolicyController::createStandingConsent hits the same constraint at
    runtime, so the whole "Publish always" surface cannot persist anything.

The condition is per RECORD, not per schema, and the app's own canonical
spec already says so — openspec/specs/consent-management/spec.md, 'Requirement:
documentId MUST be required only for scope=document records'. It is enforced
where a per-record condition can be: ConsentScopeValidator::assertValid()
rejects a scope=document record WITHOUT a documentId and a scope=entity record
WITH one. Removing it from the schema's `required` array restores the
specified behaviour and leaves nothing unvalidated.

Config version 7.6.3 -> 7.6.4.
…paced code (#360)

Adds ignore-namespaces=true to the DevelopmentCodeFragment rule, without which
the rule is dead code: PDepend resolves an unqualified call inside a namespaced
file to the current-namespace-qualified image (OCA\MyApp\Service\var_dump), which
never matches the unwanted-functions list. All production PHP here is namespaced.

Also drops the "if [ -f vendor/bin/phpmd ]" guard from the composer phpmd script,
which made the check exit 0 whenever phpmd was absent.

Config-only. Verified behaviour-neutral before merge: the phpmd job reaches the
same conclusion on the merge-base and on this head, the phpmd finding sets are
identical, and DevelopmentCodeFragment reports zero findings on both sides. The
repo's pre-existing phpmd debt in other rules is left visible and untouched.

See ConductionNL/openregister#2286.
…issions

Point the wrapper at ConductionNL/.github (the org "Conduction" does
not exist), cancel superseded runs per ref, and declare least-privilege
permissions (CodeQL actions/missing-workflow-permissions).
…esk never shipped

src/manifest.json bound both type:index pages to register slug "docudesk":

    Templates       -> register "docudesk", schema "template"
    SigningRequests -> register "docudesk", schema "signingRequest"

lib/Settings/docudesk_register.json has never declared a register with that
slug. At a406583 — the Phase 8 commit that introduced these two index pages
and wrote "docudesk" into them — the definition already declared
consent / document / dossier / signing / templates, and the whole history of
the file contains no "docudesk" register at all.

So both pages have listed objects from a register that holds none of them
since the day they were written, while every write went somewhere else:
TemplatesController resolves `template_register` from app config (the
`templates` register) and SigningService resolves `signingRequest_register`
(the `signing` register). Symptom on a clean instance: "No items found" on
both pages with three seeded templates present in the database, and
templates-crud's create -> appears-in-the-UI-list leg failing.

Also corrects the two walkthrough `advanceOn.object-created` triggers, which
carried the same wrong slug and could therefore never fire.

Fixes the read path to match both the shipped register definition and the
write path. Found by the e2e suite (run 30801457803).

Also in this commit:
- versions.spec.ts branches on version ROWS instead of the table element. The
  table always renders its header row, so the populated branch was entered on
  an instance where fileId=1 does not resolve and asserted a Download button
  no row could offer; the empty branch now accepts either surfaced reason
  (files_versions disabled, or "Document not found") but still requires one.
- ci-seed.sh probes WebDAV (PROPFIND/MKCOL/PUT/DELETE as admin) and prints the
  codes. The workflow specs seed real files through DAV and MKCOL returned 404
  with nothing in nextcloud.log; this makes the environment's DAV state a
  printed fact rather than a fixture-shaped accusation. Informational only.
rubenvdlinde and others added 29 commits August 20, 2026 11:48
…260820093739

chore(release): 0.0.38-unstable.20260820093739
…260820095005

chore(release): 0.0.38-unstable.20260820095005
…260820100609

chore(release): 0.0.38-unstable.20260820100609
…260820102210

chore(release): 0.0.38-unstable.20260820102210
Two user-visible defects are fixed by this bump.

TWO AI-COMPANION HEXES ON EVERY PAGE. The companion singleton landed in
2.7.0. Below that the host app's own companion never stands down, so any
page of this app rendered a second hex 8px from hermiq's.

THE DETAIL PAGE RECLOSED ITS SIDEBAR WHILE HYDRATING. CnDetailPage set
sidebarSeeded and never read it, so 'open' was re-applied on every sync.
Fixed in nextcloud-vue#711.

Lockfile only — the existing caret already allowed this.
…260820103532

chore(release): 0.0.38-unstable.20260820103532
…260820105835

chore(release): 0.0.38-unstable.20260820105835
chore(deps): take @conduction/nextcloud-vue 2.8.2 (was 2.3.0)
…260820111321

chore(release): 0.0.38-unstable.20260820111321
…260820113535

chore(release): 0.0.38-unstable.20260820113535
…260820115321

chore(release): 0.0.38-unstable.20260820115321
…260820120316

chore(release): 0.0.38-unstable.20260820120316
…260820121250

chore(release): 0.0.38-unstable.20260820121250
…260820122212

chore(release): 0.0.38-unstable.20260820122212
…260820124138

chore(release): 0.0.38-unstable.20260820124138
…260820125103

chore(release): 0.0.38-unstable.20260820125103
@rubenvdlinde
rubenvdlinde merged commit 3eaf343 into beta Aug 20, 2026
35 of 36 checks passed
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.

4 participants