Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Add 25 OpenSpec proposals for multi-tenant dashboard platform#42

Merged
rubenvdlinde merged 2 commits into
developmentfrom
feature/replica-spec-proposals
Apr 30, 2026
Merged

Add 25 OpenSpec proposals for multi-tenant dashboard platform#42
rubenvdlinde merged 2 commits into
developmentfrom
feature/replica-spec-proposals

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Summary

Adds 25 OpenSpec change proposals at the proposal + refinement + validation stage modelling a complete dashboard sharing and runtime experience for MyDash. Specs are organised in 4 dependency layers; all 25 pass `openspec validate --strict`.

Includes the baseline `dashboard-sharing` capability spec under `openspec/specs/dashboard-sharing/` (referenced by `dashboard-sharing-followups`).

Capabilities touched

Capability Changes
`dashboards` multi-scope-dashboards, default-dashboard-flag, active-dashboard-resolution, fork-current-as-personal
`admin-settings` group-priority-order, allow-personal-dashboards-flag
`admin-templates` group-routing
`grid-layout` responsive-grid-breakpoints, widget-collision-placement
`widgets` widget-add-edit-modal, widget-context-menu, nc-dashboard-widget-proxy
`legacy-widget-bridge` nc-dashboard-widget-proxy (pollForCallback helper)
`dashboard-sharing` dashboard-sharing-followups
NEW `dashboard-icons` dashboard-icons, custom-icon-upload-pattern
NEW `dashboard-switcher` dashboard-switcher-sidebar
NEW `runtime-shell` runtime-shell
NEW `text-display-widget` text-display-widget
NEW `label-widget` label-widget
NEW `image-widget` image-widget
NEW `link-button-widget` link-button-widget
NEW `resource-uploads` resource-uploads, resource-serving, svg-sanitisation
NEW `initial-state-contract` initial-state-contract

Status

  • ✅ All 25 changes validate (`openspec validate --strict`)
  • ⏳ Implementation (`opsx-apply`) is the next step per change

Recommended apply order (dependency layers)

  1. Foundations (parallel): multi-scope-dashboards, group-priority-order, allow-personal-dashboards-flag, dashboard-icons, resource-uploads, responsive-grid-breakpoints, widget-collision-placement, initial-state-contract, text-display-widget, label-widget
  2. Foundation extensions: default-dashboard-flag, group-routing, custom-icon-upload-pattern, image-widget, nc-dashboard-widget-proxy, resource-serving, svg-sanitisation, link-button-widget, dashboard-sharing-followups
  3. UI surfaces: active-dashboard-resolution, fork-current-as-personal, dashboard-switcher-sidebar, widget-add-edit-modal
  4. Sequential last: widget-context-menu, runtime-shell

Note on extra commits

This branch was cut from `chore/phpstan-phpunit-screenshots` so the PR diff also shows those 2 unrelated commits — they're separate WIP and will land via their own PR.

Test plan

  • `openspec validate --strict` passes for all 25 changes (verified locally before commit)
  • Reviewers spot-check 1–2 spec deltas per capability for clarity / correctness
  • Confirm baseline `openspec/specs/dashboard-sharing/spec.md` is the intended REQ-SHARE-001..007 baseline

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ a2df2d2

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 10:59 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde rubenvdlinde added ready-for-code-review Build complete — awaiting code reviewer ready-for-security-review Code review complete — awaiting security reviewer labels Apr 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 7799081

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 11:16 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
…RES-001..005

Adds the resource-uploads capability: a small mini file API for
binary branding assets (icons, widget images) that MyDash widgets
need to host directly, separate from the user's Files folder.

- POST /api/resources accepts {base64: 'data:image/<type>;base64,...'}
- Admin-only via IGroupManager::isAdmin (HTTP 403 otherwise)
- Allowed types: jpeg, jpg, png, gif, svg, webp (case-insensitive)
- 5 MB hard cap on decoded bytes, enforced BEFORE invoking the
  image library so oversize blobs cannot be loaded
- Raster MIME cross-check via getimagesizefromstring (SVG validation
  is delegated to the sibling svg-sanitisation capability)
- Storage via IAppData::getFolder('resources') with high-entropy
  resource_<uniqid>.<ext> filenames (folder auto-created on first use)
- Standardised response envelope: success {status, url, name, size};
  error {status, error, message} with stable error enum
- Typed exceptions with stable error codes; raw exception messages
  never leak to clients

