This repository was archived by the owner on May 29, 2026. It is now read-only.
feat: implement 24 OpenSpec proposals (multi-scope, widgets, resources, runtime shell)#99
Merged
Merged
Conversation
Walkthrough of all 9 archived OpenSpec features uncovered three critical
runtime bugs in features marked "implemented". This commit restores them
and clears the supporting quality stack.
Critical fixes
--------------
- ConditionalRule.createdAt: switch ?DateTime to ?string ('c' format) to
match sibling entities. Doctrine DBAL cannot bind a DateTime without
an explicit addType registration; INSERT now succeeds. Same change in
AdminSetting.updatedAt.
- AdminTemplateService + TemplateService: drop the ramsey/uuid require
and inline the random_bytes(16) UUID generator already used by
DashboardFactory. The ramsey class was never autoloaded at runtime
(Application.php does not require vendor/autoload.php), so admin
template creation and template-based dashboard provisioning both
threw 500.
- DashboardService: stop hardcoding PERMISSION_FULL when auto-creating
user dashboards. Read defaultPermissionLevel and defaultGridColumns
from AdminSettingMapper and pass them to DashboardFactory.
Quality
-------
- NamedParametersSniff: skip Entity-magic accessors (set*/get*/is*) on
classes extending OCP\AppFramework\Db\Entity. Entity::__call uses
args[0], so named arguments break those calls. Eliminates 4 false
positives.
- DashboardService: add the missing WidgetPlacement import (PHPMD).
- PageController: replace \OC::server->get(IManager::class) with proper
DI (Psalm).
- UserAttributeResolver: drop the non-existent IUser::getLanguage() call
and read 'core/lang' from IConfig instead (Psalm).
Frontend warnings
-----------------
- TileEditor + WidgetStyleEditor: NcSelect was passing 'Icon' as the
vue-select label key (option.Icon does not exist). Use input-label
for the visible label and label="label" for the option key.
- AdminSettings: replace HTML <label> + bare NcSelect with input-label
prop, satisfying NcSelect's accessibility requirement.
- DashboardSwitcher / WidgetPicker / WidgetWrapper / TileEditor /
WidgetStyleEditor: add aria-label or input-label to icon-only NcButton
/ unlabeled NcTextField / inline color-pick NcButton instances.
Tests: 106 / 106 passing (293 assertions). composer phpcs / phpmd /
psalm clean. End-to-end API verified for conditional rules, admin
templates, prometheus metrics.
…creenshots PHPStan ------- - Add autoload-dev mapping for nextcloud/ocp's OCP\* and NCU\* namespaces. The package ships PHP stubs but declares no autoload section, so PHPStan could not resolve any Nextcloud type and reported every OCP\IUser / IL10N / IURLGenerator etc. call as "unknown class" - 722 errors total. Registering the namespace in our autoload-dev makes the stubs reachable. - Tighten phpstan.neon scanDirectories to point at the OCP/ and NCU/ subdirectories (not the package root) and add focused ignoreErrors for Doctrine\DBAL\Schema\Table calls (provided by Nextcloud's 3rdparty bundle, not our app's vendor) and the JSONResponse strict-status union / Entity::jsonSerialize stub gaps. - Result: 722 -> 0 errors. PHPStan is now meaningful CI signal. Real type fixes surfaced once OCP became visible ------------------------------------------------ - AdminSettingMapper::setSetting(): pass formatted ISO string to setUpdatedAt() instead of a DateTime instance (the entity stores the timestamp as ?string). - DashboardResolver, DashboardService, TemplateService: use 1 instead of true for setIsActive(int), and 0 instead of false for the getIsCompulsory()/getIsVisible() comparisons. Same coercion semantics at runtime, but PHPStan can verify the types. - AdminTemplateService::createTemplate(): cast bool $isDefault to int before passing to the SMALLINT column setter. - DashboardResolver::getEffectivePermissionLevel(): drop the unreachable null-fallback branch (Dashboard::permissionLevel is non-nullable with PERMISSION_FULL default) and the unused settingMapper / AdminSetting imports that only existed to feed it. - ConditionalService / PermissionService: switch getIsVisible()/getIsCompulsory() === false to === 0 (the Entity stores these as int flags, not booleans). phpunit-unit.xml rename ----------------------- - git mv phpunit.xml -> phpunit-unit.xml to match the convention used across other Conduction apps. Composer scripts (test:unit, test:all, test:coverage, test) updated to pass --configuration explicitly so PHPUnit no longer needs to auto-discover. Screenshots ----------- - Add docs/screenshots/admin-settings.png, customize-panel-widgets.png, customize-panel-dashboards.png, template-create-modal.png to document the main customize / admin / template flows. Captured at 1440x1100. Tests: 106 / 106 passing, 293 assertions. PHPCS / PHPMD / Psalm / PHPStan: all clean.
…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.
…into feature/replica-spec-proposals
- Add /sendent-workspace-main/ and /2026.*_sendent-workspace-main.zip to .gitignore (research-only vendor extract, not part of the app). - Remove explicit "Sendent" mentions from image-widget and link-button-widget proposals; keep the technical content intact.
Strips active content (script tags, on* handlers, javascript: URLs, external entities) from SVG payloads before they hit storage. Used by the resource-upload pipeline to make user-supplied SVGs safe to serve inline. New InvalidSvgException is thrown when the input cannot be made safe.
- ResourceController exposes the upload + serve endpoints, with a dedicated ResourceUploadRequestParser to handle PUT/PATCH multipart payloads (PHP $_FILES doesn't populate for those verbs). - ResourceService owns the upload pipeline and routes SVGs through SvgSanitiser before persisting. - ImageMimeValidator enforces declared-vs-actual MIME parity to block smuggled-payload attacks (image/png claim with non-PNG bytes, etc.). - New typed exceptions (ResourceException base + nine concrete kinds) produce predictable HTTP responses from the controller.
…cade - DashboardShare entity, mapper, service, and API controller for user/group sharing with view/edit permission grants. - Migration Version001005 adds the oc_mydash_dashboard_shares table via DashboardShareTableBuilder. - Notifier renders dashboard_shared and dashboard_ownership_transferred notifications (REQ-SHARE-011). - UserDeletedListener cascades share cleanup and transfers admin-retained dashboards on user deletion (REQ-SHARE-012, 013). - New PersonalDashboardsDisabledException for the admin-disabled-personal scope path used by DashboardService.
- DashboardConfigMenu: top-bar entry with Conduction + Sendent brand links and access to the dashboard config modal. - DashboardConfigModal: edit dashboard metadata, manage shares. - WidgetPickerModal: dedicated picker UI for adding widgets. - Add img/conduction-logo.png and img/sendent-logo.png brand assets used by the menu.
- Register share, resource, getById, sharee-search, and revoke routes in appinfo/routes.php. - Extend DashboardApiController + DashboardService + DashboardFactory + DashboardMapper + Dashboard entity to surface share-aware lookups, ownership transfer, and the personal-disabled scope path. - PermissionService gains share-derived view/edit checks. - Frontend store + api.js learn to fetch shares, sharees, and to call the new endpoints; Views.vue mounts DashboardConfigMenu / DashboardConfigModal / WidgetPickerModal. - Bump appinfo version to 1.0.4-unstable.0.
Captured walkthrough notes from a working session through the dashboard flows; left as raw findings for follow-up tickets and spec refinement.
The DashboardShare entity declares an updatedAt field but the table builder forgot to add the matching column, so updates would silently fail to persist on a fresh schema. Adds the nullable DATETIME column.
Implements the label-widget OpenSpec proposal: renderer + form + registry entry + en/nl translations + Vitest unit coverage. Spec delta merged into widgets capability; proposal archived. Cohort infrastructure: bootstraps Vitest + jsdom + @vue/test-utils + @vitejs/plugin-vue2 with vitest.config.js, css-noop plugin, and global setup so 23 sibling widget proposals can land their unit tests too.
Registry-driven AddWidgetModal with useWidgetForm composable, validation pipeline, and close discipline (cancel/backdrop/Esc). Per-type sub-forms deferred to their owning widget proposals; registry skips types whose form is not yet registered. Toolbar dropdown rewired to consume registry. Spec delta merged into widgets capability; proposal archived.
….006) InitialStateBuilder centralises the per-page key set and validates required keys at apply time. Frontend reader returns a typed object with defaults and warns on schema-version mismatch. Workspace + admin entries refactored to inject every key via the builder/reader pair. CI lint guards against drift. Spec capability created; proposal archived.
Introduces a new built-in widget type `text` that renders user-authored multi-line content with DOMPurify-sanitised HTML support, theme-aware inline style controls (font size, colour, background, alignment), and a localised empty-state placeholder. Registry entry + en/nl translations + Vitest coverage. Spec delta merged into a new text-display-widget capability; proposal archived.
…/012/013) Pin four explicit columnOpts.breakpoints (1400/1100/768/480 → 12/8/4/1) with the moveScale reflow algorithm, centralise CELL_HEIGHT (60 px) and GRID_MARGIN (8 px) in src/composables/useGridManager.js as the single source of truth (mirrored to the --mydash-cell-height CSS custom property), and bump gridstack from ^10.3.1 to ^12.2.1 (resolved 12.6.0). The v10 → v12 bump only required dropping the now-removed gridstack-extra.min.css side-effect import — the engine generates the per-column-count CSS dynamically. GridStack.init signature, change-event payload, engine.nodes, removeWidget, enable/disable lifecycle are all unchanged across the major. Archives openspec/changes/responsive-grid-breakpoints into archive/2026-05-02-… and merges the spec delta into canonical openspec/specs/grid-layout/spec.md (updated REQ-GRID-007 with the breakpoint table + scenarios; added REQ-GRID-012 cell geometry and REQ-GRID-013 version pin). 12/12 vitest cases for the new composable; 59/59 overall vitest pass; webpack production build succeeds; openspec validate --all --strict reports 34/34 passed.
…RES-001..005)
Implements the `resource-uploads` capability — a small admin-only mini
file API for branding/icon assets that MyDash widgets reference directly.
Backend:
- POST /api/resources accepting `{base64: 'data:image/<type>;base64,...'}`
- Admin guard via IGroupManager::isAdmin (HTTP 403 otherwise)
- 5 MB hard cap on decoded bytes (enforced before getimagesizefromstring)
- Allowed types: jpeg/jpg/png/gif/svg/webp (case-insensitive); SVG routed
through SvgSanitiser
- Cross-MIME validation for raster types
- Storage via IAppData::getFolder('resources') with high-entropy
resource_<uniqid>.<ext> filenames
- Standardised {status, error, message} error envelope with stable enum;
raw exception messages never reach the client
- New route entry, frontend wrapper at src/services/resourceService.js,
English + Dutch translations for every error-code display string
Tests:
- 17 PHPUnit tests across ResourceController, ResourceUploadRequestParser,
ResourceService (with mocked IAppData), ImageMimeValidator,
SvgSanitiser, and ResourceServiceSvgIntegration — 59/59 in this scope
- 4 vitest tests for the frontend wrapper covering happy path, server
error envelope, network failure, and malformed body
- composer check:strict passes (16 unrelated DBAL\ParameterType errors
pre-existing in DashboardShareServiceFollowupsTest)
- npm run build OK
PHPStan baseline: extended ResponseHelper's existing JSONResponse status
code ignore to ResourceController, plus a getimagesizefromstring `mime`
offset noop for ImageMimeValidator (defence-in-depth `??` is intentional).
Promotes the existing `dashboard-icons` frontend registry (introduced in PR #58) to a fully wired capability: Dashboard entity now carries a typed `?string $icon` field with the registry-key/URL/NULL convention, a new migration (Version001006) adds the column on `mydash_dashboards`, the API controller and DashboardService plumb the value end-to-end, and DashboardConfigModal grows a registry-driven `<select>` icon picker so the create/edit form stays in lock-step with `Object.keys(DASHBOARD_ICONS)`. DashboardConfigMenu now resolves the per-dashboard icon through the shared `IconRenderer`. Adds 20 vitest cases covering the resolver's null/undefined/empty/unknown tolerance and the URL discriminator. Spec delta merged into the canonical `dashboard-icons` capability; proposal archived under `2026-05-02-dashboard-icons`.
Locks the dual-mode field convention so a single column may hold a built-in registry name OR a `/apps/mydash/resource/...` URL OR NULL, discriminated at render time by `isCustomIconUrl()`. Builds on the `dashboard-icons` foundation (PR #58 dropped the `IconRenderer` and the helper) and the `resource-uploads` upload pipeline. Frontend: - `dashboardIcons.js`: `getIconComponent` returns `null` for URL inputs (REQ-ICON-006); registry/null/empty inputs still resolve to `DEFAULT_ICON` so REQ-ICON-001 holds. - `IconRenderer.vue`: gains an `alt` prop wired into the `<img>` branch with a non-empty fallback (REQ-ICON-007). - `IconPicker.vue` (new): shared picker with both a registry `<select>` and an `<input type="file" accept="image/*">` visible simultaneously, 24x24 live preview through `IconRenderer`, inline error display, and previous-value preservation on upload failure (REQ-ICON-008). - `services/resourceService.js` (new): thin wrapper around `POST /apps/mydash/api/resources` that normalises the `{status, error, message}` envelope into a typed `ResourceUploadError` carrying the stable enum (`forbidden`, `file_too_large`, `invalid_image_format`, `mime_mismatch`, `network_error`, ...). Backend: - `WidgetPlacement.tileIcon` docblock documents the registry-name | URL | NULL convention (REQ-ICON-009). `Dashboard.icon` docblock update is deferred until the parallel `dashboard-icons` proposal lands the column (the entity field doesn't exist on this branch yet). Tests: - 9 IconPicker, 6 IconRenderer, 9 dashboardIcons, 4 resourceService vitest cases covering the discriminator truth table, both render branches, the upload happy/error paths, and value preservation. - Total: 75/75 vitest passing. - ESLint clean (only pre-existing widgetBridge JSDoc warnings). - `composer check:strict` ALL CHECKS PASSED (16 PHPUnit Doctrine\DBAL\ParameterType errors in DashboardShareServiceFollowupsTest pre-existing per commit de061cd). - `npm run build` OK. i18n: 5 new strings (`Failed to upload icon`, `Icon preview`, `Select icon...`, `Upload icon`, `Uploading...`) added to all four l10n files (en.js/json + nl.js/json). Refactor scope: `DashboardSwitcher`, admin CRUD UI, and link-button widget all DEFERRED — none currently render a Dashboard.icon field (switcher is label-only, admin manages templates not per-dashboard icons, link-button widget belongs to its own proposal). `IconPicker` is exported and ready for those integrations. Spec delta merged into canonical `openspec/specs/dashboard-icons/spec.md`; proposal archived under `2026-05-02-custom-icon-upload-pattern`. `openspec validate --all --strict`: 35 passed, 0 failed.
Implements REQ-DASH-011..014: third dashboard type `group_shared`
alongside `user` and `admin_template`, plus `default` synthetic group
sentinel and `GET /api/dashboards/visible` returning the deduplicated,
source-tagged union of personal + group + default-group dashboards.
- Migration Version001008Date20260502000000 adds nullable `group_id`
column + composite `(type, group_id)` index on `oc_mydash_dashboards`
- DashboardFactory enforces the (type, groupId) invariant in both
directions and accepts the new `permissionLevel` kwarg
- 6 new routes: /api/dashboards/visible plus the 5 group-scoped CRUD
endpoints (specific routes ordered before wildcard /{id} routes)
- PermissionService treats group_shared as view_only for non-admin
members and full for admins, regardless of the row's stored level
- Frontend store gains `userDashboards`, `groupSharedDashboards`,
`defaultGroupDashboards` getters; loadDashboards now calls
/api/dashboards/visible with a fallback to the legacy listing
- All four l10n files carry the new error messages (en + nl)
- Spec delta merged into openspec/specs/dashboards/spec.md and the
change folder archived under 2026-05-02-multi-scope-dashboards
The AdminApp.vue UI for managing group-shared dashboards is deferred
to a follow-up `admin-group-management` change — this commit ships
backend + store wiring only.
…ASH-015..017)
The multi-scope-dashboards parent commit already shipped the backend for
this proposal: mapper helpers (`clearGroupDefaults`, `setGroupDefaultUuid`),
the transactional `DashboardService::setGroupDefault()` flip, the
`createGroupShared` zero-default seed, the `updateGroupShared` patch
strip, and the `POST /api/dashboards/group/{groupId}/default` route. This
change finishes the proposal by adding everything that was missing:
- `src/services/api.js`: new `setGroupDashboardDefault(groupId, uuid)`
client method.
- `src/stores/dashboard.js`: new `setGroupDashboardDefault` action with
optimistic flip (target → 1, every other row in same group → 0) and
rollback on 4xx/5xx.
- `src/components/admin/AdminSettings.vue`: new "Group-shared dashboards"
section that lists each curated group's dashboards, renders a "Default"
badge on the row where `isDefault === 1`, and a "Set as default" button
on every other row. Local optimistic update with rollback mirrors the
store action so the admin panel never lies.
- `tests/Unit/Service/DashboardServiceGroupSharedTest.php`: four new
PHPUnit cases — happy-path flip, cross-group 404 + rollback, mid-flip
exception → rollback + re-throw, non-admin 403 with no transaction.
- `src/stores/__tests__/dashboard.spec.js`: two new vitest cases for the
store action's optimistic update + rollback paths.
- `l10n/{en,nl}.{json,js}`: six new keys for the admin section copy and
failure toast.
- Pre-existing fix: PHPCS docblock indentation in `DashboardService`
class header (4 errors).
Quality gates: composer check:strict OK, vitest 55/55 OK, npm run build
OK, openspec validate --all --strict 33/33 OK. PHPUnit env-break on
`Doctrine\\DBAL\\ParameterType` is pre-existing (tests run inside the
Nextcloud container).
…er_dashboards (REQ-ASET-003 extended, REQ-ASET-015) Formalises the runtime gating semantics of the existing `allow_user_dashboards` admin flag. When the flag is off (the new secure default — admins MUST opt in), the personal-dashboard create endpoint returns the stable `personal_dashboards_disabled` 403 envelope. Existing personal dashboards remain readable, editable, deletable, and activatable — only NEW creation is blocked. The admin toggle is wired end-to-end (was silently no-op'd before because the frontend sent long-form keys the controller doesn't accept). Backend - DashboardService: add `getAllowUserDashboards(): bool` precondition reader; rewire `assertPersonalDashboardsAllowed()` through it. - DashboardApiController::create: assert FIRST so the 403 envelope is returned before any other validation. - AdminSettingsService::getSettings + PermissionService::canCreateDashboard: default value flipped to `false` to match the secure default. - PageController + MyDashAdmin: route the initial-state push through the new helper for a single source of truth. Frontend - DashboardConfigMenu: hide the "Create dashboard…" entry when the flag is off, via a typed `inject` from the workspace initial-state contract. - Views.vue: hide the empty-state "Create dashboard" button and swap the description for a localised "managed by your administrator" explainer. - useDashboardStore.createDashboard: surface the 403 envelope as a localised toast via `@nextcloud/dialogs::showError`, then re-throw. - AdminSettings.vue: add helper text spelling out the data-preservation guarantee; fix the silent no-op by sending the abbreviated camelCase keys (`allowUserDash`) the controller actually accepts. Translations - Add Dutch + English entries for the toast and admin helper text in all four l10n/ files. Tests - New `tests/Unit/Exception/PersonalDashboardsDisabledExceptionTest` pins the stable error code (`personal_dashboards_disabled`), HTTP 403 mapping, and the translatable English source string. - New `tests/Unit/Service/DashboardServicePersonalGatingTest` covers the reader, the assert (throw / silent), and the default-false guarantee. - New `dashboard.spec.js` cases cover the toast surfacing on the gating envelope and silence on unrelated errors. - Updated `AdminSettingsServiceTest` for the flipped default. Spec - Merged the change delta into the canonical `admin-settings/spec.md`. - REQ-ASET-003 rewritten with the runtime-gating envelope + scenarios. - New REQ-ASET-015 documents the initial-state mirror. - Data-model table + REQ-ASET-001 default scenario flipped to `false`.
Adds the `dashboard-switcher` capability — a left-edge slide-in
navigation panel that lists every dashboard visible to the user,
grouped into three fixed-order sections by `source` discriminator
(primary group / default group / personal). Empty sections collapse
entirely so no orphan headings render.
Each row uses the shared `IconRenderer` from `dashboard-icons` (no
inline `v-if="iconUrl"` branch). Click emits `update:open(false)`
THEN `switch(id, source)` — the `source` payload is load-bearing
because the parent uses it to pick the correct API endpoint per
REQ-DASH-013/REQ-DASH-014. Personal rows expose a hover-revealed
delete button (`@click.stop` so it never triggers a switch). When
`allowUserDashboards === true` the personal section ends with a
`+ New Dashboard` row that emits `update:open(false)` then
`create-dashboard()`.
Companion `SidebarBackdrop.vue` is a tiny click-to-close shim wired
by the parent. The sidebar uses Vue 2.7's `model: { prop: 'isOpen',
event: 'update:open' }` rebind so a parent template can write
`v-model="sidebarOpen"` while the component still emits the
spec-mandated `update:open(boolean)` event — Vue 2.7 doesn't compile
Vue 3's `v-model:open` syntax. Future runtime-shell adopts the same
binding shape.
Wired into the current `src/views/Views.vue` shell (the proposal's
`WorkspaceApp.vue` belongs to the not-yet-shipped runtime-shell
change). Added a topbar hamburger toggle and the click-to-close
backdrop. Initial state (`primaryGroupName`, `allowUserDashboards`)
comes through the typed `inject` contract from `src/main.js`.
Tests: 26 new Vitest cases — section visibility matrix, emit-order
assertions, source-discriminator coverage, delete-isolation
(@click.stop), conditional create-row, reactive `.active` highlight,
icon delegation, Esc/close. 79/79 total tests pass.
Translations: `My Dashboards`, `+ New Dashboard` added to all four
l10n catalogues (en/nl × js/json); the other four required strings
already existed.
Verify: ESLint 0 errors (pre-existing widgetBridge JSDoc warnings
unchanged), webpack build clean (4 pre-existing peer-dep warnings),
`openspec validate --all --strict` 34/34 pass.
Archived as `2026-05-02-dashboard-switcher-sidebar`.
….008)
Adds the read-side of the resource pipeline that complements the
admin-only base64 upload from R1:
- GET /apps/mydash/resource/{filename} (REQ-RES-006) — non-OCS
StreamResponse with extension-derived Content-Type and a one-year
immutable Cache-Control. Path traversal returns 404.
- GET /api/resources (REQ-RES-007) — JSON listing of uploaded
resources sorted by modifiedAt desc; empty folder returns [] with
HTTP 200, never 404.
- 5 MB serving cap (REQ-RES-008) — files larger than the upload cap
are refused with HTTP 413 BEFORE bytes are read into memory.
Both endpoints carry NoAdminRequired (any logged-in user). The
streamer uses php://memory bounded by the upload cap.
Tests (13 new): Content-Type for png/jpg/svg/octet-stream fallback,
missing-file 404, missing-folder 404, encoded-traversal 404,
plain-`..`-segment 404, empty-name 404, oversize 413 without
loading bytes, list sorted desc with one/three resources, empty
folder returns [].
Archive: spec delta merged into the canonical
openspec/specs/resource-uploads/spec.md (now REQ-RES-001..008).
Drive-by: phpcbf-fixed 4 pre-existing PHPCS violations in
lib/Service/DashboardService.php (SuppressWarnings tag indentation).
…-DASH-018, REQ-DASH-019) Wires the workspace boot path to a deterministic 7-step resolver (`DashboardService::resolveActiveDashboard`) and exposes `POST /api/dashboards/active` so the frontend can persist the user's choice via `IConfig`. Stale preferences silently auto-clear with a warning log; empty UUIDs clear the pref so the chain falls through to step 2 on the next render. Frontend: Pinia store gains a `resolveActive` getter that mirrors the backend precedence and a `persistActivePreference()` action that the existing `switchDashboard()` calls fire-and-forget. Pre-existing fixes encountered along the way: - Notifier: replace `\OCP\L10N\IL10N` (wrong namespace) with imported `OCP\IL10N`; fix `setParsedMessage(subject:)` named arg to `message:`. - UserDeletedListener: inject `Psr\Log\LoggerInterface` instead of the deprecated `\OC::$server->getLogger()` accessor; suppress legitimate CouplingBetweenObjects on the orchestration listener. - DashboardShareService: use imported `DateTime` instead of `\DateTime`. - DashboardShareApiController: rename short `$s` lambda var to `$share`. - phpstan.neon: ignore narrow pre-existing OCP-stub-related issues (DataResponse template, ResourceController status int, getimagesize mime offset). Archives the change as `2026-05-02-active-dashboard-resolution` and folds REQ-DASH-018 + REQ-DASH-019 into the canonical dashboards spec.
…2/013/014)
Persists an ordered list of Nextcloud group IDs as the global setting
`group_order` (JSON `string[]`, default `[]`) so admins can deterministically
control which groups MyDash uses for workspace routing and in what order.
The first active group becomes a user's primary workspace and the
forthcoming `group-routing` change reads the same setting via
`AdminSettingsService::getGroupOrder()` to resolve REQ-TMPL-012.
Adds two admin-only endpoints:
- `GET /api/admin/groups` returns `{active, inactive, allKnown}` so the
UI can render both columns and surface stale (deleted) group IDs in
one round-trip.
- `POST /api/admin/groups` body `{groups: string[]}` replaces the
setting wholesale (UI sends the full ordered list after each drag).
Both endpoints are guarded with `IGroupManager::isAdmin($userId)` —
the inactive list reveals every group on the system, so even GET is
admin-gated. Validation rejects non-array bodies and non-empty-string
elements with HTTP 400; duplicates are deduplicated (first-occurrence-
wins); unknown IDs are tolerated and persisted (downstream resolver
drops them at runtime).
Frontend ships a two-list drag-and-drop component
(`src/components/admin/GroupPriorityOrder.vue`) mounted from
`AdminSettings.vue`. Drag, drop, and click-to-move arrows trigger a
debounced (300ms) `POST /api/admin/groups` so admins never need a Save
button. Stale IDs render with a localized "(removed)" affix. Native
HTML5 drag-and-drop is used because `vuedraggable` is not actually
installed in this app — sticking with zero new dependencies.
Defensive read in `getGroupOrder()` returns `[]` on missing or corrupt
JSON and never throws, so a hand-edited DB row cannot brick the admin
UI or the downstream resolver.
PHPUnit: 19 new admin-settings + admin-settings-controller assertions
covering all REQ-ASET-012/013/014 scenarios. Vitest: 7 new specs for
the GroupPriorityOrder component (load, stale-affix, click-to-move,
debounced auto-save, filter). All checks (PHPCS, PHPMD, Psalm, ESLint,
Vitest, webpack build) pass cleanly except for the pre-existing
`Doctrine\DBAL\ParameterType` PHPUnit env breakage that affects 28
unrelated tests and a handful of pre-existing Psalm/PHPMD warnings in
`Notifier.php` / `DashboardShareService.php`.
Adds text widget renderer/form/registry entry plus DOMPurify dependency for safe HTML rendering. Conflicts in l10n/* resolved by appending text-widget keys (Text, No text content, Text is required, Font Size). package.json kept gridstack v12 from HEAD and added dompurify ^3.2.4 from the branch.
Adds IconPicker, isCustomIconUrl discriminator, and the URL-handling contract for getIconComponent (returns null for URLs, falling through to <img> rendering by callers). Conflicts in dashboardIcons.spec.js, resourceService.js/.spec.js resolved by combining HEAD's broader test coverage with the branch's URL-specific REQ-ICON-005/006 tests. Spec extended with REQ-ICON-005..009.
Replaces the runtime-shell stub DashboardSwitcherSidebar with the full slide-in panel (3-section bucketing, hover-revealed delete, + New Dashboard row, IconRenderer). SidebarBackdrop merged: emits 'close' (used by WorkspaceApp + Views), carries both legacy and canonical CSS classes. Updated WorkspaceApp.vue to inject the new primaryGroupName + handle the new switch(id, source) signature plus update:open/create-dashboard/delete-dashboard events. Views.vue backdrop wiring updated from @click to @close.
Adds admin UI for setting per-group default dashboards. Conflicts in DashboardService.php docblock resolved in favour of HEAD's longer suppress-warning explanations. l10n: appended group-default keys to both en/nl JSON+JS bundles.
Adds POST /api/dashboards/active resolver endpoint, store mirroring, and substantial PHPStan cleanup. Conflict in routes.php resolved by moving setActiveDashboard from /api/dashboard/active to plural /api/dashboards/active so the route is matched before the singular wildcard. phpstan.neon merged: kept HEAD ResourceController rules plus branch's DataResponse rule for DashboardShareApiController.
Adds runtime gating (assertPersonalDashboardsAllowed) + 403 envelope for personal dashboard creation. Conflict in DashboardApiController resolved by putting admin gating BEFORE resolveCreateParams (per REQ-ASET-003 stable error envelope). AdminSettings.vue inject merged to keep injectedAllGroups/configuredGroups + add allowUserDashboards. Views.vue computed merged to keep sidebarGroupDashboards / sidebarUserDashboards plus add emptyStateDescription.
Adds admin drag-drop UI for group priority ordering plus the /api/admin/groups GET/POST endpoints. Routes appended after existing admin endpoints. l10n appended group-priority keys (deduped Saving…).
Adds primary group resolver + the visual primary-group label pill. Conflict in Views.vue resolved by keeping HEAD's NcButton sidebar toggle AND adding the branch's primary-group label component alongside; CSS preserved both .mydash-sidebar-toggle and .mydash-primary-group-label declarations.
Broaden inline regex to include vue-select / vue-multiselect / floating-vue so Vite transforms their .css side-effect imports through cssNoop. WorkspaceApp.spec.js was failing because Views.vue indirectly pulled NcSelect via the new TextDisplayForm registration, and the optimizer was bypassing the resolveId hook for asset CSS.
Adds POST /api/dashboards/{uuid}/fork that deep-copies a visible
dashboard into a personal copy (REQ-DASH-020..022). Conflict in
DashboardService.php docblock + use statements resolved by
combining HEAD's AdminTemplateService param with the branch's
IUserManager + IFactory imports. Routes ordered with fork before
the personal-scope wildcard. Test files extended with the new
mocks (userManager, l10nFactory) needed by the service constructor.
api.js gained both setGroupDashboardDefault (HEAD) and forkDashboard
(branch).
Adds image widget renderer/form/registry entry plus the readFileAsDataUrl FileReader helper to resourceService.js. Conflicts in widgetRegistry.js, resourceService.js, and l10n files resolved by keeping HEAD's text widget entry and adding the image entry alongside; appended image-widget l10n keys to both en/nl bundles.
Adds link widget renderer/form/registry entry plus POST /api/files/create backend (FileService + admin-configured extension allow-list). Conflicts resolved by: - widgetRegistry.js: keep all four entries (label, text, image, link) - AdminSettingsService.php: keep allowUserDashboards default false (REQ-ASET-003 admin-must-opt-in) and add linkCreateFileExtensions to the response envelope; preserve both getGroupOrder/setGroupOrder and normaliseExtensions methods - AdminSetting.php: keep both KEY_GROUP_ORDER and KEY_LINK_CREATE_FILE_EXTENSIONS - MyDashAdmin.php: combine all five constructor params (initialState, groupManager, widgetService, settingMapper, settingsService, dashboardService, fileService) - l10n: appended link-button widget keys to en/nl bundles.
Adds the nc-widget renderer + form (NcDashboardWidget.vue, NcDashboardForm.vue) for proxying Nextcloud Dashboard widgets inside MyDash. Conflicts resolved by: - widgetRegistry.js: keep all five entries (label, text, image, link, nc-widget) and all six imports - widgetRegistry.spec.js: keep both REQ-TXT-* tests and the new REQ-WDG-018 nc-widget tests - widgets/spec.md: keep REQ-WDG-015..017 (right-click menu) and add REQ-WDG-018..021 (nc-widget type, two-mode rendering, display modes, API call shape) as a continuous block - l10n: appended nc-widget keys to en/nl bundles.
…tGroupOrder param type - FileController forwards a typed-exception HTTP status into JSONResponse the same way ResourceController does; reuse the existing suppression pattern. - AdminSettingsService::setGroupOrder takes admin-supplied input; widening the @param to array<int, mixed> lets the runtime defensive checks (is_string, empty-string) stay reachable for static analysis.
Integrate 48 dev commits (Vue dep bump, ADR-005 exception envelope hardening,
ADR-004 nextcloud-vue swap, .gitignore harmonisation, sbom workflow change,
duplicate phpstan/phpcs/notifier/factory fixes) into the spec-driven feature
branch. Resolved 78 file-level conflicts:
- Vue widgets / forms / IconPicker / DashboardSwitcherSidebar / Views.vue:
kept ours (richer feature implementations the agents wrote), rewrote
six `@nextcloud/vue` imports to `@conduction/nextcloud-vue` per ADR-004.
- Controllers (DashboardApi, PageController, etc.): merged dev's ADR-005
('do not leak raw exception messages') envelopes on top of our new
endpoints (fork, setActiveDashboard, group-shared CRUD); kept our
routes.php union plus dev's per-route requirements.
- Services (DashboardService, FileService, InitialStateBuilder, etc.):
kept ours (every dev fix was either duplicated by our agents or
superseded by our richer implementation).
- DashboardSettingsService / TemplateService / Notifier / DashboardFactory:
merged docblocks; kept our naming + ADR-005 logger flow.
- l10n: regenerated en.js / nl.js from the union of resolved en.json /
nl.json strings.
- package.json: kept ours' gridstack ^12 (our DashboardGrid bump),
vitest ^1.6 (299/299 tests pass on this version), dompurify ^3.4 (dev),
superset of test:* scripts.
- package-lock.json: regenerated via `npm install`.
- sbom.cdx.json removed (dev moved SBOM to release-asset only).
- Tests: kept ours throughout (new coverage for fork, active-resolution,
initial-state, file-create, resource-uploads, user-deletion).
- phpstan.neon: kept our granular path-scoped suppressions plus dev's
general IRegistrationContext::registerRepairStep / IUser::getLanguage
ignores.
- .gitignore: dev's harmonised version with our research-notes block
appended.
- composer.json: dev's hardened phpstan (no fallback) with our
phpunit-unit.xml configuration.
Post-merge follow-ups required to make every quality gate green:
PHP:
- DashboardServiceForkTest, DashboardServiceAllowFlagTest,
DashboardServiceDefaultFlagTest, DashboardServiceActiveResolutionTest:
swap constructor args from `userManager`/`l10n` to
`adminTemplateService`/`l10nFactory` to match the merged
DashboardService signature; replace direct `IUserManager::get` stubs
with `AdminTemplateService::getUserGroupIdsFor` (REQ-TMPL-013 single
source of truth).
- DashboardShareApiControllerFollowupsTest: pass userManager + groupManager
to the controller constructor (signature gained these in dev).
- DashboardApiController::fork: emit `{status: 'success', dashboard: ...}`
envelope (was returning bare {dashboard: ...} via ResponseHelper); test
expects the dev-branch shape.
- AdminTemplateService: drop the duplicate resolvePrimaryGroup /
pickFirstMatch / generateUuid block left behind by the merge — the
helper-based versions at the top of the class are the canonical ones.
- AdminSettingsServiceTest: drop the duplicate getGroupOrder /
setGroupOrder block (the second copy targeted dev's `findByKey`
implementation; we kept the `getValue` version, so its tests stay).
- AdminSettingsService getGroupOrder: dedupe entries to match
test expectations (was leaking duplicates).
- Migration Version001006: rename $closure to $schemaClosure to satisfy
Psalm's ParamNameMismatch against IMigrationStep.
- UserDeletedListener: wrap a 131-char docblock line under the 125-char
PHPCS limit.
Frontend:
- vitest.config.js: alias `@nextcloud/axios` and
`@conduction/nextcloud-vue` to local stubs so transitive imports stop
crashing the worker (axios 2.6 is ESM-only, conduction-vue ships a CJS
bundle that requires .vue files which Vite cannot transform).
- tests/vitest/stubs/{nextcloud-axios,conduction-nextcloud-vue}.js: new
minimal stubs — actual HTTP / component behaviour is still covered via
per-test `vi.mock(...)`.
- package.json: pin `@nextcloud/axios` to ~2.5.2 (last CJS-compatible
release) so the prod build keeps working.
- src/utils/widgetPlacement.js: rephrase comment so the
REQ-GRID-014 grep guard (literal `grid.addWidget(`) doesn't false-fire.
Result: `composer check:strict` passes, `npm test` 299/299 passes,
`npm run build` produces both bundles, `openspec validate --all --strict`
passes 45/47 (two pre-existing dev-branch failures untouched).
Contributor
Quality Report — ConductionNL/mydash @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 501/501 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-05-02 15:54 UTC
Download the full PDF report from the workflow artifacts.
rubenvdlinde
added a commit
that referenced
this pull request
May 2, 2026
…ssertions Picks up the net-new test additions from the user's WIP that weren't on dev: testGroupSharedConstants verifies REQ-DASH-011/012/013 constants (TYPE_GROUP_SHARED, DEFAULT_GROUP_ID, SOURCE_USER/GROUP/DEFAULT) are exposed on the entity, and the existing testTypeConstants gains three PERMISSION_* assertions. Both files also gain SPDX-FileCopyrightText + SPDX-License-Identifier inside the existing docblock per project rules. All other WIP-vs-dev divergence is the user's branch lagging behind dev (working tree contains older versions of files that PR #99 already updated) — no further integration work needed.
rubenvdlinde
added a commit
that referenced
this pull request
May 2, 2026
Adds @playwright/test config, admin-login global setup, tiny PNG upload fixture, and per-suite README with triage results. Per-spec auth handled once via storageState harvested from a real browser login (driving the NC /login form is the only stable cross-version method since CSRF token rotation shifted across NC 28/29/30). Discovered: only 4 e2e spec files (14 cases) made it onto development via PR #99 — not the 24 originally scoped, since most feature branches carried only the canonical label-widget.spec.ts template. First-run results: 0 passing / 14 failing — every failure is the same 500 from PageController::index() because the mounted custom_apps/mydash copy predates InitialStateBuilder (introduced by initial-state-contract on dev). Specs themselves are sound; runner needs the install switched to development. Documented in tests/e2e/README.md; no .skip()/.fixme() markers added because hiding the 500 would mask the real env signal.
rubenvdlinde
added a commit
that referenced
this pull request
May 3, 2026
feat: implement 24 OpenSpec proposals (multi-scope, widgets, resources, runtime shell)
rubenvdlinde
added a commit
that referenced
this pull request
May 3, 2026
…ssertions Picks up the net-new test additions from the user's WIP that weren't on dev: testGroupSharedConstants verifies REQ-DASH-011/012/013 constants (TYPE_GROUP_SHARED, DEFAULT_GROUP_ID, SOURCE_USER/GROUP/DEFAULT) are exposed on the entity, and the existing testTypeConstants gains three PERMISSION_* assertions. Both files also gain SPDX-FileCopyrightText + SPDX-License-Identifier inside the existing docblock per project rules. All other WIP-vs-dev divergence is the user's branch lagging behind dev (working tree contains older versions of files that PR #99 already updated) — no further integration work needed.
rubenvdlinde
added a commit
that referenced
this pull request
May 3, 2026
Adds @playwright/test config, admin-login global setup, tiny PNG upload fixture, and per-suite README with triage results. Per-spec auth handled once via storageState harvested from a real browser login (driving the NC /login form is the only stable cross-version method since CSRF token rotation shifted across NC 28/29/30). Discovered: only 4 e2e spec files (14 cases) made it onto development via PR #99 — not the 24 originally scoped, since most feature branches carried only the canonical label-widget.spec.ts template. First-run results: 0 passing / 14 failing — every failure is the same 500 from PageController::index() because the mounted custom_apps/mydash copy predates InitialStateBuilder (introduced by initial-state-contract on dev). Specs themselves are sound; runner needs the install switched to development. Documented in tests/e2e/README.md; no .skip()/.fixme() markers added because hiding the 500 would mask the real env signal.
rubenvdlinde
added a commit
that referenced
this pull request
May 3, 2026
…erge The wave2 spec-proposals branch was authored before PR #99 archived our 24 OpenSpec proposals. Merging brought back the active versions of those 24 folders even though their archived copies (2026-05-02-*) are now the canonical state. Removing the actives so triage of the 41 net-new wave2 proposals isn't muddied. openspec validate --all --strict: still passing post-cleanup.
rubenvdlinde
added a commit
that referenced
this pull request
May 3, 2026
…erge The wave2 spec-proposals branch was authored before PR #99 archived our 24 OpenSpec proposals. Merging brought back the active versions of those 24 folders even though their archived copies (2026-05-02-*) are now the canonical state. Removing the actives so triage of the 41 net-new wave2 proposals isn't muddied. openspec validate --all --strict: still passing post-cleanup.
Merged
4 tasks
rubenvdlinde
added a commit
that referenced
this pull request
May 3, 2026
Resolves the SHA-based merge ambiguity caused by the wave2 filter-branch rewrite. Wave2 textually contains all of PRs #99/#100/#101/#102, and all 60 conflicting code/spec/l10n files were superset-resolved by taking wave2's version (HEAD). Brings in PR #103 (.github/CODEOWNERS) and PR #105 (openspec/changes/mydash-adopt-or-abstractions/) cleanly since neither overlapped with wave2 work.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements all 24 active OpenSpec proposals end-to-end. Each proposal was applied → verified → archived → committed on its own feature branch, then all 24 merged into this single integration branch with conflicts resolved.
42 commits = 24 proposal commits + 16 merge commits + 2 stabilisation commits, on top of
feature/replica-spec-proposals.What's in here
Foundation
widget-add-edit-modal— registry-driven AddWidgetModal + useWidgetForm composableinitial-state-contract— typed PHP→JS provide/inject contract with CI lint guardsMulti-scope dashboards
multi-scope-dashboards— group_shared scope, groupId column, visible-to-user resolver, 6 new routesdefault-dashboard-flag— admin UI for setting group defaultactive-dashboard-resolution— 7-step precedence chain + POST /api/dashboards/activeallow-personal-dashboards-flag— runtime gating + personal_dashboards_disabled 403 envelopefork-current-as-personal— POST /api/dashboards/{uuid}/fork deep-copygroup-priority-order— admin drag-drop UI for group orderinggroup-routing— AdminTemplateService::resolvePrimaryGroup resolver + grep guardDashboard UI
dashboard-icons— Dashboard.icon column + IconRenderer (migration Version001006Date20260502000000)custom-icon-upload-pattern— IconPicker dual-mode + isCustomIconUrl discriminatordashboard-switcher-sidebar— slide-in left navigation (3 sections)runtime-shell— page-level orchestrator (REQ-SHELL-001..007), 4-region shellResources
resource-uploads— POST /api/resources + JS wrapper + stable error enveloperesource-serving— GET /resource/{filename} + GET /api/resources listingsvg-sanitisation— DOM-whitelist sanitiser, 22 XSS vectors coveredWidgets
label-widget(pilot)text-display-widget— DOMPurify-sanitisedimage-widget— file-upload + URL + 4 object-fit modeslink-button-widget— typed actionType enum + POST /api/files/create + internal action registrync-dashboard-widget-proxy— proxies any NC dashboard widget (native callback OR API fallback)widget-context-menu— right-click Edit/Remove popoverwidget-collision-placement— placeNewWidget helper as single placement authorityGrid
responsive-grid-breakpoints— gridstack v10→v12 bump + 4 breakpoints (1400/1100/768/480 → 12/8/4/1)Migration version note
Version001006Date20260502000000(dashboard-icons) andVersion001008Date20260502000000(multi-scope). Collides with two uncommitted migrations onfeature/replica-spec-proposals(Version001006Date20260430130000,Version001007Date20260501100000) — renumber when integrating.Quality gates
npm test: 299 / 299 passingcomposer check:strict: PHPCS, PHPMD, Psalm, PHPStan all cleannpm run build: clean (only pre-existing asset-size warnings)openspec validate --all --strict: 21 / 0 failed (20 capability specs + 1 unrelated active change)Three judgment calls worth reviewer eyes
switch(id, source)+update:open).server.deps.inlineto includevue-select/vue-multiselect/floating-vue— NcSelect (via TextDisplayForm) was leaking.csspaths past thecssNoopplugin. Pure test-infra.FileController.php; widenedsetGroupOrder's@param string[]toarray<int, mixed>so phpstan trusts runtime defensive checks.Test plan
developmentbasefeature/replica-spec-proposals