Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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/*"
5 changes: 5 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
11 changes: 7 additions & 4 deletions docs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Binary file added docs/static/img/og-mydash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions docs/static/llms.txt
Original file line number Diff line number Diff line change
@@ -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
63 changes: 22 additions & 41 deletions openspec/changes/active-dashboard-resolution/tasks.md
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 21 additions & 37 deletions openspec/changes/custom-icon-upload-pattern/tasks.md
Original file line number Diff line number Diff line change
@@ -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 `<img :src="name" :alt="alt">` when `isCustomIconUrl(name)`, else `<component :is="getIconComponent(name)" :size="size">`; 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 `<img>` for URL inputs
- [ ] Task 5: Build `src/components/Dashboard/IconPicker.vue` with both a `<select>` of registry names AND a file-upload input visible at once; on select-change update `v-model` with the option string; on file-select POST to the `resource-uploads` endpoint then update `v-model` with the returned URL
- [ ] Task 6: Picker UX — render a 24×24 live preview via `IconRenderer`; surface loading + error states for upload (spinner during POST, visible error on non-2xx); on upload error leave the previous `v-model` value unchanged (no clobber)
- [ ] Task 7: Refactor call sites — `DashboardSwitcher`, admin dashboard list/CRUD, link-button widget icon, tile editor — replace ad-hoc icon-or-image branches with `<IconRenderer>` and use `<IconPicker>` in the create/edit forms
- [ ] Task 8: Grep verification — no remaining `v-if="iconUrl"` / inline `isCustomIconUrl` branches outside `IconRenderer.vue` and `IconPicker.vue`
- [ ] Task 9: Update the `icon` field docblock on `lib/Db/Dashboard.php` AND the `tileIcon` field docblock on `lib/Db/WidgetPlacement.php` to state the column may hold a registry name, a `/apps/mydash/resource/...` URL, or NULL
- [ ] Task 10: Playwright — switch from built-in to uploaded icon, preview swaps `<svg>` → `<img>`, value persists after save; reverse swap also persists; workspace mixing both kinds of icons across dashboards renders cleanly with no console errors
- [ ] Task 11: Quality gates — ESLint clean on changed `.vue`/`.js`; `composer check:strict` clean for the touched PHP docblock changes
- [ ] Task 12: Stylelint clean on any new component `<style>` blocks; `npm run build` produces no new warnings

## 2. IconRenderer component
## Verification

- [ ] 2.1 Create `src/components/Dashboard/IconRenderer.vue` accepting `name`, `alt`, and `size` props
- [ ] 2.2 Branch internally: `<img :src="name" :alt="alt">` when `isCustomIconUrl(name)`, else `<component :is="getIconComponent(name)" :size="size">`
- [ ] 2.3 Default `alt` to a non-empty string (consumer-supplied label, falling back to dashboard/widget name)
- [ ] 2.4 Vitest: rendering branches by input type (built-in name → svg, URL → img, null → default svg)
- [ ] 2.5 Vitest: `alt` prop is propagated to the rendered `<img>` for URL inputs
`openspec validate` exits clean. No legacy inline branches survive the refactor (per Task 8); editor flows round-trip both icon kinds.

## 3. IconPicker component
## Tests (company-wide ADR-009)

- [ ] 3.1 Create `src/components/Dashboard/IconPicker.vue` with both a `<select>` of registry names AND a file-upload input visible at the same time
- [ ] 3.2 On select change: emit/update `v-model` with the chosen option string
- [ ] 3.3 On file select: POST the file to the `resource-uploads` endpoint, then update `v-model` with the returned URL string
- [ ] 3.4 Render a 24×24 live preview of the current value via `IconRenderer`
- [ ] 3.5 Surface loading and error states for the upload (spinner during POST, visible error when the request fails or the response is non-2xx)
- [ ] 3.6 On upload error: leave the previous `v-model` value unchanged (do not clobber)
Vitest per Tasks 2 + 4; Playwright per Task 10. No new backend surface.

## 4. Refactor existing call sites
## Documentation (company-wide ADR-010)

- [ ] 4.1 Replace ad-hoc icon-or-image branches in `DashboardSwitcher` with `<IconRenderer>`
- [ ] 4.2 Replace branches in the admin dashboard list / CRUD UI with `<IconRenderer>` and use `<IconPicker>` in the create/edit forms
- [ ] 4.3 Replace branches in the link-button widget icon and the tile editor with `<IconRenderer>` and `<IconPicker>`
- [ ] 4.4 Grep test: no remaining `v-if="iconUrl"` / inline `isCustomIconUrl` branches outside `IconRenderer.vue` and `IconPicker.vue`
PHP docblock updates per Task 9; changelog entry covering the unified renderer/picker pattern.

## 5. Documentation
## i18n (company-wide ADR-005)

- [ ] 5.1 Update the `icon` field docblock on `lib/Db/Dashboard.php` to state that the column may hold either a registry name, a `/apps/mydash/resource/...` URL, or NULL
- [ ] 5.2 Update the `tileIcon` field docblock on `lib/Db/WidgetPlacement.php` with the same convention

## 6. End-to-end tests

- [ ] 6.1 Playwright: open dashboard editor, switch from a built-in icon to an uploaded one, verify the preview swaps from `<svg>` to `<img>` and the value persists after save
- [ ] 6.2 Playwright: switch back from an uploaded icon to a built-in one, verify the preview swaps back and the value persists
- [ ] 6.3 Playwright: render a workspace where multiple dashboards mix built-in and uploaded icons, confirm all render correctly with no console errors

## 7. Quality

- [ ] 7.1 ESLint clean on all changed `.vue` and `.js` files
- [ ] 7.2 `composer check:strict` clean for the touched PHP entity docblock changes (PHPCS, PHPMD, Psalm, PHPStan)
No user-facing strings added — picker surfaces existing labels via consumer-supplied props.
Loading
Loading