Resource serving (GET) and SVG sanitisation are intentionally out
of scope here — they are delivered by sibling changes resource-serving
and svg-sanitisation in their own PRs.

Spec proposal: PR #42 (feature/replica-spec-proposals)
Requirements covered: REQ-RES-001, 002, 003, 004, 005
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
….005

Centralises every Nextcloud IInitialState::provideInitialState call behind a
typed PHP builder (InitialStateBuilder + Page enum + MissingInitialStateException)
and mirrors it on the JS side with a typed loadInitialState(page) reader that
fills defaults and warns on schema-version mismatch.

- lib/Service/InitialStateBuilder.php: typed setters per page key, REQUIRED_KEYS
  map keyed by Page::WORKSPACE / Page::ADMIN, apply() validates and pushes every
  key plus _schemaVersion (INITIAL_STATE_SCHEMA_VERSION = 1).
- lib/Exception/MissingInitialStateException.php: thrown by apply() with page
  + missing-key context.
- lib/Controller/PageController.php and lib/Settings/MyDashAdmin.php: refactored
  to construct the builder and call apply() — the only sanctioned call sites.
- src/utils/loadInitialState.js: per-page key/default tables mirroring the spec
  Data Model exactly; emits a console.warn on _schemaVersion mismatch.
- src/main.js and src/admin.js: load via the reader and provide() each key by
  identical name (no renaming at the boundary, no ref/reactive wrapping).
- tests/Unit/Service/InitialStateBuilderTest.php: per-page apply pushes every
  key + schema version, missing required key throws, schema-version always
  pushed (5 tests / 28 assertions).
- tests/Unit/Service/InitialStateContractLintTest.php: PHP grep that fails when
  any file under lib/ outside InitialStateBuilder.php calls
  provideInitialState() directly.
- tests/bootstrap.php: register vendor/nextcloud/ocp PSR-4 autoload as fallback
  when running outside the Nextcloud container so OCP-interface mocks resolve.

PR #42 owns the spec text on origin/feature/replica-spec-proposals.

