feat(manifest): Tier 2 pilot — route Decisions through CnPageRenderer#138
feat(manifest): Tier 2 pilot — route Decisions through CnPageRenderer#138rubenvdlinde wants to merge 18 commits intodevelopmentfrom
Conversation
Pilot of the manifest renderer landing in @conduction/nextcloud-vue PR #89. Routes the Decisions and DecisionDetail pages through CnPageRenderer instead of mounting their view components directly; every other route is unchanged so the blast radius stays minimal. Changes: - src/manifest.json: declares Decisions and DecisionDetail as type:"custom" pages mapped to registry component names. The manifest also carries `dependencies: ["openregister"]`, anticipating the eventual move to Tier 4 with CnDependencyMissing. - src/customComponents.js: registry mapping DecisionsView / DecisionDetailView to the existing Decisions.vue / DecisionDetail.vue components. - src/App.vue: setup() calls useAppManifest('decidesk', bundledManifest); provide() exposes cnManifest, cnCustomComponents, cnTranslate so CnPageRenderer can inject them. The visible shell (NcContent → MainMenu → router-view, the OpenRegister-installed gate, and the loading state) is untouched. - src/router/index.js: Decisions and DecisionDetail routes now mount CnPageRenderer; route names match the manifest's pages[].id so the renderer matches by $route.name === page.id. - docs/manifest-pilot.md: rationale, what changed, why type:"custom", how to run in dev with the local nextcloud-vue alias, and what the pilot does NOT cover. Why type:"custom" for routes that look like list/detail pages: the renderer's built-in type:"index"/"detail" paths only forward page.config as props to the corresponding Cn*Page; they don't run useListView / useDetailView. Decidesk's existing views need those composables AND a #create-dialog slot override, so wrapping them in the registry keeps full app-side control while routing through the manifest. A future renderer iteration can add a config shape that auto-wires the composable; until then, the registry is the right tool for views with composable-driven data loading. Dev setup until @conduction/nextcloud-vue ships a beta with PR #89: cd ../nextcloud-vue && git checkout feature/json-manifest-renderer cd ../decidesk && USE_LOCAL_LIB=1 npm run dev webpack.config.js already aliases @conduction/nextcloud-vue to ../nextcloud-vue/src when USE_LOCAL_LIB=1.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/115 statements)
Quality workflow — 2026-04-28 05:41 UTC
Download the full PDF report from the workflow artifacts.
Two compounding bugs that together meant every install thought it had
configured Decidesk while no register was ever created in OpenRegister:
1. lib/Service/SettingsService.php — loadConfiguration() called the
OpenRegister importer with only `appId` + `force`, but the importer
signature is importFromApp(appId, data, version, force). PHP named
args silently dropped `data` and `version`, so the importer ran
against an empty configuration and returned an empty result. Now the
service reads lib/Settings/decidesk_register.json, parses it, picks
up the version from `info.version`, and passes all four arguments.
2. lib/Settings/decidesk_register.json — only `components.schemas`
was declared; no `components.registers` block. The OpenRegister
importer creates registers from `data.components.registers[*]` —
without it, schemas were imported but no register entity bound them
together, so `/api/objects?register=decidesk&schema=decision`
returned 404 ("Register not found: 'decidesk'"). Added a `decidesk`
register entry referencing all 17 schema slugs (governance-body,
meeting, participant, agenda-item, motion, amendment, voting-round,
vote, decision, action-item, minutes, digital-document,
monetary-amount, offer, order, product, report) following the same
shape used by procest / opencatalogi / pipelinq.
Bumped info.version from 0.1.0 to 0.2.0 so existing installs that
already imported v0.1.0 (schemas-only) pick up the new register on
the next `occ maintenance:repair`.
Verified locally: `occ maintenance:repair --include-expensive` now
reports "Decidesk configuration imported successfully (version: 0.2.0)"
and `/apps/openregister/api/registers?_search=decidesk` returns the
new register with all 17 schemas attached.
Aligns decidesk with the @conduction/nextcloud-vue stack so the new
manifest renderer's useListView API path actually works against
decidesk's existing views.
ADR-022 ("apps consume OpenRegister abstractions over local
duplication"):
- src/store/store.js: imports useObjectStore from
@conduction/nextcloud-vue (the canonical Pinia store) instead of
the local fork. The configure() + registerObjectType() calls in
initializeStores() use the same shape so no other call sites needed
changes there.
- src/store/modules/object.js: deleted. It was a 90-line fork of
the canonical store missing fetchSchema / fetchCollection /
pagination / facets / errors — exactly what useListView's new
one-arg API expects.
- 7 view/component files: rename the canonical .fetchObjects(...)
callsite to .fetchCollection(...) — the canonical store uses the
plural-collection name; both return the array so the rest of the
call site is unchanged.
Manifest renderer bootstrap (per @conduction/nextcloud-vue's
CLAUDE.md required-bootstrap section):
- src/main.js: adds registerIcons({}) + registerTranslations() before
Vue mount so library-rendered strings translate to the user's NC
locale and CnIcon resolves icon names. Wraps loadTranslations() in
.catch().then(mount) so a missing per-locale l10n/<lang>.json
doesn't block app boot — pre-existing behaviour silently dropped
the mount callback on a 404 in the locale file.
Dev-environment unblockers (USE_LOCAL_LIB=1):
- webpack.config.js: adds a vue-loader-compatible scss rule so
CnCard.vue's `<style lang="scss">` compiles in dev (sass-loader
was already in node_modules but not registered). Also sets
output.publicPath = 'auto' so dynamically-loaded chunks resolve
via document.currentScript.src instead of a hardcoded
/apps/<appId>/js/ that doesn't match the bind-mounted custom_apps
path.
Verified end-to-end via Playwright: navigating to /decisions now
mounts CnPageRenderer → Decisions.vue → CnIndexPage with the empty
state, "Add Decision" → CnSchemaFormDialog opens with all 17 fields
auto-rendered from the schema. No JS console errors.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 08:22 UTC
Download the full PDF report from the workflow artifacts.
Three pilot-soak findings, all making decidesk match procest / pipelinq visually rather than diverging. Dashboard (`src/views/Dashboard.vue`): - Replaces the hand-rolled `<div class="decidesk-dashboard">` shell with `<CnDashboardPage>` so the page picks up the same outer padding / margins used by every other Conduction app dashboard. Earlier the custom CSS (`padding: 8px 4px 24px`) drifted from the library default and rendered noticeably tighter on the left. - Wires the `#header-actions` slot with the standard quick-action set (New Decision / New Action Item / New Minutes / Refresh) — the slot was simply absent in the hand-rolled markup, so the top-right action area was empty. - Reuses the existing KPI counts via custom widgets in the CnDashboardPage grid layout instead of an inline `CnKpiGrid`. - Refresh button is wired to a `loadCounts()` method so refreshing the KPIs doesn't require a full page reload. Main menu (`src/navigation/MainMenu.vue`): - Dashboard nav icon: `Home` → `ViewDashboard` (the matrix-of-dots glyph). This is the icon every other Conduction app uses for its Dashboard entry and matches Nextcloud core's own dashboard. - Settings entry now lives inside `<NcAppNavigationSettings>` (the canonical footer slot) instead of a bare `NcAppNavigationItem`. Visually identical for the user but matches the NC convention so future settings entries can stack inside the same group without another wrapper change. Verified end-to-end via Playwright at /apps/decidesk/: the four header action buttons render in the top right, the title block has the same left margin as procest's dashboard, and the Settings entry occupies the bottom-left footer with the cog icon.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 09:26 UTC
Download the full PDF report from the workflow artifacts.
Two divergences from the canonical pattern that caused decidesk's dashboard widgets to look subtly off: 1. Empty widget titles. WIDGET_DEFS had `title: ''` for every entry; procest / pipelinq pass real translated strings. Even when `showTitle: false` hides the header bar, the title is read by CnWidgetWrapper for its aria attributes and content-area class selectors. Now resolved through a computed widgetDefs() so titles re-translate when the locale changes. 2. Double card chrome. Bottom-row "Notulen" / "Besluiten" widgets wrapped a `CnConfigurationCard` inside a (borderless) custom widget slot, so the visible card was the inner CnConfigurationCard while CnWidgetWrapper sat empty around it. Procest puts content directly in the slot and lets CnWidgetWrapper provide the card chrome via `showTitle: true` (the default). Now the bottom row drops `showTitle: false` from its layout entries — CnWidgetWrapper renders a single card with a proper title bar — and the slot contents are plain content (the link), not another card. KPI row continues to render with `showTitle: false` (borderless CnWidgetWrapper, just the CnStatsBlock visible) — same as procest's KPI row.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 09:57 UTC
Download the full PDF report from the workflow artifacts.
Migrates decidesk from Tier 2 (just /decisions through CnPageRenderer)
to Tier 4 of the manifest-renderer adoption ladder: the app shell,
side navigation, and every route are now driven by `src/manifest.json`.
Manifest expansion (`src/manifest.json` v0.2.0):
- `menu[]` grew from empty to 6 entries (Dashboard, Minutes, Decisions,
Action items, Motions, Meetings) — CnAppNav reads this and renders
the side panel.
- `pages[]` grew from 2 to 20, covering every route the old hand-rolled
router carried (Dashboard, GovernanceBodies + detail, Meetings +
detail, LiveMeeting, Participants + detail, AgendaItems + detail,
Motions + detail, AmendmentDetail, Minutes + detail, Decisions +
detail, ActionItems + detail, Settings).
- Every page is `type: "custom"` for now — they wrap existing app
views (which all use the useListView / useDetailView composables and
ship slot-overridden dialogs). Once nextcloud-vue#90 lands the
auto-wire for `type: "index"` / `type: "detail"`, most of these
collapse to declarative entries with no registry component.
`src/customComponents.js`:
- 20 entries mapping registry names to view imports.
- Each wrapped in `defineAsyncComponent` so views still chunk-split
into separate bundles (preserves the lazy-load behaviour the old
`() => import(...)` router config gave).
`src/router/index.js`:
- Routes are now generated from `manifest.pages.map(...)`. Every entry
becomes `{ name: page.id, path: page.route, component: CnPageRenderer,
props: page.route.includes(':') }`. A new route is one manifest line.
- Catch-all `path: '*'` redirect to `/` preserved.
`src/App.vue`:
- Replaces the hand-rolled NcContent + MainMenu + NcAppContent + custom
OpenRegister-installed gate with a single `<CnAppRoot>` element.
- `useAppManifest('decidesk', bundledManifest)` provides the manifest
+ isLoading state.
- `customComponents` and `translate` (closure over @nextcloud/l10n's
`translate('decidesk', key)`) are passed in.
- `manifest.dependencies: ["openregister"]` drives CnAppRoot's
dependency-check phase, replacing the old `useSettingsStore.hasOpenRegisters`
gate.
`src/main.js`:
- `initializeStores()` moved out of App.vue's `created()` hook into
the bootstrap chain (after `loadTranslations`, before mount). Stores
are ready by the time CnAppRoot renders.
`src/navigation/MainMenu.vue`:
- Deleted. CnAppNav (driven by manifest.menu[]) replaces it. Apps that
need a hand-rolled menu can use CnAppRoot's `#menu` slot to override.
Verified end-to-end via Playwright after the matching nextcloud-vue
fixes in feature/json-manifest-renderer:
- Side nav: 6 manifest items render with the right icons
- Dashboard: header actions + KPIs + Notulen/Besluiten widgets render
- Decisions list (manifest-driven): CnIndexPage renders the empty state
- Minutes list: same, navigates correctly
- Layout: content area fills the full width once CnAppRoot wraps the
router-view in NcAppContent
- No more "Required apps are missing" false positive (useAppStatus now
checks OC.appswebroots, not capabilities)
Out of scope for this commit (filed as follow-ups):
- Documentation external link in the menu — the manifest schema has
no `href` field for non-route items. Drop for now.
- Settings entry in NcAppNavigationSettings (the footer group) — the
schema has no concept of menu sections. Drop for now.
- Auto-wire for type:"index" / type:"detail" pages — see
ConductionNL/nextcloud-vue#90.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 10:41 UTC
Download the full PDF report from the workflow artifacts.
Both placed in section "settings" so CnAppNav renders them inside the NcAppNavigationSettings collapsible footer group: - Documentation: external href, opens conduction.nl in a new tab - SettingsMenu: routes to the existing Settings page Bumps manifest version to 0.3.0.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 10:57 UTC
Download the full PDF report from the workflow artifacts.
- Documentation drops the section: "settings" so it renders as the last item of the main list (always visible) instead of being hidden behind the NcAppNavigationSettings popover. Settings stays in the settings section. - icon-dashboard is not a built-in Nextcloud icon class, so the entry rendered without an icon. Switch to icon-category-dashboard. - Bump app version to 0.1.2 for cache busting after the JS change.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 11:52 UTC
Download the full PDF report from the workflow artifacts.
Documentation belongs visually next to Settings at the bottom of the navigation, not floating in the middle of the main list. CnAppNav now renders section:settings items directly in the footer slot, so moving Documentation to that section anchors it to the bottom right above Settings — same UX as procest's MainMenu without the popover. Bumps app version to 0.1.3 for cache busting.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 12:07 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 12:37 UTC
Download the full PDF report from the workflow artifacts.
Decidesk shipped four bespoke per-schema Pinia stores
(meetings/minutes/decisions/actionItems) that hand-rolled CRUD
against /apps/openregister/api/objects (or in meetings' case,
/apps/decidesk/api/meetings). The canonical useObjectStore from
@conduction/nextcloud-vue already covers every method they
implemented (fetchCollection, fetchObject, saveObject, deleteObject,
deleteObjects, pagination, search, errors, caching).
A code audit shows the duplication was almost entirely dead:
- minutes.js / actionItems.js: 0 callers anywhere
- meetings.js: 1 view used 3 filter setters; the 5 CRUD actions and
the lifecycle action were never imported
- decisions.js: 1 view used `publishDecision`; the CRUD actions were
never imported
Changes:
- Delete the four dead store modules (-840 lines).
- Drop dead exports from store/store.js.
- In initializeStores(), register every object type the views
actually reference (9 total: minutes, decision, action-item,
meeting, agenda-item, motion, amendment, governance-body,
participant). The previous list of 3 left useListView calls for
the other 6 throwing "type not registered" silently — surfaced by
the Playwright smoke test against /apps/decidesk/meetings.
- Add `setActivePinia(pinia)` in pinia.js. main.js calls
initializeStores() *before* app.$mount(), at which point Pinia is
not yet bound to the Vue app. Without setActivePinia, useStore()
returned a detached store missing its `_s` plugin chain and every
registerObjectType() call was a silent no-op (caught by the
catch() in main.js's bootstrap chain). Surfaced by an empty
objectTypeRegistry at runtime even though the source said
otherwise.
- Meetings.vue: drop meetingStore import + dead .setSearchQuery /
.setFilters / .resetFilters / .page = 1 calls. Local searchQuery
and selectedLifecycle data() props remain to bind to the toolbar
inputs.
- DecisionDetail.vue: inline the single POST to
/apps/decidesk/api/decisions/{id}/publish (formerly
decisionStore.publishDecision). Server still enforces admin /
outcome / isPublished guards.
- Bump app version to 0.1.7.
Smoke test (browser-6): /apps/decidesk/{,minutes,decisions,
action-items,motions,agenda-items,participants,governance-bodies,
meetings,settings} — all 0 console errors, 9/9 types registered.
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-28 15:13 UTC
Download the full PDF report from the workflow artifacts.
The MeetingController CRUD endpoints (index/create/show/update/destroy) were thin pass-throughs to OpenRegister's ObjectService — every MeetingService method just called createFromArray/find/updateFromArray/ deleteFromId with register='decidesk', schema='meeting' and a log line. The frontend never used them: views go through useObjectStore → /apps/openregister/api/objects?register=decidesk&schema=meeting directly, which is the canonical path per ADR-022. Deleted: - MeetingController::index, create, show, update, destroy (kept lifecycle — that one wraps real workflow logic in MeetingService::transition) - MeetingService::create, read, update, delete (pass-throughs) - The 5 matching routes in appinfo/routes.php - The container DI on MeetingController + the matching test mock Net: -260 PHP lines + -5 routes; the CRUD path is unchanged from the UI's point of view since it was already hitting openregister. Pre-existing fixes encountered: - SettingsService.php:196 PHPCS concat-with-spaces - Application.php AnalyticsController DI was missing userSession + groupManager (Psalm TooFewArguments — service registration drifted from the constructor signature). Verification: - composer phpcs / psalm: clean - phpunit Meeting* tests: 17 pass, 4 skipped - Playwright /apps/decidesk/meetings: 0 console errors - Direct hits on the deleted endpoints return 405 (POST/PUT/DELETE); GET falls through to the SPA catch-all
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/115 statements)
Quality workflow — 2026-04-28 16:03 UTC
Download the full PDF report from the workflow artifacts.
…on stack
Pilot of OpenRegister's declarative lifecycle annotation against the Meeting schema.
Backend:
- Annotate Meeting schema with x-openregister-lifecycle (field, initial, final, transitions)
under configuration.x-openregister-lifecycle so OR's existing schema persistence carries it.
- Add MeetingTransitionGuard implementing OCA\OpenRegister\Lifecycle\LifecycleGuardInterface;
delegates to existing WorkflowService (domain rules + chair gates) and QuorumService
(open-meeting quorum check). Registered under DI tag `decidesk.meeting.transitionGuard`
matching the schema's `requires` field.
- Remove MeetingService, MeetingController, /api/meetings/{id}/lifecycle route, and
associated unit tests — replaced wholesale by OR's POST /api/objects/{id}/transition
+ GET /api/objects/{id}/available-actions.
Frontend:
- MeetingLifecycle.vue: drop hardcoded transition map mirroring the backend (the warning
comment said it would silently diverge — now removed at the source). Load actions from
GET /apps/openregister/api/objects/{id}/available-actions; apply via POST
/apps/openregister/api/objects/{id}/transition. Labels and badge colours stay client-side.
Net deletion: ~533 lines of meeting-specific transition machinery + tests, replaced with
~125 lines of guard + schema annotation. Lifecycle shape is now single-source-of-truth in
the schema; the frontend cannot drift.
Live-pilot follow-ups: - Bump register version to 0.3.2 + Meeting schema version to 0.2.2 so the next maintenance:repair re-imports the lifecycle annotation. - Switch the `requires` field from the local DI tag string `decidesk.meeting.transitionGuard` to the guard's FQCN `OCA\Decidesk\Lifecycle\MeetingTransitionGuard`. OpenRegister resolves guards via the server container so it can autowire across apps; FQCNs are the only stable cross-app identifier.
The guard is referenced by FQCN in the schema's `requires` field; OpenRegister's LifecycleGuardRegistry resolves FQCNs via the server container, which autowires constructor dependencies. The local registerService block was redundant.
Declares totalCompleted, byStatus, completedThisMonth, totalOpen.
Verified end-to-end against OpenRegister's
GET /api/objects/aggregations/{register}/{schema}/{name}.
ActionItemAnalyticsService is left in place for now: avgDaysToClose
needs computed fields, getCompletionRates needs cross-schema rollup,
getMyItems needs user-scoped buckets. Migration to fully declarative
analytics ships after the calculations-annotation change lands.
- daysLate (materialise:true): diffDays(completedAt, dueDate) - isOverdue (materialise:true): and(ne(taskStatus, "completed"), lt(dueDate, $now)) - New aggregations leaning on the materialised fields: - totalOverdue: count where isOverdue=true - avgDaysLate: avg(daysLate) where taskStatus=completed Verified end-to-end via OR's CalculationOnSaveListener + AggregationRunner.
Two transition-triggered notifications: meetingOpened (action=open) and
meetingClosed (action=close), both delivered to admin via the Nextcloud
INotificationManager. Subject template uses {{title}} interpolation.
Verified end-to-end via OR's AnnotationNotificationDispatcher +
AnnotationNotifier. Trigger action filter correctly rejects unrelated
transitions (e.g. schedule fires no notification).
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ❌ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 416/416 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-04-29 08:54 UTC
Download the full PDF report from the workflow artifacts.
Summary
Pilot of the JSON manifest renderer (
@conduction/nextcloud-vuePR #89). Routes the Decisions and DecisionDetail pages throughCnPageRendererinstead of mounting their view components directly. Every other route is unchanged.Draft, because:
ConductionNL/nextcloud-vue#89, not yet merged.@conduction/nextcloud-vuepublishes a beta, dev requires the local-alias mode (USE_LOCAL_LIB=1).What changed
src/manifest.json(new)DecisionsandDecisionDetailastype: "custom"pages mapped to registry component names. Carriesdependencies: [\"openregister\"].src/customComponents.js(new)DecisionsView/DecisionDetailView→ the existingDecisions.vue/DecisionDetail.vue.src/App.vuesetup()callsuseAppManifest('decidesk', bundledManifest);provide()exposescnManifest,cnCustomComponents,cnTranslatesoCnPageRenderercaninjectthem. Visible shell (NcContent → MainMenu → router-view, OpenRegister-installed gate, loading state) is untouched.src/router/index.jsCnPageRenderer. Route names matchpages[].idso the renderer matches by `$route.name === page.id`.docs/manifest-pilot.md(new)Why
type: \"custom\"for routes that look like index/detail pagesThe renderer's built-in
type: \"index\"/\"detail\"paths only forwardpage.configas props to the correspondingCn*Page. Decidesk's existing views wire data viauseListView/useDetailViewand include slot overrides (CnSchemaFormDialoginside#create-dialog). Wrapping them in the registry keeps full app-side control while still routing through the manifest.This also surfaces a real gap in the renderer's built-in mappings:
type: \"index\"doesn't auto-wire the data-loading composables that real Conduction list views need. A future iteration of the renderer (separate change) can grow a config shape that auto-runs the composable — until then, the registry is the right tool for views with composable-backed data loading.What this pilot proves
useAppManifestloads, validates, and exposes a real-world manifest in a real app context.CnPageRenderercorrectly dispatches by\$route.name === page.idfor both list and detail routes.provide()from a non-CnAppRootparent (just plainApp.vue) is a viable Tier 2 wiring — apps can adopt the renderer incrementally without taking the full shell.customComponentsregistry pattern is a clean bailout for views with composable-backed data loading.Out of scope (deliberately)
MainMenuwithCnAppNav.CnDependencyMissing(the existing gate usesuseSettingsStorewhich is more app-aware than the genericuseAppStatus(appId)capability check).CnAppRootshell).Test plan
ConductionNL/nextcloud-vue#89, publish a beta withuseAppManifest,CnPageRenderer,validateManifestpackage.jsonto that beta and remove the local-alias section of `docs/manifest-pilot.md`Related
ConductionNL/nextcloud-vue#89— manifest renderer implementationConductionNL/hydra#194— mechanical i18n key check (consumes manifest static fields)ConductionNL/hydra#195— mechanical manifest validity check (consumes the JSON Schema this PR's manifest validates against)