diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..3390c9e5
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ cooldown:
+ default-days: 1
+ include:
+ - "*"
+ exclude:
+ - "@conduction/*"
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 00000000..3942d348
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,5 @@
+# Supply-chain hardening: reject any npm package published less than
+# 24h ago. Compromised first-party-Conduction packages are excluded via
+# Dependabot cooldown (.github/dependabot.yml); for fresh @conduction/*
+# releases, override per-install with `npm install --min-release-age=0`.
+min-release-age=1
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index 472b8a78..c481cb95 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -144,6 +144,7 @@ const config = createConfig({
/* themeConfig is shallow-merged into the preset's defaults
(colorMode + navbar + footer). prism + mermaid land alongside. */
themeConfig: {
+ image: 'img/og-mydash.png',
prism: {
theme: require('prism-react-renderer/themes/github'),
darkTheme: require('prism-react-renderer/themes/dracula'),
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 4f898166..fb7659e2 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -8,7 +8,7 @@
"name": "mydash-docs",
"version": "0.0.0",
"dependencies": {
- "@conduction/docusaurus-preset": "^2.6.1",
+ "@conduction/docusaurus-preset": "^3.6.0",
"@docusaurus/core": "^3.10.0",
"@docusaurus/preset-classic": "^3.10.0",
"@docusaurus/theme-mermaid": "^3.10.0",
@@ -2045,10 +2045,13 @@
}
},
"node_modules/@conduction/docusaurus-preset": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-2.6.1.tgz",
- "integrity": "sha512-hOkS2XAj3p1wfaZWjvIqKzwC5cbTSLUPm+DJxUp7TePbnU37kaHGegjLjfVOnQ312G8an+0M5VWIhmg/YhRVyA==",
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/@conduction/docusaurus-preset/-/docusaurus-preset-3.6.0.tgz",
+ "integrity": "sha512-gjnM6G+Cjx1WKniMhbQDYjgje0J4nqjeArKUHxisEdsY62fp8mfEzP1NXeD/2F2aYa+eHShCHeYGUM9jTl6yKQ==",
"license": "EUPL-1.2",
+ "bin": {
+ "validate-ai-baseline": "bin/validate-ai-baseline.mjs"
+ },
"peerDependencies": {
"@docusaurus/core": "^3.0.0",
"@docusaurus/preset-classic": "^3.0.0",
diff --git a/docs/package.json b/docs/package.json
index 939628f6..ffa71b61 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -6,6 +6,8 @@
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
+ "postbuild": "validate-ai-baseline",
+ "validate:ai-baseline": "validate-ai-baseline",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
@@ -15,7 +17,7 @@
"ci": "npm ci --legacy-peer-deps && npm run build"
},
"dependencies": {
- "@conduction/docusaurus-preset": "^2.6.1",
+ "@conduction/docusaurus-preset": "^3.6.0",
"@docusaurus/core": "^3.10.0",
"@docusaurus/preset-classic": "^3.10.0",
"@docusaurus/theme-mermaid": "^3.10.0",
diff --git a/docs/static/img/og-mydash.png b/docs/static/img/og-mydash.png
new file mode 100644
index 00000000..8c2ea8b6
Binary files /dev/null and b/docs/static/img/og-mydash.png differ
diff --git a/docs/static/llms.txt b/docs/static/llms.txt
new file mode 100644
index 00000000..ea366578
--- /dev/null
+++ b/docs/static/llms.txt
@@ -0,0 +1,23 @@
+# MyDash
+
+> MyDash is an open-source dashboard app for the Nextcloud workspace.
+
+MyDash is an open-source dashboard app for the Nextcloud workspace. It lets each user compose multiple personal dashboards from drag-and-drop widgets and shortcut tiles on a responsive GridStack canvas, and supports both the v1 and v2 Nextcloud Dashboard widget APIs out of the box. Administrators can ship pre-configured templates to user groups, pin compulsory widgets, and set conditional visibility rules based on group, time of day, or date range. Released under EUPL-1.2 and maintained by Conduction since 2019.
+
+## Docs
+
+- [Documentation](https://mydash.conduction.nl/docs/intro): main entry, including tutorials, user guide, and admin guide.
+- [API reference](https://mydash.conduction.nl/api): OpenAPI documentation.
+
+## Optional
+
+- [Install](https://www.conduction.nl/install): self-host on your Nextcloud instance.
+- [Source code](https://github.com/ConductionNL/mydash): repository and issue tracker.
+- [App page](https://www.conduction.nl/apps/mydash): product positioning on conduction.nl.
+
+## Contact
+
+- Email: info@conduction.nl
+- Web: https://www.conduction.nl
+- GitHub: https://github.com/ConductionNL
+- Conduction B.V. · KvK 76741850 · Lauriergracht 14h, Amsterdam, Netherlands
diff --git a/openspec/changes/active-dashboard-resolution/tasks.md b/openspec/changes/active-dashboard-resolution/tasks.md
index 0f7e067b..892e1209 100644
--- a/openspec/changes/active-dashboard-resolution/tasks.md
+++ b/openspec/changes/active-dashboard-resolution/tasks.md
@@ -1,52 +1,33 @@
# Tasks — active-dashboard-resolution
-## 1. Backend resolver
+## Tasks
-- [ ] 1.1 Add user pref key constant `DashboardService::ACTIVE_DASHBOARD_UUID_PREF_KEY = 'active_dashboard_uuid'`
-- [ ] 1.2 Add `DashboardService::resolveActiveDashboard(string $userId, ?string $primaryGroupId): ?array` returning `['dashboard' => Dashboard, 'source' => 'user'|'group'|'default']` or `null`
-- [ ] 1.3 Implement the 7-step precedence chain exactly as REQ-DASH-018 lists (saved pref → group default → default-group default → first-in-group → first-in-default-group → first personal → null)
-- [ ] 1.4 Implement stale-preference auto-clear: when the saved UUID is not in `findVisibleToUser` results, call `IConfig::deleteUserValue` (write-on-read) and emit a `LoggerInterface::warning` line
-- [ ] 1.5 Resolver MUST be otherwise pure (no other side effects on read)
+- [ ] Task 1: Define `DashboardService::ACTIVE_DASHBOARD_UUID_PREF_KEY = 'active_dashboard_uuid'` and `DashboardService::resolveActiveDashboard($userId, ?$primaryGroupId): ?array` returning `['dashboard' => Dashboard, 'source' => 'user'|'group'|'default']` or `null`
+- [ ] Task 2: Implement the 7-step precedence chain in the resolver exactly as REQ-DASH-018 lists (saved pref → group default → default-group default → first-in-group → first-in-default-group → first personal → null); resolver MUST be otherwise pure (no other read-side effects)
+- [ ] Task 3: Stale-preference auto-clear — when the saved UUID is not in `findVisibleToUser` results, call `IConfig::deleteUserValue` (write-on-read) and emit a `LoggerInterface::warning` line
+- [ ] Task 4: Add `DashboardService::setActivePreference($userId, $uuid): void` that writes via `IConfig::setUserValue` (or deletes when uuid is empty); no existence check on write per REQ-DASH-019
+- [ ] Task 5: Add `DashboardController::setActiveDashboard()` mapped to `POST /api/dashboards/active` with `#[NoAdminRequired]` — accepts `{uuid: string}`, returns HTTP 200 `{status:'success'}`; route registered in `appinfo/routes.php`
+- [ ] Task 6: Wire `WorkspaceController` to call `resolveActiveDashboard($currentUserId, $primaryGroupId)` on first render and push `activeDashboardId` (or `''` when null) + `dashboardSource` into initial-state JSON via `IInitialState`; null resolver result renders the empty-state UI
+- [ ] Task 7: Frontend — mirror the 7-step precedence in `useDashboardsStore.resolveActive()` for client-side `switchDashboard()` flows after store mutations
+- [ ] Task 8: Add `switchDashboard(uuid)` store action — updates store state and POSTs to `/api/dashboards/active` fire-and-forget (failure surfaces as a toast but does not block the UI)
+- [ ] Task 9: Add the empty-state component shown when `resolveActive()` returns null, including a "Create your first dashboard" affordance
+- [ ] Task 10: PHPUnit — table-driven test exercising all 7 precedence steps + permutations (saved pref / no pref; group default present/absent; default-group default present/absent; first-in-group present/absent; first personal present/absent; nothing-at-all); cross-group preference invalidated correctly
+- [ ] Task 11: PHPUnit — stale preference cleared exactly once per request (not on every visibility check); `setActivePreference` accepts non-existent UUIDs without erroring; empty-string uuid clears the preference (REQ-DASH-019)
+- [ ] Task 12: Playwright — empty state shows on a fresh user with no dashboards; `switchDashboard` POSTs the new UUID and the next page load picks it up; stale preference (dashboard deleted between sessions) silently falls through to step 2 — no error toast
+- [ ] Task 13: Quality gates — `composer check:strict`, ESLint+Stylelint, OpenAPI/Postman regen for the new endpoint, `nl`+`en` i18n for the empty-state copy + new error strings, SPDX-in-docblock on new PHP, all 10 hydra-gates green; document in `design.md` why stale prefs are cleaned per-request rather than via cron
-## 2. Backend write endpoint
+## Verification
-- [ ] 2.1 Add `DashboardService::setActivePreference(string $userId, string $uuid): void` that writes to `IConfig::setUserValue` (or deletes when uuid is empty string)
-- [ ] 2.2 Add `DashboardController::setActiveDashboard()` mapped to `POST /api/dashboards/active` with `#[NoAdminRequired]` — accepts `{uuid: string}`, returns HTTP 200 `{status: 'success'}`
-- [ ] 2.3 Register the route in `appinfo/routes.php`
-- [ ] 2.4 No existence check on write (per REQ-DASH-019 scenario "no existence check on write")
+`openspec validate` exits clean. Resolver returns the correct dashboard for all 7 precedence rows in the test matrix; stale prefs self-heal.
-## 3. Workspace integration
+## Tests (company-wide ADR-009)
-- [ ] 3.1 Update `WorkspaceController` to call `resolveActiveDashboard($currentUserId, $primaryGroupId)` on first render
-- [ ] 3.2 Push `activeDashboardId` (or `''` when null) and `dashboardSource` into initial-state JSON via `IInitialState`
-- [ ] 3.3 When resolver returns null, ensure the page renders the empty-state UI (per REQ-DASH-018 scenario "empty state")
+PHPUnit per Tasks 10–11; Playwright per Task 12. Newman/Postman updated for the new endpoint.
-## 4. Frontend
+## Documentation (company-wide ADR-010)
-- [ ] 4.1 Mirror the 7-step precedence in `useDashboardsStore.resolveActive()` for client-side `switchDashboard()` flows after store mutations
-- [ ] 4.2 Add `switchDashboard(uuid)` action that updates store state and POSTs to `/api/dashboards/active` (fire-and-forget; surface failure as a toast but do not block UI)
-- [ ] 4.3 Empty-state component shown when `resolveActive()` returns null — includes a "Create your first dashboard" affordance
+Changelog entry covering the active-dashboard resolution chain + the empty-state UX.
-## 5. PHPUnit tests
+## i18n (company-wide ADR-005)
-- [ ] 5.1 Table-driven test covering all 7 steps with permutations (saved pref / no pref; group default present / absent; default-group default present / absent; first-in-group present / absent; first personal present / absent; nothing-at-all)
-- [ ] 5.2 Stale preference cleared exactly once per request (not on every visibility check)
-- [ ] 5.3 Cross-group preference invalidated correctly — alice pref points to a dashboard whose group she no longer belongs to
-- [ ] 5.4 `setActivePreference` accepts non-existent UUIDs without erroring (REQ-DASH-019 scenario "no existence check on write")
-- [ ] 5.5 Empty-string uuid clears the preference (REQ-DASH-019 scenario "empty uuid clears the preference")
-
-## 6. Playwright tests
-
-- [ ] 6.1 Empty state shows on a fresh user with no dashboards (any type, any group)
-- [ ] 6.2 Switching dashboard fires `POST /api/dashboards/active` with the new UUID and the next page load picks up the saved choice
-- [ ] 6.3 Stale preference (dashboard deleted between sessions) silently falls through to step 2 of the chain — no error toast
-
-## 7. Quality gates
-
-- [ ] 7.1 `composer check:strict` (PHPCS, PHPMD, Psalm, PHPStan) passes — fix any pre-existing issues encountered along the way
-- [ ] 7.2 ESLint + Stylelint clean on touched Vue/JS files
-- [ ] 7.3 Update generated OpenAPI spec / Postman collection so external API consumers see the new endpoint
-- [ ] 7.4 `i18n` keys for new error messages and the empty-state copy in both `nl` and `en` per the i18n requirement
-- [ ] 7.5 SPDX headers on every new PHP file (inside the docblock per the SPDX-in-docblock convention) — gate-spdx must pass
-- [ ] 7.6 Run all 10 `hydra-gates` locally before opening PR
-- [ ] 7.7 Stale prefs are cleaned per request, not via cron — document the rationale in `design.md` if added (see proposal Notes)
+`nl_NL` + `en_US` for the empty-state copy and new error strings.
diff --git a/openspec/changes/custom-icon-upload-pattern/tasks.md b/openspec/changes/custom-icon-upload-pattern/tasks.md
index bfd841bb..42c5a258 100644
--- a/openspec/changes/custom-icon-upload-pattern/tasks.md
+++ b/openspec/changes/custom-icon-upload-pattern/tasks.md
@@ -1,48 +1,32 @@
# Tasks — custom-icon-upload-pattern
-## 1. Discriminator module
+## Tasks
-- [ ] 1.1 Add `isCustomIconUrl(name)` export to `src/constants/dashboardIcons.js` returning `true` only for non-null strings beginning with `'/'` or `'http'`
-- [ ] 1.2 Update `getIconComponent(name)` to return `null` when `isCustomIconUrl(name)` is true (must NOT fall back to `DEFAULT_ICON` for URLs)
-- [ ] 1.3 Add Vitest covering the truth table: URL prefixes (`/apps/...`, `http://`, `https://`), registry names (`Star`, `ViewDashboard`), and falsy inputs (`null`, `undefined`, `''`)
-- [ ] 1.4 Add Vitest asserting `getIconComponent` returns `null` for a URL input AND returns `DEFAULT_ICON` for an unknown registry name (REQ-ICON-001 still holds)
+- [ ] Task 1: Add `isCustomIconUrl(name)` to `src/constants/dashboardIcons.js` returning `true` only for non-null strings beginning with `/` or `http`; update `getIconComponent(name)` to return `null` for URL inputs (must NOT fall back to `DEFAULT_ICON` for URLs)
+- [ ] Task 2: Vitest discriminator coverage — URL prefixes (`/apps/...`, `http://`, `https://`), registry names (`Star`, `ViewDashboard`), falsy inputs (`null`, `undefined`, `''`); `getIconComponent` returns `null` for URL input AND `DEFAULT_ICON` for unknown registry name (REQ-ICON-001 still holds)
+- [ ] Task 3: Build `src/components/Dashboard/IconRenderer.vue` accepting `name`, `alt`, `size` — branches `` when `isCustomIconUrl(name)`, else ``; default `alt` falls back to consumer-supplied label (dashboard/widget name)
+- [ ] Task 4: Vitest renderer coverage — rendering branches by input type (built-in name → svg, URL → img, null → default svg); `alt` prop propagated to the rendered `` for URL inputs
+- [ ] Task 5: Build `src/components/Dashboard/IconPicker.vue` with both a `