The matching JS-side Vitest scan (loadState\(['"]mydash) is deferred pending
PR #43 (chore/add-vitest-setup); a TODO comment in
InitialStateContractLintTest references the follow-up.
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
…SET-012..014

Adds the foundational global admin setting `group_order` (JSON `string[]`
in `oc_mydash_admin_settings`) plus two admin-only HTTP endpoints needed by
the upcoming `group-routing` change (REQ-TMPL-012):

- AdminSetting::KEY_GROUP_ORDER constant.
- AdminSettingsService::getGroupOrder() — defensive read; missing row,
  null, empty string, corrupt JSON, or non-array decode all resolve to []
  without throwing (REQ-ASET-012 corrupt-JSON scenario).
- AdminSettingsService::setGroupOrder(array) — validates non-empty
  strings, deduplicates first-wins preserving order, persists as JSON.
- AdminController::listGroups() — GET /api/admin/groups, returns
  {active, inactive, allKnown}; inactive sorted by displayName, stale IDs
  preserved in active so the admin can remove them (REQ-ASET-013).
- AdminController::updateGroupOrder() — POST /api/admin/groups, replaces
  wholesale, 400 on validation failure (REQ-ASET-014).
- Both endpoints admin-gated via IGroupManager::isAdmin (403 on
  non-admin, 401 on unauthenticated). The inactive list reveals every
  group on the system, so GET is admin-only too.

Routes registered in appinfo/routes.php. Unit tests cover corrupt-JSON
fallback, dedup order preservation, replace-wholesale semantics,
admin-guard 403/401, and the disjoint+exhaustive listGroups contract
including stale-ID surfacing.

Pre-existing quality issues fixed in passing:
- 4 NamedParametersSniff false-positives on Entity __call setters
  (AdminSetting, Dashboard, ConditionalRule, WidgetPlacement) — guarded
  with phpcs:ignore + a comment explaining why named args break Entity
  __call.
- DashboardService MissingImport — added `use WidgetPlacement` and
  removed the FQCN at the call site.

Spec: #42 (feature/replica-spec-proposals).
Frontend drag-drop UI is out of scope for this PR; ships as a follow-up.
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
…RES-001..005

Adds the resource-uploads capability: a small mini file API for
binary branding assets (icons, widget images) that MyDash widgets
need to host directly, separate from the user's Files folder.

- POST /api/resources accepts {base64: 'data:image/<type>;base64,...'}
- Admin-only via IGroupManager::isAdmin (HTTP 403 otherwise)
- Allowed types: jpeg, jpg, png, gif, svg, webp (case-insensitive)
- 5 MB hard cap on decoded bytes, enforced BEFORE invoking the
  image library so oversize blobs cannot be loaded
- Raster MIME cross-check via getimagesizefromstring (SVG validation
  is delegated to the sibling svg-sanitisation capability)
- Storage via IAppData::getFolder('resources') with high-entropy
  resource_<uniqid>.<ext> filenames (folder auto-created on first use)
- Standardised response envelope: success {status, url, name, size};
  error {status, error, message} with stable error enum
- Typed exceptions with stable error codes; raw exception messages
  never leak to clients

Resource serving (GET) and SVG sanitisation are intentionally out
of scope here — they are delivered by sibling changes resource-serving
and svg-sanitisation in their own PRs.

Spec proposal: PR #42 (feature/replica-spec-proposals)
Requirements covered: REQ-RES-001, 002, 003, 004, 005
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
….005

Centralises every Nextcloud IInitialState::provideInitialState call behind a
typed PHP builder (InitialStateBuilder + Page enum + MissingInitialStateException)
and mirrors it on the JS side with a typed loadInitialState(page) reader that
fills defaults and warns on schema-version mismatch.

- lib/Service/InitialStateBuilder.php: typed setters per page key, REQUIRED_KEYS
  map keyed by Page::WORKSPACE / Page::ADMIN, apply() validates and pushes every
  key plus _schemaVersion (INITIAL_STATE_SCHEMA_VERSION = 1).
- lib/Exception/MissingInitialStateException.php: thrown by apply() with page
  + missing-key context.
- lib/Controller/PageController.php and lib/Settings/MyDashAdmin.php: refactored
  to construct the builder and call apply() — the only sanctioned call sites.
- src/utils/loadInitialState.js: per-page key/default tables mirroring the spec
  Data Model exactly; emits a console.warn on _schemaVersion mismatch.
- src/main.js and src/admin.js: load via the reader and provide() each key by
  identical name (no renaming at the boundary, no ref/reactive wrapping).
- tests/Unit/Service/InitialStateBuilderTest.php: per-page apply pushes every
  key + schema version, missing required key throws, schema-version always
  pushed (5 tests / 28 assertions).
- tests/Unit/Service/InitialStateContractLintTest.php: PHP grep that fails when
  any file under lib/ outside InitialStateBuilder.php calls
  provideInitialState() directly.
- tests/bootstrap.php: register vendor/nextcloud/ocp PSR-4 autoload as fallback
  when running outside the Nextcloud container so OCP-interface mocks resolve.

PR #42 owns the spec text on origin/feature/replica-spec-proposals.

The matching JS-side Vitest scan (loadState\(['"]mydash) is deferred pending
PR #43 (chore/add-vitest-setup); a TODO comment in
InitialStateContractLintTest references the follow-up.
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
…SET-012..014

Adds the foundational global admin setting `group_order` (JSON `string[]`
in `oc_mydash_admin_settings`) plus two admin-only HTTP endpoints needed by
the upcoming `group-routing` change (REQ-TMPL-012):

- AdminSetting::KEY_GROUP_ORDER constant.
- AdminSettingsService::getGroupOrder() — defensive read; missing row,
  null, empty string, corrupt JSON, or non-array decode all resolve to []
  without throwing (REQ-ASET-012 corrupt-JSON scenario).
- AdminSettingsService::setGroupOrder(array) — validates non-empty
  strings, deduplicates first-wins preserving order, persists as JSON.
- AdminController::listGroups() — GET /api/admin/groups, returns
  {active, inactive, allKnown}; inactive sorted by displayName, stale IDs
  preserved in active so the admin can remove them (REQ-ASET-013).
- AdminController::updateGroupOrder() — POST /api/admin/groups, replaces
  wholesale, 400 on validation failure (REQ-ASET-014).
- Both endpoints admin-gated via IGroupManager::isAdmin (403 on
  non-admin, 401 on unauthenticated). The inactive list reveals every
  group on the system, so GET is admin-only too.

Routes registered in appinfo/routes.php. Unit tests cover corrupt-JSON
fallback, dedup order preservation, replace-wholesale semantics,
admin-guard 403/401, and the disjoint+exhaustive listGroups contract
including stale-ID surfacing.

Pre-existing quality issues fixed in passing:
- 4 NamedParametersSniff false-positives on Entity __call setters
  (AdminSetting, Dashboard, ConditionalRule, WidgetPlacement) — guarded
  with phpcs:ignore + a comment explaining why named args break Entity
  __call.
- DashboardService MissingImport — added `use WidgetPlacement` and
  removed the FQCN at the call site.

Spec: #42 (feature/replica-spec-proposals).
Frontend drag-drop UI is out of scope for this PR; ships as a follow-up.
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
….005 (#45)

Centralises every Nextcloud IInitialState::provideInitialState call behind a
typed PHP builder (InitialStateBuilder + Page enum + MissingInitialStateException)
and mirrors it on the JS side with a typed loadInitialState(page) reader that
fills defaults and warns on schema-version mismatch.

- lib/Service/InitialStateBuilder.php: typed setters per page key, REQUIRED_KEYS
  map keyed by Page::WORKSPACE / Page::ADMIN, apply() validates and pushes every
  key plus _schemaVersion (INITIAL_STATE_SCHEMA_VERSION = 1).
- lib/Exception/MissingInitialStateException.php: thrown by apply() with page
  + missing-key context.
- lib/Controller/PageController.php and lib/Settings/MyDashAdmin.php: refactored
  to construct the builder and call apply() — the only sanctioned call sites.
- src/utils/loadInitialState.js: per-page key/default tables mirroring the spec
  Data Model exactly; emits a console.warn on _schemaVersion mismatch.
- src/main.js and src/admin.js: load via the reader and provide() each key by
  identical name (no renaming at the boundary, no ref/reactive wrapping).
- tests/Unit/Service/InitialStateBuilderTest.php: per-page apply pushes every
  key + schema version, missing required key throws, schema-version always
  pushed (5 tests / 28 assertions).
- tests/Unit/Service/InitialStateContractLintTest.php: PHP grep that fails when
  any file under lib/ outside InitialStateBuilder.php calls
  provideInitialState() directly.
- tests/bootstrap.php: register vendor/nextcloud/ocp PSR-4 autoload as fallback
  when running outside the Nextcloud container so OCP-interface mocks resolve.

PR #42 owns the spec text on origin/feature/replica-spec-proposals.

The matching JS-side Vitest scan (loadState\(['"]mydash) is deferred pending
PR #43 (chore/add-vitest-setup); a TODO comment in
InitialStateContractLintTest references the follow-up.
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
…SET-012..014 (#44)

Adds the foundational global admin setting `group_order` (JSON `string[]`
in `oc_mydash_admin_settings`) plus two admin-only HTTP endpoints needed by
the upcoming `group-routing` change (REQ-TMPL-012):

- AdminSetting::KEY_GROUP_ORDER constant.
- AdminSettingsService::getGroupOrder() — defensive read; missing row,
  null, empty string, corrupt JSON, or non-array decode all resolve to []
  without throwing (REQ-ASET-012 corrupt-JSON scenario).
- AdminSettingsService::setGroupOrder(array) — validates non-empty
  strings, deduplicates first-wins preserving order, persists as JSON.
- AdminController::listGroups() — GET /api/admin/groups, returns
  {active, inactive, allKnown}; inactive sorted by displayName, stale IDs
  preserved in active so the admin can remove them (REQ-ASET-013).
- AdminController::updateGroupOrder() — POST /api/admin/groups, replaces
  wholesale, 400 on validation failure (REQ-ASET-014).
- Both endpoints admin-gated via IGroupManager::isAdmin (403 on
  non-admin, 401 on unauthenticated). The inactive list reveals every
  group on the system, so GET is admin-only too.

Routes registered in appinfo/routes.php. Unit tests cover corrupt-JSON
fallback, dedup order preservation, replace-wholesale semantics,
admin-guard 403/401, and the disjoint+exhaustive listGroups contract
including stale-ID surfacing.

Pre-existing quality issues fixed in passing:
- 4 NamedParametersSniff false-positives on Entity __call setters
  (AdminSetting, Dashboard, ConditionalRule, WidgetPlacement) — guarded
  with phpcs:ignore + a comment explaining why named args break Entity
  __call.
- DashboardService MissingImport — added `use WidgetPlacement` and
  removed the FQCN at the call site.

Spec: #42 (feature/replica-spec-proposals).
Frontend drag-drop UI is out of scope for this PR; ships as a follow-up.
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
…RES-001..005

Adds the resource-uploads capability: a small mini file API for
binary branding assets (icons, widget images) that MyDash widgets
need to host directly, separate from the user's Files folder.

- POST /api/resources accepts {base64: 'data:image/<type>;base64,...'}
- Admin-only via IGroupManager::isAdmin (HTTP 403 otherwise)
- Allowed types: jpeg, jpg, png, gif, svg, webp (case-insensitive)
- 5 MB hard cap on decoded bytes, enforced BEFORE invoking the
  image library so oversize blobs cannot be loaded
- Raster MIME cross-check via getimagesizefromstring (SVG validation
  is delegated to the sibling svg-sanitisation capability)
- Storage via IAppData::getFolder('resources') with high-entropy
  resource_<uniqid>.<ext> filenames (folder auto-created on first use)
- Standardised response envelope: success {status, url, name, size};
  error {status, error, message} with stable error enum
- Typed exceptions with stable error codes; raw exception messages
  never leak to clients

Resource serving (GET) and SVG sanitisation are intentionally out
of scope here — they are delivered by sibling changes resource-serving
and svg-sanitisation in their own PRs.

Spec proposal: PR #42 (feature/replica-spec-proposals)
Requirements covered: REQ-RES-001, 002, 003, 004, 005
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
…RES-001..005 (#46)

Adds the resource-uploads capability: a small mini file API for
binary branding assets (icons, widget images) that MyDash widgets
need to host directly, separate from the user's Files folder.

- POST /api/resources accepts {base64: 'data:image/<type>;base64,...'}
- Admin-only via IGroupManager::isAdmin (HTTP 403 otherwise)
- Allowed types: jpeg, jpg, png, gif, svg, webp (case-insensitive)
- 5 MB hard cap on decoded bytes, enforced BEFORE invoking the
  image library so oversize blobs cannot be loaded
- Raster MIME cross-check via getimagesizefromstring (SVG validation
  is delegated to the sibling svg-sanitisation capability)
- Storage via IAppData::getFolder('resources') with high-entropy
  resource_<uniqid>.<ext> filenames (folder auto-created on first use)
- Standardised response envelope: success {status, url, name, size};
  error {status, error, message} with stable error enum
- Typed exceptions with stable error codes; raw exception messages
  never leak to clients

Resource serving (GET) and SVG sanitisation are intentionally out
of scope here — they are delivered by sibling changes resource-serving
and svg-sanitisation in their own PRs.

Spec proposal: PR #42 (feature/replica-spec-proposals)
Requirements covered: REQ-RES-001, 002, 003, 004, 005
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
#52)

Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
@rubenvdlinde rubenvdlinde force-pushed the feature/replica-spec-proposals branch from 5fda513 to 9e204d5 Compare April 30, 2026 12:36
rubenvdlinde added a commit that referenced this pull request Apr 30, 2026
* feat(widgets): label widget per REQ-LBL-001..006

Adds a new `label` widget type for short, single-line plain-text headings
inside dashboard cells. Distinct from `text-display` (which sanitises HTML
and is multi-line): the renderer NEVER uses `v-html` — text is always
output via Vue interpolation, eliminating the XSS surface entirely
(REQ-LBL-001).

- LabelWidget.vue renderer with theme-aware default styling
  (16px / bold / centred / var(--color-main-text) / transparent bg) and
  `overflow-wrap: break-word` so long single words wrap (REQ-LBL-002,
  REQ-LBL-003). Empty/whitespace text shows the localised `Label`
  fallback (REQ-LBL-004). Wrapper fills cell with 12px padding via
  flexbox (REQ-LBL-006).
- LabelForm.vue sub-form with six controls (text, fontSize, color,
  backgroundColor, fontWeight select, textAlign select) and a
  `validate()` method requiring non-empty trimmed text (REQ-LBL-005).
- widgetRegistry.js gets a `label` entry with the spec-mandated
  defaults so the type is selectable in the Add Widget modal
  (REQ-LBL-007).
- Vitest coverage for HTML-as-literal-text, defaults application,
  long-word wrap, empty placeholder, form validation and registry
  exposure.
- l10n keys `Label`, `Label text is required`, `Font Weight`,
  `Normal`, `Bold` added to en.json and nl.json.

Mirrors PR #50 (text-display-widget) shape but plain-text only.
References spec proposals on PR #42.

* chore: update SBOM

---------

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

Adds 25 OpenSpec change proposals modelling a complete dashboard sharing
and runtime experience for MyDash. Specs are organised across 4 dependency
layers (foundation → extensions → UI surfaces → runtime shell):

Dashboards capability (4): multi-scope-dashboards, default-dashboard-flag,
active-dashboard-resolution, fork-current-as-personal — adds group_shared
type, default flag, 7-step resolution chain, fork-from-current action.

Admin (3): group-priority-order, group-routing, allow-personal-dashboards-flag
— admin-controlled group ordering, primary-group resolver, runtime gating.

Grid layout (2): responsive-grid-breakpoints, widget-collision-placement —
4 viewport breakpoints, moveScale reflow, deterministic add-widget placement.

Widgets (5 + 2 modal/menu): text-display-widget, label-widget, image-widget,
link-button-widget, nc-dashboard-widget-proxy, widget-add-edit-modal,
widget-context-menu — five widget types plus shared add/edit modal and
right-click menu.

Resources (3): resource-uploads, resource-serving, svg-sanitisation —
admin-only base64 upload pipeline, immutable-cached serve route, DOM-based
SVG whitelist.

Icons (2): dashboard-icons, custom-icon-upload-pattern — curated MDI
registry plus URL discriminator for uploaded icons.

UI (2): dashboard-switcher-sidebar, runtime-shell — slide-in 3-section
sidebar, page-level shell with canEdit gate.

Infra (1): initial-state-contract — typed PHP→JS bootstrap contract.

Sharing follow-ups (1): dashboard-sharing-followups — extends the existing
dashboard-sharing capability with notifications, bulk management, and
deletion cascade with admin-retention guard.

Includes the baseline dashboard-sharing capability spec under openspec/specs/.

All 25 changes pass openspec validate --strict. Implementation (opsx-apply)
is the next step for each.
@rubenvdlinde rubenvdlinde force-pushed the feature/replica-spec-proposals branch from fd1d7c7 to 1ec571d Compare April 30, 2026 18:35
@rubenvdlinde rubenvdlinde merged commit 172f636 into development Apr 30, 2026
29 of 32 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/replica-spec-proposals branch April 30, 2026 18:35
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/mydash @ 6a3ad6d

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 103/103
npm ✅ 342/342
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-04-30 18:36 UTC

Download the full PDF report from the workflow artifacts.

rubenvdlinde added a commit that referenced this pull request May 3, 2026
….005 (#45)

Centralises every Nextcloud IInitialState::provideInitialState call behind a
typed PHP builder (InitialStateBuilder + Page enum + MissingInitialStateException)
and mirrors it on the JS side with a typed loadInitialState(page) reader that
fills defaults and warns on schema-version mismatch.

- lib/Service/InitialStateBuilder.php: typed setters per page key, REQUIRED_KEYS
  map keyed by Page::WORKSPACE / Page::ADMIN, apply() validates and pushes every
  key plus _schemaVersion (INITIAL_STATE_SCHEMA_VERSION = 1).
- lib/Exception/MissingInitialStateException.php: thrown by apply() with page
  + missing-key context.
- lib/Controller/PageController.php and lib/Settings/MyDashAdmin.php: refactored
  to construct the builder and call apply() — the only sanctioned call sites.
- src/utils/loadInitialState.js: per-page key/default tables mirroring the spec
  Data Model exactly; emits a console.warn on _schemaVersion mismatch.
- src/main.js and src/admin.js: load via the reader and provide() each key by
  identical name (no renaming at the boundary, no ref/reactive wrapping).
- tests/Unit/Service/InitialStateBuilderTest.php: per-page apply pushes every
  key + schema version, missing required key throws, schema-version always
  pushed (5 tests / 28 assertions).
- tests/Unit/Service/InitialStateContractLintTest.php: PHP grep that fails when
  any file under lib/ outside InitialStateBuilder.php calls
  provideInitialState() directly.
- tests/bootstrap.php: register vendor/nextcloud/ocp PSR-4 autoload as fallback
  when running outside the Nextcloud container so OCP-interface mocks resolve.

PR #42 owns the spec text on origin/feature/replica-spec-proposals.

The matching JS-side Vitest scan (loadState\(['"]mydash) is deferred pending
PR #43 (chore/add-vitest-setup); a TODO comment in
InitialStateContractLintTest references the follow-up.
rubenvdlinde added a commit that referenced this pull request May 3, 2026
…SET-012..014 (#44)

Adds the foundational global admin setting `group_order` (JSON `string[]`
in `oc_mydash_admin_settings`) plus two admin-only HTTP endpoints needed by
the upcoming `group-routing` change (REQ-TMPL-012):

- AdminSetting::KEY_GROUP_ORDER constant.
- AdminSettingsService::getGroupOrder() — defensive read; missing row,
  null, empty string, corrupt JSON, or non-array decode all resolve to []
  without throwing (REQ-ASET-012 corrupt-JSON scenario).
- AdminSettingsService::setGroupOrder(array) — validates non-empty
  strings, deduplicates first-wins preserving order, persists as JSON.
- AdminController::listGroups() — GET /api/admin/groups, returns
  {active, inactive, allKnown}; inactive sorted by displayName, stale IDs
  preserved in active so the admin can remove them (REQ-ASET-013).
- AdminController::updateGroupOrder() — POST /api/admin/groups, replaces
  wholesale, 400 on validation failure (REQ-ASET-014).
- Both endpoints admin-gated via IGroupManager::isAdmin (403 on
  non-admin, 401 on unauthenticated). The inactive list reveals every
  group on the system, so GET is admin-only too.

Routes registered in appinfo/routes.php. Unit tests cover corrupt-JSON
fallback, dedup order preservation, replace-wholesale semantics,
admin-guard 403/401, and the disjoint+exhaustive listGroups contract
including stale-ID surfacing.

Pre-existing quality issues fixed in passing:
- 4 NamedParametersSniff false-positives on Entity __call setters
  (AdminSetting, Dashboard, ConditionalRule, WidgetPlacement) — guarded
  with phpcs:ignore + a comment explaining why named args break Entity
  __call.
- DashboardService MissingImport — added `use WidgetPlacement` and
  removed the FQCN at the call site.

Spec: #42 (feature/replica-spec-proposals).
Frontend drag-drop UI is out of scope for this PR; ships as a follow-up.
rubenvdlinde added a commit that referenced this pull request May 3, 2026
…RES-001..005 (#46)

Adds the resource-uploads capability: a small mini file API for
binary branding assets (icons, widget images) that MyDash widgets
need to host directly, separate from the user's Files folder.

- POST /api/resources accepts {base64: 'data:image/<type>;base64,...'}
- Admin-only via IGroupManager::isAdmin (HTTP 403 otherwise)
- Allowed types: jpeg, jpg, png, gif, svg, webp (case-insensitive)
- 5 MB hard cap on decoded bytes, enforced BEFORE invoking the
  image library so oversize blobs cannot be loaded
- Raster MIME cross-check via getimagesizefromstring (SVG validation
  is delegated to the sibling svg-sanitisation capability)
- Storage via IAppData::getFolder('resources') with high-entropy
  resource_<uniqid>.<ext> filenames (folder auto-created on first use)
- Standardised response envelope: success {status, url, name, size};
  error {status, error, message} with stable error enum
- Typed exceptions with stable error codes; raw exception messages
  never leak to clients

Resource serving (GET) and SVG sanitisation are intentionally out
of scope here — they are delivered by sibling changes resource-serving
and svg-sanitisation in their own PRs.

Spec proposal: PR #42 (feature/replica-spec-proposals)
Requirements covered: REQ-RES-001, 002, 003, 004, 005
rubenvdlinde added a commit that referenced this pull request May 3, 2026
#52)

Add a transactional single-default-per-group invariant for
`group_shared` dashboards by reusing the existing `isDefault SMALLINT`
column (no schema migration required).

Changes
- `DashboardMapper::clearGroupDefaults()` and `setGroupDefaultUuid()`:
  the two halves of the transactional flip.
- `DashboardService::setGroupDefault()`: admin-only, wraps both writes
  in a single `IDBConnection::beginTransaction()` / `commit()` /
  `rollBack()` block. Order is SET-target first so the 0-row count
  triggers a 404 with the previous default preserved.
- `DashboardService::createGroupShared()`: explicitly forces
  `isDefault = 0` on insert (REQ-DASH-016 — defense-in-depth against
  payload smuggling).
- `DashboardService::updateGroupShared()`: drops `isDefault` from the
  patch before applying (REQ-DASH-017 — PUT can never flip the flag).
- `DashboardApiController::setGroupDefault()` mapped to
  `POST /api/dashboards/group/{groupId}/default`. Admin guard is in
  the body (matches existing `updateGroup`/`deleteGroup` pattern).

Tests
- 7 service tests cover the transactional invariant (commit on
  happy path, rollBack on cross-group 404, rollBack on mid-flip
  exception), the non-admin guard, and the create/update field
  hardening.
- 6 controller tests cover the route surface — non-admin 403,
  anonymous 401, missing-uuid 400, happy path 200, cross-group 404,
  and reflection-level checks that `createGroup`/`updateGroup`
  signatures do not expose `isDefault` as a parameter.
- `tests/Stubs/DoctrineStubs.php` defines minimal Doctrine DBAL
  placeholder classes so PHPUnit's automatic mock generator can
  introspect `IDBConnection` outside the Nextcloud container; no-op
  inside the container.

Spec source: PR #42 (`feature/replica-spec-proposals`,
`openspec/changes/default-dashboard-flag/`). Builds on PR #47
(`multi-scope-dashboards`, already in `development`).
rubenvdlinde added a commit that referenced this pull request May 3, 2026
* feat(widgets): label widget per REQ-LBL-001..006

Adds a new `label` widget type for short, single-line plain-text headings
inside dashboard cells. Distinct from `text-display` (which sanitises HTML
and is multi-line): the renderer NEVER uses `v-html` — text is always
output via Vue interpolation, eliminating the XSS surface entirely
(REQ-LBL-001).

- LabelWidget.vue renderer with theme-aware default styling
  (16px / bold / centred / var(--color-main-text) / transparent bg) and
  `overflow-wrap: break-word` so long single words wrap (REQ-LBL-002,
  REQ-LBL-003). Empty/whitespace text shows the localised `Label`
  fallback (REQ-LBL-004). Wrapper fills cell with 12px padding via
  flexbox (REQ-LBL-006).
- LabelForm.vue sub-form with six controls (text, fontSize, color,
  backgroundColor, fontWeight select, textAlign select) and a
  `validate()` method requiring non-empty trimmed text (REQ-LBL-005).
- widgetRegistry.js gets a `label` entry with the spec-mandated
  defaults so the type is selectable in the Add Widget modal
  (REQ-LBL-007).
- Vitest coverage for HTML-as-literal-text, defaults application,
  long-word wrap, empty placeholder, form validation and registry
  exposure.
- l10n keys `Label`, `Label text is required`, `Font Weight`,
  `Normal`, `Bold` added to en.json and nl.json.

Mirrors PR #50 (text-display-widget) shape but plain-text only.
References spec proposals on PR #42.

* chore: update SBOM

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
rubenvdlinde added a commit that referenced this pull request May 3, 2026
* feat(openspec): add 25 spec proposals for multi-tenant dashboard platform

Adds 25 OpenSpec change proposals modelling a complete dashboard sharing
and runtime experience for MyDash. Specs are organised across 4 dependency
layers (foundation → extensions → UI surfaces → runtime shell):

Dashboards capability (4): multi-scope-dashboards, default-dashboard-flag,
active-dashboard-resolution, fork-current-as-personal — adds group_shared
type, default flag, 7-step resolution chain, fork-from-current action.

Admin (3): group-priority-order, group-routing, allow-personal-dashboards-flag
— admin-controlled group ordering, primary-group resolver, runtime gating.

Grid layout (2): responsive-grid-breakpoints, widget-collision-placement —
4 viewport breakpoints, moveScale reflow, deterministic add-widget placement.

Widgets (5 + 2 modal/menu): text-display-widget, label-widget, image-widget,
link-button-widget, nc-dashboard-widget-proxy, widget-add-edit-modal,
widget-context-menu — five widget types plus shared add/edit modal and
right-click menu.

Resources (3): resource-uploads, resource-serving, svg-sanitisation —
admin-only base64 upload pipeline, immutable-cached serve route, DOM-based
SVG whitelist.

Icons (2): dashboard-icons, custom-icon-upload-pattern — curated MDI
registry plus URL discriminator for uploaded icons.

UI (2): dashboard-switcher-sidebar, runtime-shell — slide-in 3-section
sidebar, page-level shell with canEdit gate.

Infra (1): initial-state-contract — typed PHP→JS bootstrap contract.

Sharing follow-ups (1): dashboard-sharing-followups — extends the existing
dashboard-sharing capability with notifications, bulk management, and
deletion cascade with admin-retention guard.

Includes the baseline dashboard-sharing capability spec under openspec/specs/.

All 25 changes pass openspec validate --strict. Implementation (opsx-apply)
is the next step for each.

* feat(dashboard-icons): implement icon registry per REQ-ICON-001..004
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

ready-for-code-review Build complete — awaiting code reviewer ready-for-security-review Code review complete — awaiting security reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant