All notable changes to this project will be documented in this file.
- Dashboard quota limits (
dashboard-quota-limits): two admin-configurable numeric quotas —maxDashboardsPerUserandmaxWidgetsPerDashboard— with server-side fail-closed enforcement on every user-initiated creation path (create, duplicate, fork, import, add-widget / add-tile) via a singleQuotaService. Both default to0(unlimited), so behaviour is unchanged on upgrade. Exceeding a limit returns HTTP 409 with a structuredquota_exceededbody; the dashboards list response carries an additivequotaenvelope so the UI disables the "Add dashboard" / "Add widget" affordances at the limit with a localised tooltip. Lowering a limit never deletes or hides existing data (grandfathering);allowMultipleDashboards = falseis honoured as an effective limit of 1 (most-restrictive-wins); admin template rollout and compulsory-widget pushes are exempt via an explicit provisioning bypass.
- Admin group-management UI (
multi-scope-dashboardsTask 13): the admin-facing group-shared CRUD UI was filed as the dedicated follow-up changeopenspec/changes/admin-group-management/so themulti-scope-dashboardsAPI + read endpoint slice could ship without UX dependencies. The follow-up adds an eighth Beheer tab (Group dashboards) wrapping the existing/api/dashboards/group/...endpoints; no new backend semantics.
-
GridStack bumped to v12.x (REQ-GRID-013):
gridstackdependency updated from10.3.1to^12.2.1. Downstream consumers that pin an exact GridStack version or import internal GridStack APIs should update their constraints accordingly. ThecolumnOpts.breakpoints/moveScaleAPI surface used by this app is stable across v10+. -
Responsive grid breakpoints (REQ-GRID-007): the dashboard grid now reflows proportionally at four explicit viewport widths — 12 columns (≥ 1400 px), 8 columns (≥ 1100 px), 4 columns (≥ 768 px), 1 column (≥ 480 px or below). The
moveScalealgorithm preserves relative widget widths on breakpoint changes. Cell height is 60 px; inter-cell margin is 8 px. All geometry constants are centralised inuseGridManager.jsand mirrored to the--launchpad-cell-heightCSS custom property at init time.
- SVG upload sanitisation (REQ-RES-009..013): all SVG uploads are now
passed through a server-side DOM whitelist sanitiser (
SvgSanitiser) before persistence. Allowed elements (24) and attributes (50) are declared as conservative whitelists;<script>,<foreignObject>,<iframe>,on*event handlers,javascript:/data:href values, and CSSexpression()/url(data:)constructs are stripped unconditionally. The parser usesLIBXML_NONETto block external entity and DTD fetches (XXE protection). Unparseable SVG or fully-stripped documents return HTTP 400{error: 'invalid_svg'}and no file is written. The 5 MB size cap is measured against the sanitised bytes, not the original upload. Existing on-disk SVGs are not retroactively re-sanitised.
-
Widget right-click context menu in edit mode (REQ-WDG-015..017, issue #36): right-clicking any widget placement in edit mode now opens a small popover at the cursor with three actions — Edit (reopens
AddWidgetModalfor the placement), Remove (calls the placement-delete path of REQ-WDG-005), and Cancel (no-op close). The popover is viewport-clamped so it stays fully on-screen near right and bottom edges (min-width: 150 px,z-index: 10000). View mode is untouched — right-click falls through to the browser's native context menu. Auto-close on outside click via a single document-level listener managed byuseGridManager.json mount/unmount (no listener leak). i18n:nl_NL+en_USfor Edit, Remove, Cancel. Keyboard navigation (Up/Down/Enter/Esc) is deferred to a follow-up change. -
Nextcloud Dashboard widget proxy (
nc-widgetplacement type, REQ-WDG-018..021): any installed Nextcloud Dashboard widget (Mail, Calendar, Talk, Weather, etc.) can now be embedded as a grid cell. Two rendering modes: (1) native — the widget's own bundle registers its callback viaOCA.Dashboard.register; the bridge hands the render container to it for full feature parity with/dashboard; (2) API fallback — items are fetched viaGET /api/widgets/items?widgets[]=<id>&limit=7and rendered as a flat 7-item card list. A 200 ms × 15 retries (~3 s) poll detects late-loading bundles and upgrades the cell to native mode without flicker. Display modes:vertical(32 px icons, flex-column list) andhorizontal(120 px cards, 44 px icons, flex-row wrap). Widget picker usesNcWidgetGridPicker(icon cards populated from thewidgetsinitial-state list). i18n:nl_NL+en_US. -
Dashboard-switcher sidebar (REQ-SWITCH-001..007): a fixed-position slide-in left navigation panel (
DashboardSwitcherSidebar.vue) lists every dashboard visible to the user, grouped into three labelled sections in fixed order — primary group, default group, and personal ("My Dashboards"). Empty sections collapse entirely. Each row renders its icon via the sharedIconRenderer(no inline URL branching). Clicking a row emitsupdate:open(false)thenswitch(id, source), wheresourceis load-bearing (drives the correct API endpoint in the runtime shell). Personal rows expose a cog menu (DashboardRowActions) with Edit / Configure / Add custom widget / Delete entries; the delete action emitsdelete-dashboard(id)without triggering a switch. A dedicatedNcButtoncard below the personal list serves as the+ New Dashboardaffordance whenallowUserDashboards === true. Slide-in animation is CSS-only (transform: translateX(-100%) ↔ translateX(0)over 0.25 s ease). CompanionSidebarBackdrop.vueprovides a click-to-close overlay. A persistentSidebarFooterwith "Powered by Sendent / Conduction" brand logos and a Documentation link is pinned viaposition: sticky; bottom: 0. Wired inViews.vue; Esc closes the sidebar; all string labels translated (en_US+nl_NL). -
Per-user RSS / Atom dashboard feeds (REQ-FEED-001..009): users can now opt-in to a personal feed of their accessible dashboards via
GET /api/feed/token. New routesPOST /api/feed/token/regenerate(atomic rotate),DELETE /api/feed/token(idempotent soft-revoke), and the publicGET /feed/{token}.xml(no Nextcloud auth — gated only by the opaque token). Tokens are 32 random bytes encoded URL-safe base64 (~43 chars, 256-bit entropy, non-enumerable). Feed output is RSS 2.0 by default;Accept: application/atom+xml(or the?format=atomquery fallback) switches to Atom 1.0. Visibility reusesDashboardService::getVisibleToUser()so private dashboards never leak to public feed consumers; item count is capped at the admin-tunablelaunchpad.feed_item_cap(default 50). Newoc_launchpad_feed_tokenstable withUNIQUE(user_id)enforces the one-token-per-user rotation invariant. -
Unified add/edit modal (
widget-add-edit-modal): a singleAddWidgetModal.vuenow handles both "add a widget" and "edit a widget" flows. Per-widget edit dialogs are removed; the unified modal is driven bywidgetRegistry.js— adding a new widget type only requires a registry entry. TheuseWidgetFormcomposable owns state management (resetForm,loadEditingWidget,validate,assembleContent). Modal closes on Cancel, backdrop click, and Esc without submitting (REQ-WDG-013). Type-switching resets form state with no cross-type field leakage (REQ-WDG-010). Per-type sub-forms exposevalidate(): string[]gating the action button (REQ-WDG-012). The toolbar dropdown and grid renderer both consult the same registry (REQ-WDG-014).
- GridStack bumped from
^10.3.1to^12.2.1(resolved 12.6.0). Major version bump bundled with the responsive-grid-breakpoints change. TheGridStack.initsignature, thechangeevent payload, theengine.nodesaccessor, theremoveWidget(el, removeDOM)call, and theenable()/disable()lifecycle methods used byDashboardGrid.vueare unchanged across the v10 -> v12 jump, so no caller-side breakage was observed during the bump. Downstream forks pinning a narrowergridstackrange will need to widen their dependency.
-
Responsive grid breakpoints (REQ-GRID-007 / REQ-GRID-012 / REQ-GRID-013): the GridStack instance now reflows proportionally at four viewport widths instead of staying fixed-12-column on narrow screens. Breakpoints
[{w:1400,c:12},{w:1100,c:8},{w:768,c:4},{w:480,c:1}]with themoveScalelayout algorithm. Geometry constants (CELL_HEIGHT = 60,GRID_MARGIN = 8,BREAKPOINTS) live insrc/composables/useGridManager.jsas the single source of truth and are mirrored to the CSS custom property--launchpad-cell-heightat init time socalc()expressions stay in sync. Cell height moved from the previously documented 80 px to 60 px to better support multi-row info widgets; flip theCELL_HEIGHTconstant in the composable (single edit) if a denser/looser default is preferred. -
Initial-state contract (REQ-INIT-001..006):
lib/Service/InitialStateBuilder.phpcentralises the per-page initial-state payload pushed via Nextcloud'sIInitialStateservice. The matching JS reader atsrc/utils/loadInitialState.jsreturns a typed default-filled object for the workspace and admin pages. Both sides stamp / validate a_schemaVersionconstant; deploy skew between PHP and JS surfaces as a console warning at runtime.Adding, removing, or renaming a key requires four coordinated edits in the same commit:
- update the spec Data Model in
openspec/specs/initial-state-contract/spec.md, - bump
INITIAL_STATE_SCHEMA_VERSIONinlib/Service/InitialStateBuilder.phpANDsrc/utils/loadInitialState.js, - add (or remove) the typed setter in the PHP builder and the matching
entry in the JS reader's
PAGE_KEYStable, - update the controller(s) that call the builder.
CI guards (
composer lint:initial-state,npm run lint:initial-state) forbid directIInitialState::provideInitialState()calls outside the builder and directloadState('launchpad', ...)calls outside the reader. - update the spec Data Model in
- Initial app structure
- Basic Nextcloud integration