Skip to content

feat(manifest): Tier 2 pilot — route Decisions through CnPageRenderer#138

Draft
rubenvdlinde wants to merge 18 commits intodevelopmentfrom
feature/json-manifest-pilot
Draft

feat(manifest): Tier 2 pilot — route Decisions through CnPageRenderer#138
rubenvdlinde wants to merge 18 commits intodevelopmentfrom
feature/json-manifest-pilot

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Summary

Pilot of the JSON manifest renderer (@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.

Draft, because:

  1. It depends on ConductionNL/nextcloud-vue#89, not yet merged.
  2. Until that lands and @conduction/nextcloud-vue publishes a beta, dev requires the local-alias mode (USE_LOCAL_LIB=1).

What changed

File Change
src/manifest.json (new) Declares Decisions and DecisionDetail as type: "custom" pages mapped to registry component names. Carries dependencies: [\"openregister\"].
src/customComponents.js (new) Registry mapping DecisionsView / DecisionDetailView → the existing Decisions.vue / DecisionDetail.vue.
src/App.vue setup() calls useAppManifest('decidesk', bundledManifest); provide() exposes cnManifest, cnCustomComponents, cnTranslate so CnPageRenderer can inject them. Visible shell (NcContent → MainMenu → router-view, OpenRegister-installed gate, loading state) is untouched.
src/router/index.js Decisions / DecisionDetail routes now mount CnPageRenderer. Route names match pages[].id so the renderer matches by `$route.name === page.id`.
docs/manifest-pilot.md (new) Rationale, scope, dev setup, what the pilot does NOT cover.

Why type: \"custom\" for routes that look like index/detail pages

The renderer's built-in type: \"index\" / \"detail\" paths only forward page.config as props to the corresponding Cn*Page. Decidesk's existing views wire data via useListView / useDetailView and include slot overrides (CnSchemaFormDialog inside #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

  • useAppManifest loads, validates, and exposes a real-world manifest in a real app context.
  • CnPageRenderer correctly dispatches by \$route.name === page.id for both list and detail routes.
  • provide() from a non-CnAppRoot parent (just plain App.vue) is a viable Tier 2 wiring — apps can adopt the renderer incrementally without taking the full shell.
  • The customComponents registry pattern is a clean bailout for views with composable-backed data loading.

Out of scope (deliberately)

  • Any change to non-Decisions routes.
  • Replacing MainMenu with CnAppNav.
  • Replacing the OpenRegister-installed gate with CnDependencyMissing (the existing gate uses useSettingsStore which is more app-aware than the generic useAppStatus(appId) capability check).
  • Migrating decidesk fully to Tier 4 (CnAppRoot shell).

Test plan

  • Merge ConductionNL/nextcloud-vue#89, publish a beta with useAppManifest, CnPageRenderer, validateManifest
  • Bump package.json to that beta and remove the local-alias section of `docs/manifest-pilot.md`
  • Smoke test: navigate to /decisions and /decisions/:id, confirm same behaviour as before (list loads, create dialog works, row click navigates to detail)
  • Confirm no regressions on other routes
  • Optional: open follow-up issue to migrate one more page (Minutes? AgendaItems?) once Decisions soaks for a sprint

Related

  • ConductionNL/nextcloud-vue#89 — manifest renderer implementation
  • ConductionNL/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)

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 7cfb1e3

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 3ff4fc0

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ c9849bb

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 80ba452

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 248377c

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 0baf7de

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ dc056cf

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 2ce9ce3

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.

@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 62060c8

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.
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ fa50f41

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
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 22985da

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).
@github-actions
Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/decidesk @ 15de79d

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.

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