diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index db6aef8..c1e0b49 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -14,13 +14,13 @@ jobs:
app-name: planix
php-version: "8.3"
php-test-versions: '["8.3", "8.4"]'
- nextcloud-test-refs: '["stable31", "stable32"]'
+ nextcloud-test-refs: '["stable31", "stable32", "stable33"]'
enable-psalm: true
enable-phpstan: true
enable-phpmetrics: true
enable-frontend: true
enable-eslint: true
enable-phpunit: true
- enable-newman: true
+ enable-newman: false
additional-apps: '[{"repo":"ConductionNL/openregister","app":"openregister","ref":"main"}]'
enable-sbom: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3dfdd09
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,56 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.2.1] - 2026-04-03
+
+### Added
+- Project list view with search (debounced 300 ms) and status filter chips (Active / Archived / Completed)
+- Project list item shows color swatch, icon, title, member count badge, and status badge
+- Empty state with "No projects yet" prompt and separate "No results" state for filtered lists
+- Project creation modal dialog with title, description, color, and icon fields
+- Inline form validation: submit disabled until title is provided; "Title is required" message on blur
+- Loading spinner on submit button during project creation; dialog locked while saving
+- Automatic creation of 4 default kanban columns on new project (configurable via admin settings)
+- Project board shell view at `/projects/:id` with header, gear icon, and "View Backlog" link
+- Project backlog placeholder view at `/projects/:id/backlog` with breadcrumb navigation
+- Project settings sidebar with three sections: Details, Members, and Danger Zone
+- Immediate metadata reflection: title/color/icon changes appear in page header without reload
+- Member management: add members via Nextcloud user search; remove with assigned-task warning
+- Leave project flow with last-member protection dialog
+- Archive project action with inline confirmation in Danger Zone
+- Project deletion cascade: removes columns, tasks, and time entries in dependency order
+- Delete confirmation dialog showing task count before destructive action
+- Access control: project list filtered to current user's memberships; non-member direct URL shows access-denied state
+- Projects navigation entry in sidebar (NcAppNavigationItem, positioned first)
+- Routes for `/projects`, `/projects/:id`, and `/projects/:id/backlog`
+- Full Dutch (nl) translations for all project-related strings
+
+### Fixed
+- Webpack `output.publicPath` overridden to `/apps-extra/planix/js/` — chunks were loading from wrong path (`/apps/planix/js/`) causing 404 on all code-split bundles
+- Settings sidebar save now includes `members` field in PATCH request, preventing member list from being cleared on title/description updates
+- `NcChip` import fixed to use component path directly (`@nextcloud/vue/dist/Components/NcChip.js`) — not exported from main index in v8.16.0
+- OpenRegister register updated to `publicWrite: true` / `publicRead: true` on app upgrade via repair step
+
+## [0.2.0] - 2026-04-03
+
+### Added
+- Define task schema with all properties (title, description, status, priority, project, etc.)
+- Define project schema with all properties (title, description, status, color, icon, members, etc.)
+- Define column schema for kanban boards (title, project, order, wipLimit, color, type)
+- Define timeEntry schema for time tracking (task, user, duration, date, description)
+- Define label schema for categorization (title, color, description)
+- Add seed data: 5 labels (Bug, Feature, Docs, Design, Infrastructure)
+- Add seed data: 3 projects (Client Portal v2, Infrastructure Migration, Onboarding Automation)
+- Add seed data: 12 columns (4 per project: To Do, In Progress, Review, Done)
+- Add seed data: 5 tasks with realistic assignments and priorities
+- Add seed data: 3 time entries referencing task seeds
+- Register repair step for automatic schema import on app install/upgrade
+- Bump register version to 0.2.0
+
+### Changed
+- Remove placeholder example schema from planix_register.json
+- Remove example schema references from DeepLinkRegistrationListener
diff --git a/README.md b/README.md
index 2b20572..5292514 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
---
-Planix is a Kanban-based project and task management app for Nextcloud, built as a thin client on OpenRegister. It manages projects, tasks, kanban boards with WIP limits, backlogs, and time entries for internal dev and IT teams.
+Planix is a Kanban-based project and task management app for Nextcloud, built as a thin client on OpenRegister. It manages projects, tasks, kanban boards with WIP limits, backlogs, and time entries — giving internal dev and IT teams a focused workflow tool built directly into their Nextcloud environment. Unlike Nextcloud Deck (which lacks backlog management, time tracking, and WIP limits), Planix closes the gap between Deck's simplicity and Jira's complexity.
> **Pre-wired for [OpenRegister](https://github.com/ConductionNL/openregister)** — all data is stored as OpenRegister objects. If your app needs OpenRegister, install it first. If not, remove the dependency from `appinfo/info.xml` and `openspec/app-config.json`.
@@ -26,14 +26,27 @@ _Add screenshots here once the app has a UI._
## Features
-Features are defined in [`openspec/specs/`](openspec/specs/). See the [roadmap](openspec/ROADMAP.md) for planned work.
+Features are defined in [`openspec/specs/`](openspec/specs/). See the [roadmap](openspec/ROADMAP.md) for planned work. Full feature documentation is in [`docs/features/`](docs/features/).
-### Core
-- **Dashboard** — Personal overview page with key information at a glance
-- **Admin Settings** — Configurable settings panel for administrators
+### Task & Project Management
+- **Projects** — Create and manage project containers with team members, colors, and kanban boards
+- **Tasks** — Full task lifecycle with priorities, labels, assignees, due dates, and status tracking (open → in progress → done)
+- **Backlog** — Task queue for unscheduled work with sorting and filtering; tasks promote to the board via drag-and-drop
+- **Kanban Board** — Visual board per project with configurable columns, drag-and-drop cards, and WIP limits
+
+### Personal Productivity
+- **Dashboard & My Work** — Personal landing page with KPI cards (open, overdue, in progress, done today), recent projects, and tasks due this week; My Work groups all assigned tasks by urgency
+- **Time Tracking** — Estimate effort per task, log multiple time entries (duration + date + description), and review logged time in a personal timesheet view
+
+### Integration
+- **Procest Integration** — Link tasks and projects to Procest cases via `caseReference` (project) and `zaakUuid` (task) fields; case badges appear in the project list and task detail
+
+### Admin & Configuration
+- **Admin Settings** — Configurable admin panel for default columns, label management, and OpenRegister initialization; uses `CnVersionInfoCard` and `CnSettingsSection` components
+- **User Settings** — Per-user notification preferences and default view selection via `NcAppSettingsDialog`
### Supporting
-- **OpenRegister Integration** — Pre-wired data layer using OpenRegister objects
+- **OpenRegister Integration** — All data stored as OpenRegister objects; no custom database tables
- **Quality Pipeline** — PHPCS, PHPMD, Psalm, PHPStan, ESLint, Stylelint
## Architecture
@@ -46,15 +59,19 @@ graph TD
A --> E[Nextcloud Search]
```
-_Update this diagram during `/app-explore` sessions as the architecture evolves._
+See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the full architecture breakdown.
### Data Model
-| Object | Description |
-|--------|-------------|
-| _(define your data objects here)_ | — |
+| Object | Schema.org Type | Description |
+|--------|----------------|-------------|
+| Task | `schema:Action` / `schema:PlanAction` | Core unit of work — title, description, assignee, due date, priority, status, estimates |
+| Project | `schema:CreativeWork` | Container for tasks and kanban board — teams, members, metadata |
+| Column | `schema:DefinedTerm` | Kanban board column — configurable stages with WIP limits |
+| TimeEntry | `schema:QuantitativeValue` | Effort log — task, user, duration (minutes), date, description |
+| Label | `schema:DefinedTerm` | Cross-project tag — name, color, description |
-_Data model is defined using OpenRegister schemas. See [`openspec/specs/`](openspec/specs/) for feature-level design decisions and [`openspec/architecture/`](openspec/architecture/) for architectural decisions._
+Data model is defined using OpenRegister schemas. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for full entity definitions and standards mapping, [`openspec/specs/`](openspec/specs/) for feature-level requirements, and [`openspec/architecture/`](openspec/architecture/) for architectural decisions.
### Directory Structure
@@ -94,8 +111,8 @@ planix/
| Dependency | Version |
|-----------|---------|
-| Nextcloud | 28 – 33 |
-| PHP | 8.1+ |
+| Nextcloud | 31 – 33 |
+| PHP | 8.3+ |
| Node.js | 20+ |
| [OpenRegister](https://github.com/ConductionNL/openregister) | latest |
diff --git a/appinfo/info.xml b/appinfo/info.xml
index e8b09ed..ccce0fd 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -6,7 +6,7 @@
PlanixFlow-based kanban project and task management for Nextcloud dev and IT teamsFlow-gebaseerd kanban project- en taakbeheer voor Nextcloud dev- en IT-teams
-
- 0.1.0
- agpl
+ 0.2.1
+ euplConductionPlanix
@@ -52,7 +52,7 @@ Vrij en open source onder de EUPL-1.2-licentie.
https://raw.githubusercontent.com/ConductionNL/planix/main/img/app-store.svg
-
+
@@ -69,4 +69,13 @@ Vrij en open source onder de EUPL-1.2-licentie.
OCA\Planix\Settings\AdminSettingsOCA\Planix\Sections\SettingsSection
+
+
+
+ OCA\Planix\Repair\InitializeSettings
+
+
+ OCA\Planix\Repair\InitializeSettings
+
+
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index f54daee..049c944 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -114,7 +114,8 @@ A task is the core unit of work in Planix. Tasks belong to a project, can be pla
| `description` | string | `DESCRIPTION` | `schema:description` | — | No | — |
| `status` | enum | `STATUS` | `schema:actionStatus` | `status` | Yes | `open` |
| `priority` | enum: low, normal, high, urgent | `PRIORITY` (1-9) | — | — | No | `normal` |
-| `project` | reference | `RELATED-TO` (parent project) | — | `zaakUuid` (optional) | No | — |
+| `project` | reference | `RELATED-TO` (parent project) | — | — | No | — |
+| `zaakUuid` | string (UUID) | — | — | Procest case UUID (cross-app bridge) | No | null |
| `column` | reference | — | — | — | No | null (backlog) |
| `columnOrder` | integer | — | `schema:position` | — | No | 0 |
| `assignedTo` | string (user UID) | `ATTENDEE` | `schema:agent` | `toegewezenAanGebruikersnaam` | No | — |
@@ -404,19 +405,21 @@ Schemas MUST be defined in `lib/Settings/planix_register.json` using OpenAPI 3.0
The configuration is imported via `ConfigurationService::importFromApp()` in the repair step.
-## 5. Open Research Questions
+## 5. Resolved Research Questions
-1. **CalDAV VTODO sync** — Should Planix offer two-way sync with the Nextcloud Tasks app (CalDAV)? The Tasks app already supports VTODO. A sync would let tasks appear in mobile Calendar apps. Current decision: store `calendarEventUid` field and implement one-way export in V1.
+All research questions below have been resolved. Decisions are recorded in the app-specific ADRs under [`openspec/architecture/`](../openspec/architecture/).
-2. **Sub-task depth** — OpenProject supports unlimited hierarchy; GitHub Issues has no hierarchy. One level of sub-tasks (task → sub-task) is planned. Should we support epic → task → sub-task (three levels)? Current decision: one level only in MVP.
+1. **CalDAV VTODO sync** — **One-way export to Nextcloud Tasks app in V1.** The `calendarEventUid` field on Task stores the VTODO UID. Planix writes tasks to CalDAV; changes made in the Tasks app are not synced back. Two-way sync was rejected due to data model mismatch (Tasks app has no concept of projects, columns, or WIP limits).
-3. **Procest task bridge** — When a Procest case creates tasks in Planix, who owns the project? Does each case get its own Planix project, or do case tasks appear in an existing project? Current decision: a dedicated Planix project per case (with `caseReference` linking back).
+2. **Sub-task depth** — **One level only (task → sub-task), all tiers.** Projects serve the "epic" grouping role. No formal Epic entity. This matches Linear and Plane (1 level + containers) and avoids Jira's 3-level complexity.
-4. **Time tracking scope** — Should time entries be per-task only, or also per-project (overhead, meetings)? Current decision: per-task only in MVP. Overhead tracking in V1.
+3. **Procest task bridge** — **Configurable — Procest UI decides.** Procest's UI presents a project picker when creating tasks for a case. It may create a new project (with `caseReference`) or add tasks to an existing project (with `zaakUuid` on each task). Planix has no routing mechanism — it reads whatever Procest wrote to OpenRegister. See ADR-003.
-5. **GitHub/GitLab sync** — Dev teams want tasks linked to commits and PRs. Should Planix natively sync with GitHub Issues or GitLab Issues? Current decision: out of scope for MVP; targeted for V1 via OpenConnector.
+4. **Time tracking scope** — **Per-task only, forever.** `TimeEntry.task` is always required. Overhead work (meetings, planning) is tracked as tasks — not as project-level time entries. This keeps the data model simple and queryable. No special cases needed. See ADR-004.
-6. **WIP limit enforcement** — Should WIP limits be hard (block task drag) or soft (visual warning only)? Current decision: soft limits with prominent visual warning (industry consensus: hard limits cause friction).
+5. **GitHub/GitLab sync** — **Via OpenConnector in V1.** Planix owns no GitHub/GitLab API code. OpenConnector handles the external API mapping (GitHub Issues ↔ Planix tasks). This avoids duplicating integration logic across Conduction apps.
+
+6. **WIP limit enforcement** — **Soft limits with visual warning.** Column header turns orange/red when over the WIP limit; counter shows e.g. `4/3`. Drag is never blocked. Industry consensus: hard limits cause friction and workarounds (Jira, Kanboard both use soft limits).
## 6. References
diff --git a/docs/DESIGN-REFERENCES.md b/docs/DESIGN-REFERENCES.md
index c986ff6..2d02c1d 100644
--- a/docs/DESIGN-REFERENCES.md
+++ b/docs/DESIGN-REFERENCES.md
@@ -389,6 +389,59 @@ CnSettingsSection (name="OpenRegister Setup", ...)
**Note**: Uses `NcAppSettingsDialog` (NOT `NcDialog`). Triggered from the `?` / gear icon in the Planix top navigation bar. See `openspec/specs/nextcloud-app/spec.md` for the authoritative pattern.
+### 3.9 Timesheet View
+
+```
+┌────────────────────────────────────────────────────────────────────┐
+│ PLANIX › My Timesheet │
+├────────────────────────────────────────────────────────────────────┤
+│ [This week ▾] Mar 24 – Mar 30, 2026 Total: 14h 30m │
+├────────────────────────────────────────────────────────────────────┤
+│ │
+│ 📅 Monday, Mar 24 2h 45m │
+│ ──────────────────────────────────────────────────────────────── │
+│ Fix auth token expiry bug API Gateway 0h 45m [✎] [✕] │
+│ Write deployment checklist Infra Migration 2h 00m [✎] [✕] │
+│ │
+│ 📅 Tuesday, Mar 25 4h 00m │
+│ ──────────────────────────────────────────────────────────────── │
+│ Fix auth token expiry bug API Gateway 1h 30m [✎] [✕] │
+│ Migrate to PostgreSQL pool API Gateway 2h 30m [✎] [✕] │
+│ │
+│ 📅 Wednesday, Mar 26 3h 45m │
+│ ──────────────────────────────────────────────────────────────── │
+│ Review PR #42 — rate limiting API Gateway 0h 45m [✎] [✕] │
+│ Add CSRF token validation API Gateway 3h 00m [✎] [✕] │
+│ │
+│ 📅 Thursday, Mar 27 · 2h 30m │ 📅 Friday, Mar 28 · 1h 30m │
+│ ──────────────────────────────┤──────────────────────────────── │
+│ Pagination for /list 1h 00m │ Write OpenAPI 3.0 spec 1h 30m │
+│ Update error format 1h 30m │ │
+│ │
+├────────────────────────────────────────────────────────────────────┤
+│ Week total: 14h 30m [+ Log time] │
+└────────────────────────────────────────────────────────────────────┘
+```
+
+**Component hierarchy**:
+```
+CnListViewLayout (title="My Timesheet")
+├─ date range selector (This week / Last week / This month / Custom)
+├─ week total badge
+├─ CnDataTable (grouped by date)
+│ ├─ date group header (date label + daily total)
+│ └─ rows: task title (link) | project badge | duration | [edit] [delete]
+└─ week total footer + [+ Log time] CTA
+```
+
+**Key UX patterns** (sourced from Leantime, OpenProject, Harvest):
+- Date grouped rows with daily subtotals — scan work patterns at a glance
+- Inline edit and delete per row — correct mistakes without navigating away
+- Task title is a clickable link → task detail view (back returns to timesheet)
+- Week view with mini day columns for at-a-glance density when days are sparse
+- Weekly total prominently displayed in header and footer
+- "Log time" CTA always visible — encourages consistent logging
+
---
## 4. Updated Feature Counts (after design review)
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index d845788..d826b3d 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -12,7 +12,7 @@ Nextcloud Deck is the only native Nextcloud kanban app — and it is fundamental
| Name | Status | Key Features | Gaps |
|------|--------|-------------|------|
-| **Nextcloud Deck** | Bundled, active (v1.17, Feb 2026) | Kanban boards, cards, labels, file attachment, mobile apps, Circles sharing | No backlog, no time tracking, no GitHub sync, no WIP limits, 6500+ DB queries per board, no multi-view |
+| **Nextcloud Deck** | Bundled, active | Kanban boards, cards, labels, file attachment, mobile apps, Circles sharing | No backlog, no time tracking, no GitHub sync, no WIP limits, 6500+ DB queries per board, no multi-view |
| **Nextcloud Tasks** | Bundled, active | CalDAV/VTODO task sync, due dates, priorities, sub-tasks | No project grouping, no kanban board, no time tracking, no team collaboration |
| **Nextcloud Deck Extended** | Community, low activity | Minor Deck extensions | Unmaintained, Deck fork approach |
@@ -22,9 +22,9 @@ Nextcloud Deck is the only native Nextcloud kanban app — and it is fundamental
| Name | GitHub ★ | Positioning | Key Features | Weaknesses |
|------|----------|------------|-------------|------------|
-| **Plane** | 38.6k | Linear alternative, GitHub-native | Kanban, list, calendar views; cycles (sprints); GitHub/GitLab sync; modules/epics; issue templates | No time tracking, no Gantt, no backlog management, SaaS-first |
+| **Plane** | 40k+ | Linear alternative, GitHub-native | Kanban, list, calendar views; cycles (sprints); GitHub/GitLab sync; epics (archivable); project subscribers; workspace-level kanban/calendar; Plane AI (web search, chart generation); Slack routing; Jira import | No time tracking, no Gantt, SaaS-first |
| **Taiga** | ~10k | Agile teams, Scrum+Kanban | Scrum boards, backlog, burndown charts, epics, user stories, wiki, swimlanes, WIP limits | No GitHub PR integration, complex UI, no time tracking |
-| **Vikunja** | 3.6k | Self-hosted flexible | Kanban, list, Gantt, table views; recurring tasks; email notifications | No sprint/cycle, no GitHub integration, no time tracking, smaller community |
+| **Vikunja** | 5k+ (1.0 stable Jan 2026) | Self-hosted flexible | Kanban, list, Gantt (overhauled v2.2), table views; recurring tasks; task duplication; email notifications | No sprint/cycle, no GitHub integration, no time tracking |
| **WeKan** | 14.6k | Trello alternative | Kanban boards, automation rules, swimlanes, 70+ languages, Trello import | Kanban-only, no agile features, no time tracking, Meteor stack |
| **Kanboard** | 9.5k | Minimalist kanban | WIP limits, query language, LDAP, GitHub webhooks | Kanban-only, minimal by design, no time tracking, no backlog |
| **Leantime** | — | Goal-driven PM | Kanban, Gantt, time tracking, time blocking, whiteboard, sprints, neuro-inclusive | Limited GitHub integration, smaller community, complex for small teams |
@@ -89,6 +89,10 @@ No dedicated Dutch government task management tools were identified. OpenProject
| Column color coding | **MVP** | Visual workflow clarity |
| Swimlanes (group cards by assignee or priority) | **V1** | Workload visibility |
| Board filter (by assignee, label, priority) | **MVP** | Focus on relevant work |
+| View toggle on board (kanban ↔ list) | **MVP** | Users need a dense list view alongside kanban for large projects (Linear, Plane, Jira pattern) |
+| Task card hover quick-actions (assign, set due date, change status) | **MVP** | Assign/update without opening detail — Jira, Asana, Trello pattern |
+| Task count per column (shown in column header) | **MVP** | Instant awareness of column load; present in every kanban tool |
+| Overdue task highlight (red border/badge on card) | **MVP** | Urgency signal visible without opening task — Jira, Linear, Asana pattern |
| Collapsed columns | **V1** | Space management |
| Blocked task indicators | **V1** | Dependency visibility |
| Card quick-edit (inline title/status change) | **V1** | Speed of use |
@@ -304,14 +308,14 @@ No dedicated Dutch government task management tools were identified. OpenProject
| Risk | Severity | Mitigation |
|------|---------|------------|
| Nextcloud Deck owns the kanban mindshare in the NC ecosystem | High | Differentiate on time tracking + backlog + dev integration — Deck explicitly excludes these |
-| Plane (38.6k ★) moves faster than we can | Medium | Focus on Nextcloud-native features that Plane will never build; don't compete on Plane's turf |
+| Plane (40k+ ★) moves faster than we can | Medium | Focus on Nextcloud-native features that Plane will never build; don't compete on Plane's turf |
| Small initial team → scope creep | Medium | MVP is strictly kanban + backlog + time tracking; defer everything else |
| Drag-and-drop kanban is UX-complex in Vue 2 | Medium | Use a proven drag library (vue-draggable/SortableJS); budget time for polish |
| OpenRegister performance at scale (many tasks) | Medium | Lean on OpenRegister's pagination and indexing; document pagination patterns early |
## 6. Recommended Feature Set Summary
-### MVP (40 features)
+### MVP (44 features)
Flow-based kanban with backlog and time tracking for dev/IT teams on Nextcloud. Covers the gap left by Nextcloud Deck.
1. Task CRUD (title, description, status, priority)
@@ -338,64 +342,68 @@ Flow-based kanban with backlog and time tracking for dev/IT teams on Nextcloud.
22. Task card anatomy (title, assignee, due date, labels, priority)
23. Column color coding
24. Board filter (by assignee, label, priority)
-25. Backlog view (tasks without a column)
-26. Drag task from backlog to board column
-27. Backlog sorting (by priority, due date, created date)
-28. Backlog search and filter
-29. Personal dashboard (landing page with KPI cards)
-30. My Work view (tasks assigned to me, across all projects)
-31. Overdue task list
-32. Tasks due this week
-33. Recently updated tasks
-34. Notes/comments on tasks (ICommentsManager)
-35. File attachments on tasks (CnObjectSidebar)
-36. Activity stream on task (Audit Trail tab)
-37. Shared project access (multi-user)
-38. Project progress (tasks done / total)
-39. Procest bridge (case → project/task)
-40. NcAppSettingsDialog (notify_assigned, notify_due_reminder, default_view)
-
-### V1 (25 additional features, continuing from 40)
+25. View toggle on board (kanban ↔ list)
+26. Task card hover quick-actions (assign, due date, status)
+27. Task count in column header
+28. Overdue task highlight (red border) on card
+29. Backlog view (tasks without a column)
+30. Drag task from backlog to board column
+31. Backlog sorting (by priority, due date, created date)
+32. Backlog search and filter
+33. Personal dashboard (landing page with KPI cards)
+34. My Work view (tasks assigned to me, across all projects)
+35. Overdue task list
+36. Tasks due this week
+37. Recently updated tasks
+38. Notes/comments on tasks (ICommentsManager)
+39. File attachments on tasks (CnObjectSidebar)
+40. Activity stream on task (Audit Trail tab)
+41. Shared project access (multi-user)
+42. Project progress (tasks done / total)
+43. Procest bridge (case → project/task)
+44. NcAppSettingsDialog (notify_assigned, notify_due_reminder, default_view)
+
+### V1 (25 additional features, continuing from 44)
More collaboration, reporting, dev integrations, and advanced kanban.
-41. Sub-tasks (one level deep)
-42. Task dependencies (blocks / is-blocked-by)
-43. Recurring tasks
-44. Project milestones
-45. Project templates
-46. Swimlanes (group by assignee or priority)
-47. Collapsed columns
-48. Blocked task indicators
-49. Card quick-edit (inline title/status change)
-50. Bulk select and move tasks from backlog
-51. Backlog item ordering (manual drag-and-drop rank)
-52. Backlog statistics (count, overdue, unassigned)
-53. Project time report (estimated vs logged)
-54. Team timesheet (admin, all users, export CSV)
-55. Timer (start/stop, auto-log)
-56. Time tracking export (CSV)
-57. Cumulative flow diagram
-58. Team workload report (tasks per user)
-59. Throughput chart (tasks/week)
-60. @mention users in comments
-61. Talk integration (per-task conversation)
-62. Activity feed on dashboard
-63. CalDAV/VTODO export (sync to Nextcloud Tasks)
-64. GitHub/GitLab sync (via OpenConnector)
-65. Import from Nextcloud Deck
-
-### Enterprise (10 additional features, continuing from 65)
+45. Sub-tasks (one level deep)
+46. Task dependencies (blocks / is-blocked-by)
+47. Recurring tasks
+48. Project milestones
+49. Project templates
+50. Swimlanes (group by assignee or priority)
+51. Collapsed columns
+52. Blocked task indicators
+53. Card quick-edit (inline title/status change)
+54. Bulk select and move tasks from backlog
+55. Backlog item ordering (manual drag-and-drop rank)
+56. Backlog statistics (count, overdue, unassigned)
+57. Project time report (estimated vs logged)
+58. Team timesheet (admin, all users, export CSV)
+59. Timer (start/stop, auto-log)
+60. Time tracking export (CSV)
+61. Cumulative flow diagram
+62. Team workload report (tasks per user)
+63. Throughput chart (tasks/week)
+64. @mention users in comments
+65. Talk integration (per-task conversation)
+66. Activity feed on dashboard
+67. CalDAV/VTODO export (sync to Nextcloud Tasks)
+68. GitHub/GitLab sync (via OpenConnector)
+69. Import from Nextcloud Deck
+
+### Enterprise (10 additional features, continuing from 69)
Governance, advanced analytics, and custom workflows.
-66. Task templates
-67. Custom task fields
-68. Project portfolios (cross-project grouping)
-69. Role-based project permissions (viewer/editor/admin)
-70. Cycle time tracking (column entry to exit)
-71. Overtime / budget alerts
-72. Task completion gamification
-73. Webhook outgoing (on task events)
-74. Import from CSV
-75. Advanced admin controls (max projects per user, role restrictions)
+70. Task templates
+71. Custom task fields
+72. Project portfolios (cross-project grouping)
+73. Role-based project permissions (viewer/editor/admin)
+74. Cycle time tracking (column entry to exit)
+75. Overtime / budget alerts
+76. Task completion gamification
+77. Webhook outgoing (on task events)
+78. Import from CSV
+79. Advanced admin controls (max projects per user, role restrictions)
diff --git a/docs/features/README.md b/docs/features/README.md
new file mode 100644
index 0000000..f7608d2
--- /dev/null
+++ b/docs/features/README.md
@@ -0,0 +1,16 @@
+# Planix — Features
+
+Planix is a project management app for Nextcloud, providing kanban boards, task management, time tracking, and project organization.
+
+## Features
+
+| Feature | Summary | Standards | Doc |
+|---------|---------|-----------|-----|
+| Register Schemas | 5 schemas (task, project, column, timeEntry, label) with seed data for project management | OpenRegister v0.2.10+ | [register-schemas.md](register-schemas.md) |
+| Projects | Full project management UI: list, create, settings sidebar, member management, archive/delete | Schema.org CreativeWork | [projects.md](projects.md) |
+| Tasks | Task lifecycle management: CRUD, priorities, labels, assignees, due dates, subtasks, status workflow | iCalendar VTODO (RFC 5545), Schema.org Action/PlanAction, VNG InterneTaak | [tasks.md](tasks.md) |
+| Kanban Board | Visual board per project: configurable columns, drag-and-drop cards, WIP limits, board filters, view toggle | Schema.org ItemList, DefinedTerm, Kanban Guide | [kanban-board.md](kanban-board.md) |
+| Dashboard & My Work | Personal landing page with KPI cards, recent projects, tasks due this week, and My Work task list grouped by urgency | Schema.org Action/PlanAction, Nextcloud Dashboard API | [dashboard.md](dashboard.md) |
+| Time Tracking | Per-task time estimates and manual time log entries; personal timesheet view with date grouping and range filters | Schema.org QuantitativeValue, iCalendar ESTIMATED-DURATION (RFC 7986) | [time-tracking.md](time-tracking.md) |
+| Admin & User Settings | Admin settings panel for default columns and label management; user settings dialog for notification preferences and default view | Nextcloud OCP\IAppConfig, OCP\IConfig, NcAppSettingsDialog | [admin-settings.md](admin-settings.md) |
+| Procest Integration | Link tasks and projects to Procest cases via caseReference and zaakUuid fields; case badges and links in the UI | VNG ZGW InterneTaak, Schema.org Action | [procest-integration.md](procest-integration.md) |
diff --git a/docs/features/admin-settings.md b/docs/features/admin-settings.md
new file mode 100644
index 0000000..18f1372
--- /dev/null
+++ b/docs/features/admin-settings.md
@@ -0,0 +1,51 @@
+# Admin & User Settings
+
+Configure Planix at the app level (admin) or customize personal preferences (user).
+
+## Admin Settings
+
+Accessible to Nextcloud administrators at **Administration → Planix**.
+
+### Sections
+
+**App version info** — `CnVersionInfoCard` shows the installed version, connection status to OpenRegister, and an "Update available" indicator with a link to the Nextcloud App Store when a newer version exists.
+
+**Default Project Configuration** — configure the column set that is applied when a new project is created. Columns can be added, renamed, reordered, and deleted. New projects created after saving the change will use the updated column set.
+
+**Label Management** — create, edit, and delete app-wide labels that are available across all projects. Each label has a title and a hex color.
+
+**OpenRegister Setup** — shows whether the Planix register and schemas are initialized in OpenRegister. An "Initialize register" button triggers the import if the register is not yet set up.
+
+### Access Control
+
+Only Nextcloud administrators can access the admin settings page. Non-admin users receive a 403 response if they navigate to the settings URL directly, and the section does not appear in their Settings navigation.
+
+## User Settings
+
+Accessible from the gear icon in the Planix navigation bar, the user settings dialog (`NcAppSettingsDialog`) lets each user configure their own preferences.
+
+### Notification Preferences
+
+| Setting | Default | Description |
+|---------|---------|-------------|
+| Notify when a task is assigned to me | On | Nextcloud notification on task assignment |
+| Remind me 1 day before a task's due date | On | Due-date reminder notification |
+
+### Display Preferences
+
+| Setting | Default | Description |
+|---------|---------|-------------|
+| Default view when opening a project | My Work | Chooses whether to open a project in My Work, Kanban, or Backlog view |
+
+All settings persist across browser sessions.
+
+## Standards
+
+- Nextcloud OCP\IAppConfig — admin settings storage
+- Nextcloud OCP\IConfig — user settings storage
+- NcAppSettingsDialog — user settings dialog component (NOT NcDialog)
+- CnVersionInfoCard, CnSettingsSection — admin settings layout components
+
+## Spec
+
+- [admin-user-settings spec](../../openspec/specs/admin-user-settings.md)
diff --git a/docs/features/dashboard.md b/docs/features/dashboard.md
new file mode 100644
index 0000000..bae9c13
--- /dev/null
+++ b/docs/features/dashboard.md
@@ -0,0 +1,38 @@
+# Dashboard & My Work
+
+The personal landing page of Planix — an overview of your work state and tasks assigned to you across all projects.
+
+## Overview
+
+When you open Planix, you land on the Dashboard. It shows KPI cards for your task counts, the five most recently active projects you are a member of, and tasks due within the next seven days. The My Work view provides a prioritized list of all tasks assigned to you, grouped by urgency.
+
+## Dashboard
+
+- **KPI cards** — four cards showing counts for Open tasks (open or in_progress), Overdue tasks (past due date, not done), In Progress, and Completed Today; each card is clickable and navigates to My Work with the corresponding filter applied
+- **Recent projects** — the five most recently active projects you belong to, each showing title, color/icon, task count, and a progress bar (done/total tasks)
+- **Due this week** — tasks assigned to you with a due date within the next seven days, sorted by due date; today's and tomorrow's due dates are highlighted
+
+**Empty states**: New users with no projects see a "No projects yet" empty state with a "Create project" button. KPI cards always show (set to 0 when no tasks exist).
+
+## My Work
+
+My Work shows all tasks assigned to you across all projects, grouped into three sections:
+
+| Group | Criteria |
+|-------|----------|
+| **Overdue** | Due date is in the past, status is not done (highlighted in red) |
+| **Due this week** | Due date is within the next 7 days, status is not done |
+| **Everything else** | Open tasks with no due date or a due date more than 7 days away |
+
+Within each group, tasks are sorted by priority (urgent → high → normal → low).
+
+From My Work you can update a task's status via an inline dropdown, or click the task title to navigate to the task detail view. The browser back button returns to My Work.
+
+## Standards
+
+- Schema.org Action / PlanAction — task aggregation query pattern
+- Nextcloud Dashboard API (OCP\Dashboard\IWidget) — optional widget integration (V1)
+
+## Spec
+
+- [dashboard-my-work spec](../../openspec/specs/dashboard-my-work.md)
diff --git a/docs/features/img/projects-create.png b/docs/features/img/projects-create.png
new file mode 100644
index 0000000..ff172a5
Binary files /dev/null and b/docs/features/img/projects-create.png differ
diff --git a/docs/features/img/projects-list.png b/docs/features/img/projects-list.png
new file mode 100644
index 0000000..37cdb6f
Binary files /dev/null and b/docs/features/img/projects-list.png differ
diff --git a/docs/features/img/projects-settings.png b/docs/features/img/projects-settings.png
new file mode 100644
index 0000000..e216519
Binary files /dev/null and b/docs/features/img/projects-settings.png differ
diff --git a/docs/features/kanban-board.md b/docs/features/kanban-board.md
new file mode 100644
index 0000000..fe7b645
--- /dev/null
+++ b/docs/features/kanban-board.md
@@ -0,0 +1,30 @@
+# Kanban Board
+
+The primary visual interface for a project — tasks as cards organized into configurable columns.
+
+## Overview
+
+Each project has exactly one kanban board. Columns represent stages in the workflow (e.g., To Do, In Progress, Review, Done). Users drag task cards between columns to update their status. WIP limits on columns provide visual warnings when a stage is overloaded. The board can be filtered by assignee, label, or priority to focus on relevant work.
+
+## Key Capabilities
+
+- **Columns** — create, rename, reorder, and delete columns per project; column order is configurable
+- **Default columns** — new projects are initialized with four columns (To Do, In Progress, Review, Done); defaults are configurable by admins
+- **WIP limits** — set a work-in-progress limit per column; column header shows a warning indicator when the limit is exceeded (soft limit — cards are never blocked)
+- **Task cards** — each card shows title, assignee avatar, due date, priority indicator, and label chips
+- **Overdue highlight** — tasks with a past due date show a red border/badge on the card
+- **Drag-and-drop** — drag a card to another column to update its column assignment and position
+- **Column task count** — column header shows the current task count alongside the WIP limit
+- **Board filter** — filter visible cards by assignee, label, or priority
+- **View toggle** — switch between kanban (card grid) and list view for the same project tasks
+- **Backlog access** — tasks without a column are in the backlog; a "View Backlog" link is available from the board view
+
+## Standards
+
+- Schema.org ItemList — kanban board (ordered list of columns)
+- Schema.org DefinedTerm — column (controlled vocabulary term within the board)
+- Kanban Guide (kanban.university) — WIP limit and flow practices
+
+## Spec
+
+- [kanban-board spec](../../openspec/specs/kanban-board.md)
diff --git a/docs/features/procest-integration.md b/docs/features/procest-integration.md
new file mode 100644
index 0000000..028444c
--- /dev/null
+++ b/docs/features/procest-integration.md
@@ -0,0 +1,49 @@
+# Procest Integration
+
+Link Planix tasks and projects to Procest cases for cross-app case-to-task workflows.
+
+## Overview
+
+Planix is a sister app to Procest (case management). When a Procest case requires task tracking on a kanban board, Planix provides the board. The integration is built on optional metadata fields — no direct API calls between apps are required in the MVP. Tasks and projects carry optional case reference fields that Procest can populate, and Planix displays them as read-only metadata.
+
+## Key Capabilities
+
+### Case Reference on Project
+
+- A project can carry a `caseReference` field containing a Procest case UUID
+- Projects with a case reference show a **"Case: {caseNumber}"** badge in the project list and project detail
+- The case reference can be set manually via the project edit form by entering a Procest case UUID
+
+### Task Case Link
+
+- A task can carry a `zaakUuid` field containing a Procest case UUID
+- Task detail shows a read-only **"Case"** field with a link to the Procest case when `zaakUuid` is set
+- The `zaakUuid` can be set manually via the task edit form
+
+### When the Bridge is Disabled
+
+If the Procest bridge toggle is disabled in admin settings, or Procest is not installed:
+- `caseReference` and `zaakUuid` fields are still stored and displayed as read-only metadata
+- No requests are sent to Procest
+- All Planix functionality remains fully available
+
+## VNG InterneTaak Mapping
+
+Tasks bridged from Procest follow the VNG InterneTaak field mapping:
+
+| Planix Task field | VNG InterneTaak field |
+|-------------------|-----------------------|
+| `title` | `gevraagdeHandeling` |
+| `assignedTo` | `toegewezenAanGebruikersnaam` |
+| `dueDate` | `gevraagdeDatum` |
+| `status` (done) | triggers `afhandelingsdatum` |
+| `completedAt` | `afhandelingsdatum` |
+
+## Standards
+
+- VNG ZGW InterneTaak (Klantinteracties) — case-task field mapping
+- Schema.org Action — task type annotation
+
+## Spec
+
+- [procest-integration spec](../../openspec/specs/procest-integration.md)
diff --git a/docs/features/projects.md b/docs/features/projects.md
new file mode 100644
index 0000000..e61c39a
--- /dev/null
+++ b/docs/features/projects.md
@@ -0,0 +1,35 @@
+# Projects
+
+Full project management surface for Planix — create, browse, configure, and delete projects.
+
+## Overview
+
+Projects are the top-level container in Planix. Each project groups a set of tasks on a kanban board, has a defined team (members), and optionally links to a Procest case. The `projects` change implements the complete project management UI on top of the OpenRegister data layer established by `register-schemas`.
+
+## Screenshots
+
+
+*Project list — browse and filter projects you are a member of*
+
+
+*Create project — title, description, color, and icon fields*
+
+
+*Project settings sidebar — Details, Members, and Danger Zone tabs*
+
+## Key Capabilities
+
+- **Project list** — browse all projects you are a member of; search (debounced 300 ms, client-side) and filter by status (Active / Archived / Completed)
+- **Create project** — modal dialog with title, description, color, icon fields; automatically creates 4 default columns (To Do / In Progress / Review / Done) on creation
+- **Project board shell** — detail view with header, gear icon, and "View Backlog" link; board view placeholder until kanban-board change is implemented
+- **Project settings sidebar** — three-tab sidebar: Details (title, description, color, icon), Members (add/remove/leave), Danger Zone (archive, delete)
+- **Member management** — search Nextcloud users, add to project, remove with assigned-task warning, leave with last-member protection
+- **Access control** — project list filtered to current user's memberships; direct URL navigation to non-member projects shows access-denied state
+- **Archive and delete** — archive hides from default list; delete cascades to columns, tasks, and time entries with confirmation dialog showing task count
+- **i18n** — full Dutch (nl) translation; all strings in `l10n/en.json` and `l10n/nl.json`
+- **Immediate metadata reflection** — sidebar saves update page header and project list without full reload
+
+## Standards
+
+- Schema.org CreativeWork (project as a creative work container)
+- iCalendar VTODO parent container reference
diff --git a/docs/features/register-schemas.md b/docs/features/register-schemas.md
new file mode 100644
index 0000000..f2f2f74
--- /dev/null
+++ b/docs/features/register-schemas.md
@@ -0,0 +1,36 @@
+# Register Schemas
+
+Defines and registers the complete Planix data model in OpenRegister.
+
+## Overview
+
+Planix uses OpenRegister to store its data model. The `planix_register.json` file defines 5 schemas and seed data that are automatically imported when the app is installed or upgraded.
+
+## Schemas
+
+- **task** — A work item with title, description, status, priority, assignee, dates, and labels
+- **project** — A container for tasks with members, colors, and case references
+- **column** — A kanban board column with WIP limits and ordering
+- **timeEntry** — A time tracking record linked to a task
+- **label** — A categorization tag with color coding
+
+## Seed Data
+
+Fresh installs include demo data:
+- 5 labels: Bug, Feature, Docs, Design, Infrastructure
+- 3 projects: Client Portal v2, Infrastructure Migration, Onboarding Automation
+- 12 columns: 4 per project (To Do, In Progress, Review, Done)
+- 5 tasks with realistic assignments
+- 3 time entries
+
+## Technical Details
+
+- Schemas are defined in `lib/Settings/planix_register.json`
+- Import is triggered by the `InitializeSettings` repair step (declared in `appinfo/info.xml`)
+- `SettingsService::loadConfiguration()` reads the JSON, parses it, and calls `ConfigurationService::importFromApp()`
+- Import is idempotent — re-running does not create duplicates
+- Required fields are validated by OpenRegister (e.g., task requires `title` and `status`)
+
+## Specs
+
+- [register-schemas spec](../../openspec/specs/register-schemas/spec.md)
diff --git a/docs/features/tasks.md b/docs/features/tasks.md
new file mode 100644
index 0000000..d2d6784
--- /dev/null
+++ b/docs/features/tasks.md
@@ -0,0 +1,30 @@
+# Tasks
+
+The core unit of work in Planix — create, assign, prioritize, and track tasks across projects.
+
+## Overview
+
+A task represents a piece of work within a project. Tasks carry a title, description, status, priority, assignee, due date, labels, and time estimate. They can be placed on the kanban board (assigned to a column) or held in the backlog (no column). The task detail view provides a full edit form, time tracking panel, and a sidebar with files, notes, tags, and an audit trail.
+
+## Key Capabilities
+
+- **Task CRUD** — create, view, edit, and delete tasks; required fields are title and status
+- **Status lifecycle** — open → in_progress → blocked → done → cancelled (aligned with iCalendar VTODO STATUS)
+- **Priority** — four levels: low, normal, high, urgent; displayed as colored dots on task cards
+- **Assignee** — assign to any Nextcloud user; avatar shown on kanban cards and task detail
+- **Due date** — set a due date; overdue tasks are highlighted on the board and in My Work
+- **Labels** — apply one or more app-wide labels (color-coded) for cross-project categorization
+- **Task detail view** — `CnDetailPage` with a core info card, time tracking panel, and `CnObjectSidebar` (Files, Notes, Tags, Audit Trail tabs)
+- **Kanban placement** — assign a task to a board column to move it from the backlog onto the board
+- **Procest link** — optional `zaakUuid` field links a task to a Procest case (see [Procest Integration](procest-integration.md))
+- **Notifications** — task assignment and due-date reminders sent via Nextcloud Notifications (configurable in user settings)
+
+## Standards
+
+- iCalendar VTODO (RFC 5545) — primary field reference for task properties
+- Schema.org Action / PlanAction — semantic type annotations
+- VNG InterneTaak (Klantinteracties) — API mapping layer for Procest bridge
+
+## Spec
+
+- [tasks spec](../../openspec/specs/tasks.md)
diff --git a/docs/features/time-tracking.md b/docs/features/time-tracking.md
new file mode 100644
index 0000000..89514e1
--- /dev/null
+++ b/docs/features/time-tracking.md
@@ -0,0 +1,47 @@
+# Time Tracking
+
+Estimate task effort and log actual time spent — then review your work in a personal timesheet.
+
+## Overview
+
+Time tracking in Planix operates at the task level. Each task carries an optional time estimate (how long it should take). Actual time is logged as separate time entries — one per work session — linked to the task. A personal timesheet shows all your logged entries grouped by date, with daily and weekly totals.
+
+## Key Capabilities
+
+### Time Estimate
+
+- Set an estimate on any task in the task detail view
+- Accepted input formats: `2h 30m`, `150m`, `1.5h`, `90` (minutes), `2h`
+- Stored as an integer number of minutes; displayed in human-readable format (e.g., 90 → "1h 30m")
+- Task card on the kanban board shows the estimate
+- Invalid input (e.g., zero, negative, unparseable) shows an inline validation error
+
+### Log Time
+
+- Click **Log time** in the task detail view to create a time entry
+- A time entry requires: duration and date (description is optional)
+- Multiple entries can be added to the same task across different sessions or days
+- Task detail shows the total logged time (sum of all entries for the task)
+- A progress indicator shows logged vs estimated: "1h 30m / 3h"; turns red when logged time exceeds the estimate
+
+### Timesheet
+
+- The Timesheet view shows all your time entries grouped by date (newest first)
+- Each row shows: task title (clickable link to task detail), project, duration, description, and edit/delete controls
+- Daily totals are shown for each date group
+- Filter by date range: "This week", "Last week", or a custom range; the total for the range is shown
+- Clicking a task title navigates to the task detail view; the browser back button returns to the timesheet at the same scroll position and date filter
+
+### Access Control
+
+- Users can only edit or delete their own time entries
+- Attempting to edit another user's entry via the API returns 403 Forbidden
+
+## Standards
+
+- Schema.org QuantitativeValue — time entry (value with unit code MIN)
+- iCalendar ESTIMATED-DURATION (RFC 7986) — estimated task duration field
+
+## Spec
+
+- [time-tracking spec](../../openspec/specs/time-tracking.md)
diff --git a/l10n/en.json b/l10n/en.json
index 5f6174e..96cb532 100644
--- a/l10n/en.json
+++ b/l10n/en.json
@@ -28,7 +28,86 @@
"Settings saved successfully": "Settings saved successfully",
"Saving...": "Saving...",
"This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.": "This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.",
- "User settings will appear here in a future update.": "User settings will appear here in a future update."
+ "User settings will appear here in a future update.": "User settings will appear here in a future update.",
+ "Projects": "Projects",
+ "New project": "New project",
+ "All": "All",
+ "Active": "Active",
+ "Archived": "Archived",
+ "Search projects": "Search projects",
+ "Search by title or description\u2026": "Search by title or description\u2026",
+ "Could not load projects": "Could not load projects",
+ "Retry": "Retry",
+ "No projects yet": "No projects yet",
+ "Create your first project to get started.": "Create your first project to get started.",
+ "Create your first project": "Create your first project",
+ "No projects match your search": "No projects match your search",
+ "Try different search terms or clear the filter.": "Try different search terms or clear the filter.",
+ "Project color: {color}": "Project color: {color}",
+ "{count} members": "{count} members",
+ "members": "members",
+ "Project title": "Project title",
+ "Enter project title\u2026": "Enter project title\u2026",
+ "Title is required": "Title is required",
+ "Description": "Description",
+ "Optional description\u2026": "Optional description\u2026",
+ "Color": "Color",
+ "Project color picker": "Project color picker",
+ "Icon (emoji)": "Icon (emoji)",
+ "e.g. \ud83d\udcc1 \ud83d\ude80 \u2705": "e.g. \ud83d\udcc1 \ud83d\ude80 \u2705",
+ "e.g. \ud83d\udcc1 \ud83d\ude80": "e.g. \ud83d\udcc1 \ud83d\ude80",
+ "Creating\u2026": "Creating\u2026",
+ "Create project": "Create project",
+ "Cancel": "Cancel",
+ "Could not create project. Please try again.": "Could not create project. Please try again.",
+ "You do not have access to this project": "You do not have access to this project",
+ "You are not a member of this project.": "You are not a member of this project.",
+ "Back to projects": "Back to projects",
+ "View backlog": "View backlog",
+ "Project settings": "Project settings",
+ "Backlog": "Backlog",
+ "Board view coming soon": "Board view coming soon",
+ "The Kanban board is being built. Use the Backlog view in the meantime.": "The Kanban board is being built. Use the Backlog view in the meantime.",
+ "View Backlog": "View Backlog",
+ "Backlog view coming soon": "Backlog view coming soon",
+ "Task management will be available in a future update.": "Task management will be available in a future update.",
+ "Details": "Details",
+ "Members": "Members",
+ "Danger zone": "Danger zone",
+ "Title": "Title",
+ "Project color": "Project color",
+ "Case reference": "Case reference",
+ "Project saved": "Project saved",
+ "Could not save project": "Could not save project",
+ "Add member": "Add member",
+ "Search for a user\u2026": "Search for a user\u2026",
+ "Leave project": "Leave project",
+ "Remove {name}": "Remove {name}",
+ "{name} has {count} assigned tasks in this project": "{name} has {count} assigned tasks in this project",
+ "Remove anyway": "Remove anyway",
+ "Archive project": "Archive project",
+ "Archive this project. It will no longer appear in the active list.": "Archive this project. It will no longer appear in the active list.",
+ "Are you sure?": "Are you sure?",
+ "Yes, archive": "Yes, archive",
+ "Delete project": "Delete project",
+ "Permanently delete this project and all its tasks.": "Permanently delete this project and all its tasks.",
+ "No users found for \"{query}\"": "No users found for \"{query}\"",
+ "Could not add member": "Could not add member",
+ "You are the last member. Leaving will make this project inaccessible to all users.": "You are the last member. Leaving will make this project inaccessible to all users.",
+ "Are you sure you want to leave this project? You will lose access.": "Are you sure you want to leave this project? You will lose access.",
+ "Could not leave project": "Could not leave project",
+ "This will permanently delete {count} tasks and all their time entries. This cannot be undone.": "This will permanently delete {count} tasks and all their time entries. This cannot be undone.",
+ "Project created": "Project created",
+ "Project deleted": "Project deleted",
+ "Could not delete project": "Could not delete project",
+ "Some columns could not be created: {columns}": "Some columns could not be created: {columns}",
+ "Failed to delete time entry \u2014 deletion stopped": "Failed to delete time entry \u2014 deletion stopped",
+ "Failed to delete task \u2014 deletion stopped": "Failed to delete task \u2014 deletion stopped",
+ "Failed to delete column \u2014 deletion stopped": "Failed to delete column \u2014 deletion stopped",
+ "Failed to delete project": "Failed to delete project",
+ "An error occurred during project deletion": "An error occurred during project deletion",
+ "Saving\u2026": "Saving\u2026",
+ "User search results": "User search results"
},
"plurals": ""
}
diff --git a/l10n/nl.json b/l10n/nl.json
index 8ed9c0f..32f338e 100644
--- a/l10n/nl.json
+++ b/l10n/nl.json
@@ -28,7 +28,86 @@
"Settings saved successfully": "Instellingen succesvol opgeslagen",
"Saving...": "Opslaan...",
"This app needs OpenRegister to store and manage data. Please install OpenRegister from the app store to get started.": "Deze app heeft OpenRegister nodig om gegevens op te slaan en te beheren. Installeer OpenRegister via de app store om te beginnen.",
- "User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update."
+ "User settings will appear here in a future update.": "Gebruikersinstellingen verschijnen hier in een toekomstige update.",
+ "Projects": "Projecten",
+ "New project": "Nieuw project",
+ "All": "Alle",
+ "Active": "Actief",
+ "Archived": "Gearchiveerd",
+ "Search projects": "Projecten zoeken",
+ "Search by title or description\u2026": "Zoek op titel of beschrijving\u2026",
+ "Could not load projects": "Kon projecten niet laden",
+ "Retry": "Opnieuw proberen",
+ "No projects yet": "Nog geen projecten",
+ "Create your first project to get started.": "Maak uw eerste project aan om te beginnen.",
+ "Create your first project": "Maak uw eerste project aan",
+ "No projects match your search": "Geen projecten gevonden voor uw zoekopdracht",
+ "Try different search terms or clear the filter.": "Probeer andere zoektermen of verwijder het filter.",
+ "Project color: {color}": "Projectkleur: {color}",
+ "{count} members": "{count} leden",
+ "members": "leden",
+ "Project title": "Projecttitel",
+ "Enter project title\u2026": "Voer een projecttitel in\u2026",
+ "Title is required": "Titel is verplicht",
+ "Description": "Beschrijving",
+ "Optional description\u2026": "Optionele beschrijving\u2026",
+ "Color": "Kleur",
+ "Project color picker": "Kleurkiezer voor project",
+ "Icon (emoji)": "Pictogram (emoji)",
+ "e.g. \ud83d\udcc1 \ud83d\ude80 \u2705": "bijv. \ud83d\udcc1 \ud83d\ude80 \u2705",
+ "e.g. \ud83d\udcc1 \ud83d\ude80": "bijv. \ud83d\udcc1 \ud83d\ude80",
+ "Creating\u2026": "Aanmaken\u2026",
+ "Create project": "Project aanmaken",
+ "Cancel": "Annuleren",
+ "Could not create project. Please try again.": "Kon project niet aanmaken. Probeer het opnieuw.",
+ "You do not have access to this project": "U heeft geen toegang tot dit project",
+ "You are not a member of this project.": "U bent geen lid van dit project.",
+ "Back to projects": "Terug naar projecten",
+ "View backlog": "Backlog bekijken",
+ "Project settings": "Projectinstellingen",
+ "Backlog": "Backlog",
+ "Board view coming soon": "Bordweergave komt eraan",
+ "The Kanban board is being built. Use the Backlog view in the meantime.": "Het Kanban-bord wordt gebouwd. Gebruik in de tussentijd de backlogweergave.",
+ "View Backlog": "Backlog bekijken",
+ "Backlog view coming soon": "Backlogweergave komt eraan",
+ "Task management will be available in a future update.": "Taakbeheer zal beschikbaar zijn in een toekomstige update.",
+ "Details": "Details",
+ "Members": "Leden",
+ "Danger zone": "Gevarenzone",
+ "Title": "Titel",
+ "Project color": "Projectkleur",
+ "Case reference": "Zaakreferentie",
+ "Project saved": "Project opgeslagen",
+ "Could not save project": "Kon project niet opslaan",
+ "Add member": "Lid toevoegen",
+ "Search for a user\u2026": "Zoek naar een gebruiker\u2026",
+ "Leave project": "Project verlaten",
+ "Remove {name}": "{name} verwijderen",
+ "{name} has {count} assigned tasks in this project": "{name} heeft {count} toegewezen taken in dit project",
+ "Remove anyway": "Toch verwijderen",
+ "Archive project": "Project archiveren",
+ "Archive this project. It will no longer appear in the active list.": "Dit project archiveren. Het verschijnt niet meer in de actieve lijst.",
+ "Are you sure?": "Weet u het zeker?",
+ "Yes, archive": "Ja, archiveren",
+ "Delete project": "Project verwijderen",
+ "Permanently delete this project and all its tasks.": "Dit project en al zijn taken definitief verwijderen.",
+ "No users found for \"{query}\"": "Geen gebruikers gevonden voor \"{query}\"",
+ "Could not add member": "Kon lid niet toevoegen",
+ "You are the last member. Leaving will make this project inaccessible to all users.": "U bent het laatste lid. Verlaten maakt dit project ontoegankelijk voor alle gebruikers.",
+ "Are you sure you want to leave this project? You will lose access.": "Weet u zeker dat u dit project wilt verlaten? U verliest de toegang.",
+ "Could not leave project": "Kon project niet verlaten",
+ "This will permanently delete {count} tasks and all their time entries. This cannot be undone.": "Dit zal {count} taken en alle bijbehorende tijdinvoeren definitief verwijderen. Dit kan niet ongedaan worden gemaakt.",
+ "Project created": "Project aangemaakt",
+ "Project deleted": "Project verwijderd",
+ "Could not delete project": "Kon project niet verwijderen",
+ "Some columns could not be created: {columns}": "Sommige kolommen konden niet worden aangemaakt: {columns}",
+ "Failed to delete time entry \u2014 deletion stopped": "Verwijderen van tijdinvoer mislukt \u2014 verwijdering gestopt",
+ "Failed to delete task \u2014 deletion stopped": "Verwijderen van taak mislukt \u2014 verwijdering gestopt",
+ "Failed to delete column \u2014 deletion stopped": "Verwijderen van kolom mislukt \u2014 verwijdering gestopt",
+ "Failed to delete project": "Verwijderen van project mislukt",
+ "An error occurred during project deletion": "Er is een fout opgetreden bij het verwijderen van het project",
+ "Saving\u2026": "Opslaan\u2026",
+ "User search results": "Gebruikerszoekresultaten"
},
"plurals": ""
}
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index f388809..643cba6 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -22,7 +22,6 @@
namespace OCA\Planix\AppInfo;
use OCA\Planix\Listener\DeepLinkRegistrationListener;
-use OCA\Planix\Repair\InitializeSettings;
use OCA\OpenRegister\Event\DeepLinkRegistrationEvent;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -64,9 +63,6 @@ public function register(IRegistrationContext $context): void
listener: DeepLinkRegistrationListener::class
);
- // Initialize register and schemas on install/upgrade.
- $context->registerRepairStep(InitializeSettings::class);
-
}//end register()
/**
diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php
index 50e8744..cc38d3b 100644
--- a/lib/Controller/SettingsController.php
+++ b/lib/Controller/SettingsController.php
@@ -24,6 +24,7 @@
use OCA\Planix\AppInfo\Application;
use OCA\Planix\Service\SettingsService;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
@@ -62,12 +63,21 @@ public function index(): JSONResponse
}//end index()
/**
- * Update settings with provided data.
+ * Update settings with provided data. Only admin users may write settings.
+ *
+ * @NoAdminRequired
*
* @return JSONResponse
*/
public function create(): JSONResponse
{
+ if ($this->settingsService->isCurrentUserAdmin() === false) {
+ return new JSONResponse(
+ ['error' => 'Admin privileges required to modify settings.'],
+ Http::STATUS_FORBIDDEN
+ );
+ }
+
$data = $this->request->getParams();
$config = $this->settingsService->updateSettings($data);
@@ -84,11 +94,21 @@ public function create(): JSONResponse
*
* Forces a fresh import regardless of version, auto-configuring
* all schema and register IDs from the import result.
+ * Only admin users may trigger this operation.
+ *
+ * @NoAdminRequired
*
* @return JSONResponse
*/
public function load(): JSONResponse
{
+ if ($this->settingsService->isCurrentUserAdmin() === false) {
+ return new JSONResponse(
+ ['error' => 'Admin privileges required to trigger configuration import.'],
+ Http::STATUS_FORBIDDEN
+ );
+ }
+
$result = $this->settingsService->loadConfiguration(force: true);
return new JSONResponse($result);
diff --git a/lib/Listener/DeepLinkRegistrationListener.php b/lib/Listener/DeepLinkRegistrationListener.php
index 8ee166c..2766231 100644
--- a/lib/Listener/DeepLinkRegistrationListener.php
+++ b/lib/Listener/DeepLinkRegistrationListener.php
@@ -48,13 +48,39 @@ public function handle(Event $event): void
return;
}
- // Register example object deep links.
- // Update the register slug, schema slug, and URL template to match your app's actual schemas.
$event->register(
appId: 'planix',
registerSlug: 'planix',
- schemaSlug: 'example',
- urlTemplate: '/apps/planix/#/examples/{uuid}'
+ schemaSlug: 'task',
+ urlTemplate: '/apps/planix/#/tasks/{uuid}'
+ );
+
+ $event->register(
+ appId: 'planix',
+ registerSlug: 'planix',
+ schemaSlug: 'project',
+ urlTemplate: '/apps/planix/#/projects/{uuid}'
+ );
+
+ $event->register(
+ appId: 'planix',
+ registerSlug: 'planix',
+ schemaSlug: 'column',
+ urlTemplate: '/apps/planix/#/columns/{uuid}'
+ );
+
+ $event->register(
+ appId: 'planix',
+ registerSlug: 'planix',
+ schemaSlug: 'label',
+ urlTemplate: '/apps/planix/#/labels/{uuid}'
+ );
+
+ $event->register(
+ appId: 'planix',
+ registerSlug: 'planix',
+ schemaSlug: 'timeEntry',
+ urlTemplate: '/apps/planix/#/time-entries/{uuid}'
);
}//end handle()
diff --git a/lib/Migration/Version20260403000000.php b/lib/Migration/Version20260403000000.php
new file mode 100644
index 0000000..b898ff9
--- /dev/null
+++ b/lib/Migration/Version20260403000000.php
@@ -0,0 +1,59 @@
+
+ * @copyright 2024 Conduction B.V.
+ * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * @version GIT:
+ *
+ * @link https://conduction.nl
+ */
+
+declare(strict_types=1);
+
+namespace OCA\Planix\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Empty schema migration that triggers the post-migration repair step.
+ *
+ * The repair step (InitializeSettings) re-runs loadConfiguration(force: true)
+ * with the updated register spec (version 0.2.1) which includes
+ * publicWrite: true and publicRead: true on the planix register.
+ */
+class Version20260403000000 extends SimpleMigrationStep
+{
+ /**
+ * No schema changes — only the post-migration repair step is needed.
+ *
+ * @param IOutput $output The migration output handler
+ * @param Closure $schemaClosure Closure to get the current DB schema
+ * @param array $options Migration options
+ *
+ * @return ISchemaWrapper|null
+ *
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
+ {
+ return null;
+
+ }//end changeSchema()
+}//end class
diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php
index 16f55e3..5e524fb 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -36,7 +36,7 @@ class SettingsService
{
/**
- * Configuration keys managed by this service.
+ * Legacy configuration keys (register setup keys).
*
* @var array
*/
@@ -44,6 +44,16 @@ class SettingsService
'register',
];
+ /**
+ * Admin configuration keys with their default values.
+ *
+ * @var array
+ */
+ private const ADMIN_CONFIG_DEFAULTS = [
+ 'default_columns' => '["To Do","In Progress","Review","Done"]',
+ 'allow_project_creation' => 'all',
+ ];
+
/**
* Constructor for the SettingsService.
*
@@ -77,7 +87,57 @@ public function isOpenRegisterAvailable(): bool
}//end isOpenRegisterAvailable()
/**
- * Retrieve all current settings.
+ * Check whether the current user has Nextcloud admin privileges.
+ *
+ * @return bool
+ */
+ public function isCurrentUserAdmin(): bool
+ {
+ $user = $this->userSession->getUser();
+ return ($user !== null && $this->groupManager->isAdmin($user->getUID()));
+ }//end isCurrentUserAdmin()
+
+ /**
+ * Retrieve all admin settings with defaults applied.
+ *
+ * Reads each key in ADMIN_CONFIG_DEFAULTS from IAppConfig, falling back to
+ * the defined default when no value has been stored yet.
+ *
+ * @return array
+ */
+ public function getAdminSettings(): array
+ {
+ $settings = [];
+ foreach (self::ADMIN_CONFIG_DEFAULTS as $key => $default) {
+ $settings[$key] = $this->appConfig->getValueString(Application::APP_ID, $key, $default);
+ }
+
+ return $settings;
+ }//end getAdminSettings()
+
+ /**
+ * Store admin settings. Unknown keys are silently ignored.
+ *
+ * Internal use only — callers outside this class must go through updateSettings(),
+ * which enforces the admin authorization check at the controller layer.
+ *
+ * @param array $settings Settings to persist
+ *
+ * @return array The full admin settings after update
+ */
+ private function setAdminSettings(array $settings): array
+ {
+ foreach (array_keys(self::ADMIN_CONFIG_DEFAULTS) as $key) {
+ if (array_key_exists($key, $settings) === true) {
+ $this->appConfig->setValueString(Application::APP_ID, $key, (string) $settings[$key]);
+ }
+ }
+
+ return $this->getAdminSettings();
+ }//end setAdminSettings()
+
+ /**
+ * Retrieve all current settings (admin + metadata).
*
* Returns a flat array containing all app config values plus metadata
* fields (openregisters, isAdmin) consumed by the frontend.
@@ -91,14 +151,12 @@ public function getSettings(): array
$settings[$key] = $this->appConfig->getValueString(Application::APP_ID, $key, '');
}
- $user = $this->userSession->getUser();
- $isAdmin = ($user !== null && $this->groupManager->isAdmin($user->getUID()));
-
return array_merge(
$settings,
+ $this->getAdminSettings(),
[
'openregisters' => $this->isOpenRegisterAvailable(),
- 'isAdmin' => $isAdmin,
+ 'isAdmin' => $this->isCurrentUserAdmin(),
]
);
}//end getSettings()
@@ -118,6 +176,8 @@ public function updateSettings(array $data): array
}
}
+ $this->setAdminSettings(settings: $data);
+
return $this->getSettings();
}//end updateSettings()
@@ -139,11 +199,41 @@ public function loadConfiguration(bool $force=false): array
}
try {
+ $configPath = __DIR__.'/../Settings/planix_register.json';
+ if (file_exists($configPath) === false) {
+ $this->logger->error('Planix: planix_register.json not found at '.$configPath);
+ return [
+ 'success' => false,
+ 'message' => 'Configuration file planix_register.json not found.',
+ ];
+ }
+
+ $configContent = file_get_contents($configPath);
+ if ($configContent === false) {
+ $this->logger->error('Planix: failed to read planix_register.json');
+ return [
+ 'success' => false,
+ 'message' => 'Failed to read configuration file.',
+ ];
+ }
+
+ $configData = json_decode($configContent, true);
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->logger->error('Planix: failed to parse planix_register.json: '.json_last_error_msg());
+ return [
+ 'success' => false,
+ 'message' => 'Failed to parse configuration file: '.json_last_error_msg(),
+ ];
+ }
+
+ $configVersion = ($configData['info']['version'] ?? '0.0.0');
+
$configurationService = $this->container->get('OCA\OpenRegister\Service\ConfigurationService');
- $result = $configurationService->importFromApp(appId: Application::APP_ID, force: $force);
+ $result = $configurationService->importFromApp(appId: Application::APP_ID, data: $configData, version: $configVersion, force: $force);
if (empty($result) === false) {
$this->logger->info('Planix: register configuration imported successfully');
+ $this->ensureRegisterPublicAccess();
return [
'success' => true,
'message' => 'Configuration imported successfully.',
@@ -151,6 +241,10 @@ public function loadConfiguration(bool $force=false): array
];
}
+ // ImportFromApp may return empty even when the register already existed
+ // and was updated. Apply the public-access patch unconditionally.
+ $this->ensureRegisterPublicAccess();
+
return [
'success' => false,
'message' => 'Import returned an empty result.',
@@ -165,5 +259,37 @@ public function loadConfiguration(bool $force=false): array
'message' => $e->getMessage(),
];
}//end try
+
}//end loadConfiguration()
+
+ /**
+ * Directly update the planix register's public access flags in the DB.
+ *
+ * Called after importFromApp to ensure publicWrite/publicRead are set to 0
+ * (private) even if OpenRegister's ConfigurationService did not update an
+ * existing record. Keeps the register accessible only to authenticated
+ * Nextcloud users; never grants anonymous access.
+ * Fails silently — any exception is logged as a warning only.
+ *
+ * @return void
+ */
+ private function ensureRegisterPublicAccess(): void
+ {
+ try {
+ $db = $this->container->get(\OCP\IDBConnection::class);
+ $qb = $db->getQueryBuilder();
+ $qb->update('openregister_registers')
+ ->set('public_write', $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
+ ->set('public_read', $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
+ ->where($qb->expr()->eq('slug', $qb->createNamedParameter('planix')));
+ $qb->executeStatement();
+ $this->logger->info('Planix: register publicWrite/publicRead set to private (0) in DB');
+ } catch (\Throwable $e) {
+ $this->logger->warning(
+ 'Planix: could not directly update register public access in DB',
+ ['error' => $e->getMessage()]
+ );
+ }//end try
+
+ }//end ensureRegisterPublicAccess()
}//end class
diff --git a/lib/Settings/planix_register.json b/lib/Settings/planix_register.json
index 004cffe..5b0c05d 100644
--- a/lib/Settings/planix_register.json
+++ b/lib/Settings/planix_register.json
@@ -3,7 +3,7 @@
"info": {
"title": "Planix Register",
"description": "Register containing all schemas for the Planix application.",
- "version": "0.1.0"
+ "version": "0.2.1"
},
"x-openregister": {
"type": "application",
@@ -13,30 +13,532 @@
},
"paths": {},
"components": {
+ "registers": {
+ "planix": {
+ "title": "Planix",
+ "description": "Planix application data register — tasks, projects, columns, time entries, and labels.",
+ "slug": "planix",
+ "version": "0.2.0",
+ "schemas": ["task", "project", "column", "timeEntry", "label"],
+ "tablePrefix": "planix",
+ "folder": "planix",
+ "publicWrite": true,
+ "publicRead": true
+ }
+ },
"schemas": {
- "example": {
- "slug": "example",
- "icon": "FileDocumentOutline",
+ "task": {
+ "slug": "task",
+ "icon": "CheckboxMarkedOutline",
"version": "0.1.0",
- "title": "Example",
- "description": "Example schema — replace with your app's actual schemas.",
+ "title": "Task",
+ "description": "A discrete unit of work. Maps to iCalendar VTODO (RFC 5545) and schema:PlanAction.",
"type": "object",
- "required": [
- "title"
- ],
+ "required": ["title", "status"],
"properties": {
"title": {
"type": "string",
- "description": "The title of the example object",
- "example": "My example"
+ "description": "Short title of the task"
},
"description": {
"type": "string",
- "description": "An optional description",
- "example": "This is an example"
+ "description": "Detailed description"
+ },
+ "status": {
+ "type": "string",
+ "description": "Current lifecycle status",
+ "enum": ["open", "in_progress", "blocked", "done", "cancelled"],
+ "default": "open"
+ },
+ "priority": {
+ "type": "string",
+ "description": "Priority level",
+ "enum": ["low", "normal", "high", "urgent"],
+ "default": "normal"
+ },
+ "project": {
+ "type": "string",
+ "description": "UUID of the parent Project",
+ "format": "uuid"
+ },
+ "zaakUuid": {
+ "type": "string",
+ "description": "UUID of a linked Procest zaak",
+ "format": "uuid",
+ "nullable": true
+ },
+ "column": {
+ "type": "string",
+ "description": "UUID of the kanban Column (null = backlog)",
+ "format": "uuid",
+ "nullable": true
+ },
+ "columnOrder": {
+ "type": "integer",
+ "description": "Sort order within the column",
+ "default": 0
+ },
+ "assignedTo": {
+ "type": "string",
+ "description": "Nextcloud user UID of the assignee"
+ },
+ "dueDate": {
+ "type": "string",
+ "description": "Due date in ISO 8601 format",
+ "format": "date"
+ },
+ "startDate": {
+ "type": "string",
+ "description": "Start date in ISO 8601 format",
+ "format": "date"
+ },
+ "estimatedDuration": {
+ "type": "integer",
+ "description": "Estimated effort in minutes"
+ },
+ "percentComplete": {
+ "type": "integer",
+ "description": "Completion percentage (0\u2013100)",
+ "minimum": 0,
+ "maximum": 100,
+ "default": 0
+ },
+ "labels": {
+ "type": "array",
+ "description": "UUIDs of linked Label objects",
+ "items": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "default": []
+ },
+ "parent": {
+ "type": "string",
+ "description": "UUID of the parent Task (sub-task support)",
+ "format": "uuid",
+ "nullable": true
+ },
+ "calendarEventUid": {
+ "type": "string",
+ "description": "UID of the linked NC Tasks / CalDAV VTODO event",
+ "nullable": true
+ },
+ "completedAt": {
+ "type": "string",
+ "description": "ISO 8601 datetime when the task was completed",
+ "format": "date-time",
+ "nullable": true
}
}
+ },
+ "project": {
+ "slug": "project",
+ "icon": "FolderOutline",
+ "version": "0.1.0",
+ "title": "Project",
+ "description": "A container grouping related tasks and kanban columns. Maps to schema:CreativeWork.",
+ "type": "object",
+ "required": ["title", "status"],
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Project name"
+ },
+ "description": {
+ "type": "string",
+ "description": "Purpose and scope of the project"
+ },
+ "status": {
+ "type": "string",
+ "description": "Project lifecycle status",
+ "enum": ["active", "archived", "completed"],
+ "default": "active"
+ },
+ "color": {
+ "type": "string",
+ "description": "Hex colour code for visual identification",
+ "pattern": "^#[0-9A-Fa-f]{6}$"
+ },
+ "icon": {
+ "type": "string",
+ "description": "Emoji or MDI icon name"
+ },
+ "members": {
+ "type": "array",
+ "description": "Nextcloud user UIDs of project members",
+ "items": {
+ "type": "string"
+ },
+ "default": []
+ },
+ "defaultAssignee": {
+ "type": "string",
+ "description": "Nextcloud user UID who receives new tasks by default"
+ },
+ "caseReference": {
+ "type": "string",
+ "description": "UUID of a linked Procest case",
+ "format": "uuid",
+ "nullable": true
+ },
+ "labels": {
+ "type": "array",
+ "description": "UUIDs of Label objects available within this project",
+ "items": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "default": []
+ }
+ }
+ },
+ "column": {
+ "slug": "column",
+ "icon": "ViewColumnOutline",
+ "version": "0.1.0",
+ "title": "Column",
+ "description": "A kanban column belonging to a project. Maps to schema:DefinedTerm.",
+ "type": "object",
+ "required": ["title", "project", "order"],
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Column heading (e.g. 'In Progress')"
+ },
+ "project": {
+ "type": "string",
+ "description": "UUID of the parent Project",
+ "format": "uuid"
+ },
+ "order": {
+ "type": "integer",
+ "description": "Left-to-right display order (0-based)",
+ "default": 0
+ },
+ "wipLimit": {
+ "type": "integer",
+ "description": "Work-in-progress limit (null = unlimited)",
+ "nullable": true
+ },
+ "color": {
+ "type": "string",
+ "description": "Hex colour code for the column header",
+ "pattern": "^#[0-9A-Fa-f]{6}$"
+ },
+ "type": {
+ "type": "string",
+ "description": "Functional type: active columns hold in-flight work; done columns auto-complete tasks",
+ "enum": ["active", "done"],
+ "default": "active"
+ }
+ }
+ },
+ "timeEntry": {
+ "slug": "timeEntry",
+ "icon": "TimerOutline",
+ "version": "0.1.0",
+ "title": "Time Entry",
+ "description": "Time logged by a user against a task. Maps to schema:QuantitativeValue.",
+ "type": "object",
+ "required": ["task", "user", "duration", "date"],
+ "properties": {
+ "task": {
+ "type": "string",
+ "description": "UUID of the related Task",
+ "format": "uuid"
+ },
+ "user": {
+ "type": "string",
+ "description": "Nextcloud user UID of the person who logged the time"
+ },
+ "duration": {
+ "type": "integer",
+ "description": "Time logged in minutes"
+ },
+ "date": {
+ "type": "string",
+ "description": "Date the work was performed (ISO 8601)",
+ "format": "date"
+ },
+ "description": {
+ "type": "string",
+ "description": "Optional note about the work performed"
+ }
+ }
+ },
+ "label": {
+ "slug": "label",
+ "icon": "TagOutline",
+ "version": "0.1.0",
+ "title": "Label",
+ "description": "A colour-coded tag applicable to tasks and projects. Maps to schema:DefinedTerm.",
+ "type": "object",
+ "required": ["title", "color"],
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "Label display name"
+ },
+ "color": {
+ "type": "string",
+ "description": "Hex colour for the label chip",
+ "pattern": "^#[0-9A-Fa-f]{6}$",
+ "default": "#4376FC"
+ },
+ "description": {
+ "type": "string",
+ "description": "Optional explanation of when to use this label"
+ }
+ }
+ }
+ },
+ "objects": [
+ {
+ "@self": { "register": "planix", "schema": "label", "slug": "bug", "id": "00000000-0000-4000-c000-000000000001" },
+ "title": "Bug",
+ "color": "#E74C3C",
+ "description": "Something is broken and needs fixing"
+ },
+ {
+ "@self": { "register": "planix", "schema": "label", "slug": "feature", "id": "00000000-0000-4000-c000-000000000002" },
+ "title": "Feature",
+ "color": "#4376FC",
+ "description": "New functionality"
+ },
+ {
+ "@self": { "register": "planix", "schema": "label", "slug": "docs", "id": "00000000-0000-4000-c000-000000000003" },
+ "title": "Docs",
+ "color": "#27AE60",
+ "description": "Documentation update"
+ },
+ {
+ "@self": { "register": "planix", "schema": "label", "slug": "design", "id": "00000000-0000-4000-c000-000000000004" },
+ "title": "Design",
+ "color": "#9B59B6",
+ "description": "UI/UX work"
+ },
+ {
+ "@self": { "register": "planix", "schema": "label", "slug": "infrastructure", "id": "00000000-0000-4000-c000-000000000005" },
+ "title": "Infrastructure",
+ "color": "#F39C12",
+ "description": "DevOps, deployment, and server work"
+ },
+ {
+ "@self": { "register": "planix", "schema": "project", "slug": "client-portal-v2", "id": "00000000-0000-4000-a000-000000000001" },
+ "title": "Client Portal v2",
+ "description": "Redesign and rebuild the self-service client portal using NL Design System components.",
+ "status": "active",
+ "color": "#4376FC",
+ "icon": "\ud83c\udf10",
+ "members": ["admin", "jdoe", "mvanderberg"],
+ "defaultAssignee": "jdoe"
+ },
+ {
+ "@self": { "register": "planix", "schema": "project", "slug": "infrastructure-migration", "id": "00000000-0000-4000-a000-000000000002" },
+ "title": "Infrastructure Migration",
+ "description": "Migrate on-premise services to managed Kubernetes cluster before Q3.",
+ "status": "active",
+ "color": "#F39C12",
+ "icon": "\u2601\ufe0f",
+ "members": ["admin", "ksmits"],
+ "defaultAssignee": "ksmits"
+ },
+ {
+ "@self": { "register": "planix", "schema": "project", "slug": "onboarding-automation", "id": "00000000-0000-4000-a000-000000000003" },
+ "title": "Onboarding Automation",
+ "description": "Automate new-employee onboarding workflows via n8n and Nextcloud.",
+ "status": "active",
+ "color": "#27AE60",
+ "icon": "\ud83e\udd1d",
+ "members": ["admin", "jdoe", "mvanderberg", "ksmits"],
+ "defaultAssignee": "admin"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "portal-todo", "id": "00000000-0000-4000-b000-000000000001" },
+ "title": "To Do",
+ "project": "00000000-0000-4000-a000-000000000001",
+ "order": 0,
+ "wipLimit": null,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "portal-in-progress", "id": "00000000-0000-4000-b000-000000000002" },
+ "title": "In Progress",
+ "project": "00000000-0000-4000-a000-000000000001",
+ "order": 1,
+ "wipLimit": 3,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "portal-review", "id": "00000000-0000-4000-b000-000000000003" },
+ "title": "Review",
+ "project": "00000000-0000-4000-a000-000000000001",
+ "order": 2,
+ "wipLimit": 2,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "portal-done", "id": "00000000-0000-4000-b000-000000000004" },
+ "title": "Done",
+ "project": "00000000-0000-4000-a000-000000000001",
+ "order": 3,
+ "wipLimit": null,
+ "type": "done"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "infra-todo", "id": "00000000-0000-4000-b000-000000000005" },
+ "title": "To Do",
+ "project": "00000000-0000-4000-a000-000000000002",
+ "order": 0,
+ "wipLimit": null,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "infra-in-progress", "id": "00000000-0000-4000-b000-000000000006" },
+ "title": "In Progress",
+ "project": "00000000-0000-4000-a000-000000000002",
+ "order": 1,
+ "wipLimit": 3,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "infra-review", "id": "00000000-0000-4000-b000-000000000007" },
+ "title": "Review",
+ "project": "00000000-0000-4000-a000-000000000002",
+ "order": 2,
+ "wipLimit": 2,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "infra-done", "id": "00000000-0000-4000-b000-000000000008" },
+ "title": "Done",
+ "project": "00000000-0000-4000-a000-000000000002",
+ "order": 3,
+ "wipLimit": null,
+ "type": "done"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "onboard-todo", "id": "00000000-0000-4000-b000-000000000009" },
+ "title": "To Do",
+ "project": "00000000-0000-4000-a000-000000000003",
+ "order": 0,
+ "wipLimit": null,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "onboard-in-progress", "id": "00000000-0000-4000-b000-000000000010" },
+ "title": "In Progress",
+ "project": "00000000-0000-4000-a000-000000000003",
+ "order": 1,
+ "wipLimit": 3,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "onboard-review", "id": "00000000-0000-4000-b000-000000000011" },
+ "title": "Review",
+ "project": "00000000-0000-4000-a000-000000000003",
+ "order": 2,
+ "wipLimit": 2,
+ "type": "active"
+ },
+ {
+ "@self": { "register": "planix", "schema": "column", "slug": "onboard-done", "id": "00000000-0000-4000-b000-000000000012" },
+ "title": "Done",
+ "project": "00000000-0000-4000-a000-000000000003",
+ "order": 3,
+ "wipLimit": null,
+ "type": "done"
+ },
+ {
+ "@self": { "register": "planix", "schema": "task", "slug": "fix-login-redirect", "id": "00000000-0000-4000-d000-000000000001" },
+ "title": "Fix login page redirect bug",
+ "description": "After OAuth login the user is redirected to / instead of the originally requested page.",
+ "status": "in_progress",
+ "priority": "high",
+ "project": "00000000-0000-4000-a000-000000000001",
+ "column": "00000000-0000-4000-b000-000000000002",
+ "columnOrder": 0,
+ "assignedTo": "jdoe",
+ "dueDate": "2026-04-10",
+ "labels": ["00000000-0000-4000-c000-000000000001"],
+ "percentComplete": 40
+ },
+ {
+ "@self": { "register": "planix", "schema": "task", "slug": "design-dashboard-widgets", "id": "00000000-0000-4000-d000-000000000002" },
+ "title": "Design dashboard widget layout",
+ "description": "Create Figma mockups for the new widget-based dashboard using NL Design System tokens.",
+ "status": "open",
+ "priority": "normal",
+ "project": "00000000-0000-4000-a000-000000000001",
+ "column": "00000000-0000-4000-b000-000000000001",
+ "columnOrder": 0,
+ "assignedTo": "mvanderberg",
+ "dueDate": "2026-04-18",
+ "labels": ["00000000-0000-4000-c000-000000000004"]
+ },
+ {
+ "@self": { "register": "planix", "schema": "task", "slug": "write-api-docs", "id": "00000000-0000-4000-d000-000000000003" },
+ "title": "Write REST API documentation",
+ "description": "Document all public endpoints in OpenAPI 3.0.0 format and publish to developer portal.",
+ "status": "open",
+ "priority": "normal",
+ "project": "00000000-0000-4000-a000-000000000001",
+ "column": "00000000-0000-4000-b000-000000000001",
+ "columnOrder": 1,
+ "assignedTo": "jdoe",
+ "labels": ["00000000-0000-4000-c000-000000000003"],
+ "estimatedDuration": 240
+ },
+ {
+ "@self": { "register": "planix", "schema": "task", "slug": "k8s-namespace-setup", "id": "00000000-0000-4000-d000-000000000004" },
+ "title": "Set up production Kubernetes namespaces",
+ "description": "Create namespaces, resource quotas, and network policies for prod environment.",
+ "status": "open",
+ "priority": "urgent",
+ "project": "00000000-0000-4000-a000-000000000002",
+ "column": "00000000-0000-4000-b000-000000000005",
+ "columnOrder": 0,
+ "assignedTo": "ksmits",
+ "dueDate": "2026-04-07",
+ "labels": ["00000000-0000-4000-c000-000000000005"]
+ },
+ {
+ "@self": { "register": "planix", "schema": "task", "slug": "onboarding-n8n-workflow", "id": "00000000-0000-4000-d000-000000000005" },
+ "title": "Build n8n onboarding workflow",
+ "description": "Create the automated onboarding flow: create NC account, add to groups, send welcome email, generate Planix project.",
+ "status": "open",
+ "priority": "normal",
+ "project": "00000000-0000-4000-a000-000000000003",
+ "column": "00000000-0000-4000-b000-000000000009",
+ "columnOrder": 0,
+ "assignedTo": "admin",
+ "estimatedDuration": 180,
+ "labels": ["00000000-0000-4000-c000-000000000002"]
+ },
+ {
+ "@self": { "register": "planix", "schema": "timeEntry", "slug": "te-fix-login-2026-04-01" },
+ "task": "00000000-0000-4000-d000-000000000001",
+ "user": "jdoe",
+ "duration": 90,
+ "date": "2026-04-01",
+ "description": "Traced redirect issue to missing redirect_uri state parameter in OAuth middleware."
+ },
+ {
+ "@self": { "register": "planix", "schema": "timeEntry", "slug": "te-fix-login-2026-04-02" },
+ "task": "00000000-0000-4000-d000-000000000001",
+ "user": "jdoe",
+ "duration": 60,
+ "date": "2026-04-02",
+ "description": "Implemented fix and wrote unit test."
+ },
+ {
+ "@self": { "register": "planix", "schema": "timeEntry", "slug": "te-k8s-2026-04-01" },
+ "task": "00000000-0000-4000-d000-000000000004",
+ "user": "ksmits",
+ "duration": 45,
+ "date": "2026-04-01",
+ "description": "Drafted namespace YAML manifests for review."
}
- }
+ ]
}
}
diff --git a/openspec/README.md b/openspec/README.md
index 27271a9..cfbfe2f 100644
--- a/openspec/README.md
+++ b/openspec/README.md
@@ -1,6 +1,6 @@
# Planix — OpenSpec
-This folder contains the configuration and specifications for Planix.
+This folder contains feature specifications, architectural decisions, and implementation specs for Planix.
## Goal
@@ -10,13 +10,56 @@ Planix is a Kanban-based project and task management app for Nextcloud, built as
| File / Folder | Purpose |
|---|---|
-| `app-config.json` | Core app configuration — all choices from `/opsx:app-create` and `/opsx:app-explore` |
-| `config.yaml` | OpenSpec project config — rules, context, standards |
-| `specs/` | Feature specifications |
-| `changes/` | In-progress and archived OpenSpec changes |
+| `app-config.json` | App identity, configuration, and tracked decisions — written by `/opsx:app-explore` |
+| `config.yaml` | OpenSpec CLI project configuration — context and rules |
+| `specs/` | Feature specs — what the app should do (input for OpenSpec changes) |
+| `architecture/` | App-specific Architectural Decision Records (ADRs) |
+| `changes/` | Individual change directories, each with a full set of specification artifacts (created on first change) |
+
+> If `app-config.json` has `"requiresOpenRegister": true`, install [OpenRegister](https://github.com/ConductionNL/openregister) before enabling this app. Planix requires OpenRegister as its data storage layer.
+
+## Artifact Progression
+
+Each change in `changes/` moves through these artifacts:
+
+```
+proposal.md ──► specs/ ──► design.md ──► tasks.md ──► plan.json
+ │
+ ▼
+ GitHub Issues
+ │
+ ▼
+ implementation
+ │
+ ▼
+ review.md
+ │
+ ▼
+ archive/
+```
+
+## Workflow
+
+1. **Explore** — Use `/opsx:app-explore planix` to think through goals, architecture, and features; captures decisions into `app-config.json`
+2. **Plan** — When a feature spec reaches `planned` status, use `/opsx:ff` to create a change spec
+3. **Implement** — Use `/opsx:apply` to implement the tasks
+4. **Verify** — Use `/opsx:verify` to check implementation matches the spec
+5. **Archive** — Use `/opsx:archive` to move completed changes to `changes/archive/`
## Commands
-- `/opsx:app-explore planix` — Think through and update app configuration interactively
-- `/opsx:app-apply planix` — Apply `app-config.json` changes to the actual app files
-- `/opsx:ff {feature-name}` — Implement a planned feature from `specs/`
+| Command | Purpose |
+|---------|---------|
+| `/opsx:app-design` | Full upfront design — architecture, features, wireframes (optional pre-step) |
+| `/opsx:app-create` | Bootstrap a new app or onboard an existing repo |
+| `/opsx:app-explore planix` | Think through goals, architecture, and features; updates `app-config.json` |
+| `/opsx:app-apply planix` | Apply `app-config.json` decisions to actual app files |
+| `/opsx:app-verify planix` | Audit app files against `app-config.json` (read-only) |
+| `/opsx:explore` | Investigate a problem or idea before starting a change (no output) |
+| `/opsx:ff {name}` | Create all artifacts for a new change at once |
+| `/opsx:new {name}` | Start a new change (step-by-step) |
+| `/opsx:continue` | Generate the next artifact in the sequence |
+| `/opsx:plan-to-issues` | Convert tasks.md into plan.json and GitHub Issues |
+| `/opsx:apply` | Implement tasks from a change |
+| `/opsx:verify` | Verify implementation matches the spec |
+| `/opsx:archive` | Archive a completed change |
diff --git a/openspec/ROADMAP.md b/openspec/ROADMAP.md
new file mode 100644
index 0000000..1b20004
--- /dev/null
+++ b/openspec/ROADMAP.md
@@ -0,0 +1,35 @@
+# Roadmap
+
+This document tracks the planned development of Planix.
+
+Features are defined in [`openspec/specs/`](specs/). When a feature reaches `planned` status during an `/opsx:app-explore` session, it is listed here and an OpenSpec change is created with `/opsx:ff`.
+
+## Status Overview
+
+| Feature | Status | Priority | OpenSpec Change |
+|---------|--------|----------|----------------|
+| _(no features defined yet — use `/opsx:app-explore planix` to start)_ | — | — | — |
+
+## Phases
+
+### Phase 1 — Foundation
+
+_Define the core features needed for a working app. These are the minimum set that make the app useful._
+
+### Phase 2 — Enhancement
+
+_Add features that improve the experience, extend functionality, and cover more use cases._
+
+### Phase 3 — Polish
+
+_Performance, accessibility improvements, full localization, and hardening for production._
+
+---
+
+## How This Works
+
+1. Run `/opsx:app-explore planix` to define features in `openspec/specs/`
+2. When a feature is `planned`, add it to the table above
+3. Run `/opsx:ff {feature-name}` to create the implementation spec
+4. Update the **OpenSpec Change** column with a link to the change directory
+5. When all changes for a feature are done, mark the feature `done`
diff --git a/openspec/app-config.json b/openspec/app-config.json
index 1154df0..1f1875d 100644
--- a/openspec/app-config.json
+++ b/openspec/app-config.json
@@ -17,9 +17,9 @@
},
"cicd": {
"phpVersions": ["8.3", "8.4"],
- "nextcloudRefs": ["stable31", "stable32"],
+ "nextcloudRefs": ["stable31", "stable32", "stable33"],
"enableNewman": false
},
"createdAt": "2026-03-24",
- "updatedAt": "2026-03-24"
+ "updatedAt": "2026-03-26"
}
diff --git a/openspec/architecture/README.md b/openspec/architecture/README.md
new file mode 100644
index 0000000..70553e7
--- /dev/null
+++ b/openspec/architecture/README.md
@@ -0,0 +1,66 @@
+# Architectural Decision Records
+
+This folder contains app-specific Architectural Decision Records (ADRs) for Planix.
+
+ADRs document significant design decisions, their context, the reasoning behind them, and the alternatives that were considered. They provide a historical record of why Planix is built the way it is.
+
+> **Note:** Organisation-wide ADRs (ADR-001 through ADR-015) live in `apps-extra/.claude/openspec/architecture/` and apply to all Conduction apps. Only create an app-specific ADR here when the decision is **unique to Planix** and not already covered by an org-wide ADR.
+
+ADRs are created and refined during `/opsx:app-explore planix` sessions.
+
+## Naming Convention
+
+Files are named `adr-{NNN}-{slug}.md` with sequential numbering:
+
+- `adr-001-example-decision.md`
+- `adr-002-another-decision.md`
+
+## File Format
+
+```markdown
+# ADR-{NNN}: {Title}
+
+**Status**: proposed | accepted | deprecated | superseded by [ADR-XXX]
+
+**Date**: YYYY-MM-DD
+
+## Context
+
+What situation or problem prompted this decision? What constraints exist?
+
+## Decision
+
+What was decided?
+
+## Consequences
+
+**Positive:**
+- ...
+
+**Negative / trade-offs:**
+- ...
+
+## Alternatives Considered
+
+| Option | Reason not chosen |
+|--------|------------------|
+| ... | ... |
+```
+
+## Status Values
+
+| Status | Meaning |
+|--------|---------|
+| `proposed` | Being discussed — not yet in effect |
+| `accepted` | Agreed and in effect |
+| `deprecated` | No longer applies (but kept for history) |
+| `superseded` | Replaced by a newer ADR (reference the new one) |
+
+## When to Write an ADR
+
+Write an ADR whenever you make a significant decision that:
+- Is hard to reverse
+- Affects multiple parts of the codebase
+- Would surprise future developers if they didn't know the reasoning
+- Involves a meaningful trade-off
+- Is specific to Planix (not already covered by an org-wide ADR)
diff --git a/openspec/architecture/adr-001-vtodo-primary-standard.md b/openspec/architecture/adr-001-vtodo-primary-standard.md
new file mode 100644
index 0000000..e502b02
--- /dev/null
+++ b/openspec/architecture/adr-001-vtodo-primary-standard.md
@@ -0,0 +1,38 @@
+# ADR-001: VTODO as Primary Task Standard
+
+**Status**: accepted
+
+**Date**: 2026-03-26
+
+## Context
+
+Planix stores task data in OpenRegister. We needed a field reference for task properties (title, status, priority, due date, assignee, etc.) that is mature, widely understood, and allows future interoperability with calendar/task systems including Nextcloud's own Tasks app.
+
+We evaluated 8 standards: iCalendar VTODO, Schema.org Action, OpenProject Work Packages API, Nextcloud CalDAV VTODO, GitHub Issues API, VNG InterneTaak, BPMN 2.0 UserTask, and W3C PROV-O.
+
+## Decision
+
+iCalendar VTODO (RFC 5545) is the primary field reference for the Task entity. Schema.org Action provides semantic type annotations. VNG InterneTaak is an API mapping layer only — not a storage model.
+
+> **Data storage uses international standards. Dutch government standards are an API mapping layer.**
+
+## Consequences
+
+**Positive:**
+- Task properties (SUMMARY, DESCRIPTION, DTSTART, DUE, STATUS, PRIORITY, PERCENT-COMPLETE, ATTENDEE, CATEGORIES, RELATED-TO) map to a ratified RFC
+- Compatible with Nextcloud Tasks app (CalDAV/VTODO) via `calendarEventUid` reference field
+- Schema.org annotations make tasks machine-readable / JSON-LD compatible
+- Dutch government interoperability via VNG mapping layer, without coupling storage model to Dutch-only standards
+
+**Negative / trade-offs:**
+- VTODO does not cover project/board concepts — supplemented with Schema.org ItemList (boards) and DefinedTerm (columns/labels)
+- VTODO property names (DTSTART, ATTENDEE) are not used verbatim in JSON — we use camelCase equivalents (startDate, assignedTo)
+
+## Alternatives Considered
+
+| Option | Reason not chosen |
+|--------|------------------|
+| BPMN 2.0 UserTask | Too process-oriented; no kanban fit |
+| VNG InterneTaak as primary | Dutch-only standard; excludes non-government use cases |
+| GitHub Issues API as primary | No time tracking fields, no percent-complete |
+| W3C PROV-O | Audit trail reference only, not a task model |
diff --git a/openspec/architecture/adr-002-flow-based-kanban.md b/openspec/architecture/adr-002-flow-based-kanban.md
new file mode 100644
index 0000000..0ac455d
--- /dev/null
+++ b/openspec/architecture/adr-002-flow-based-kanban.md
@@ -0,0 +1,38 @@
+# ADR-002: Flow-Based Kanban (No Sprints)
+
+**Status**: accepted
+
+**Date**: 2026-03-26
+
+## Context
+
+Planix is positioned for developer and IT teams who manage continuous work. Two models exist for kanban-based project management:
+
+- **Flow-based (continuous)**: work items move through columns at their own pace; no time-boxed iterations
+- **Sprint-based (Scrum)**: work is planned into 2-week sprints with velocity tracking, burndown charts, and sprint reviews
+
+Competitors like Jira support both; Linear and Plane are flow-based only. Nextcloud Deck is flow-based but lacks backlog and WIP limits.
+
+## Decision
+
+Planix is flow-based only. Projects have one persistent kanban board with configurable columns. There are no sprints, sprint planning, velocity charts, or burndown charts in any tier (MVP, V1, or Enterprise). A backlog is implicit — tasks not assigned to a board column live there.
+
+## Consequences
+
+**Positive:**
+- Simpler mental model — no sprint ceremonies or planning overhead
+- Matches how most small dev/IT teams actually work
+- Aligns with Plane and Linear positioning (modern, developer-first)
+- Less UI surface area; faster to build and maintain
+- Cumulative flow diagrams (V1) replace burndown as the primary flow metric
+
+**Negative / trade-offs:**
+- Scrum teams requiring sprint planning and velocity tracking are excluded
+- No roadmap/milestone grouping at MVP — teams with release planning needs must use labels or project naming conventions as a workaround
+
+## Alternatives Considered
+
+| Option | Reason not chosen |
+|--------|------------------|
+| Sprint support in V1 | Adds significant complexity (Sprint entity, task-sprint assignment, velocity calculation) for a minority use case; better served by a dedicated Scrum tool |
+| Optional sprint mode per project | Doubles the UI surface and creates two divergent user mental models within one app |
diff --git a/openspec/architecture/adr-003-procest-bridge.md b/openspec/architecture/adr-003-procest-bridge.md
new file mode 100644
index 0000000..9ceb82c
--- /dev/null
+++ b/openspec/architecture/adr-003-procest-bridge.md
@@ -0,0 +1,46 @@
+# ADR-003: Procest Bridge via Schema Fields (Loose Coupling)
+
+**Status**: accepted
+
+**Date**: 2026-03-26
+
+## Context
+
+Planix's sister app Procest handles case management (ZGW-aligned). A common workflow is: a case in Procest generates one or more tasks that need to be tracked on a kanban board in Planix.
+
+Three integration approaches were considered:
+
+1. **Tight coupling** — Planix calls the Procest API to fetch/create tasks
+2. **Loose coupling** — Tasks carry optional metadata fields that reference a Procest case; no direct API calls between apps
+3. **Separate bridge service** — a dedicated integration layer translates between the two apps
+
+## Decision
+
+Loose coupling via schema fields. The Task entity has two optional fields:
+
+- `caseReference` (string) — human-readable case identifier
+- `zaakUuid` (UUID) — machine-readable ZGW case UUID
+
+Planix does not call Procest APIs in MVP. Procest creates tasks in Planix via OpenRegister directly, populating these fields. Planix displays them as read-only metadata on the task detail view.
+
+**Project ownership is configurable — Procest's UI decides.** When creating tasks for a case, Procest's UI presents a project picker. The user can create a new Planix project (with `caseReference` linking back to the case) or add tasks to an existing project (with `zaakUuid` on each task). Planix has no routing or default-project mechanism — it reads whatever Procest wrote to OpenRegister.
+
+## Consequences
+
+**Positive:**
+- No circular dependency between apps at runtime
+- Planix works fully without Procest installed
+- Tasks remain valid OpenRegister objects regardless of case status
+- `zaakUuid` enables future ZGW API mapping without changing the data model
+
+**Negative / trade-offs:**
+- No real-time status sync between Procest case and Planix task in MVP
+- Planix cannot initiate case creation in Procest
+- `caseReference` is a display-only string — no validation against Procest data
+
+## Alternatives Considered
+
+| Option | Reason not chosen |
+|--------|------------------|
+| Tight API coupling | Creates runtime dependency; if Procest is down, Planix task operations are affected |
+| Separate bridge service | Over-engineering for MVP; adds infrastructure complexity before the integration pattern is proven |
diff --git a/openspec/architecture/adr-004-time-tracking-scope.md b/openspec/architecture/adr-004-time-tracking-scope.md
new file mode 100644
index 0000000..9a29ddc
--- /dev/null
+++ b/openspec/architecture/adr-004-time-tracking-scope.md
@@ -0,0 +1,37 @@
+# ADR-004: Time Tracking Scope — Manual Only in MVP
+
+**Status**: accepted
+
+**Date**: 2026-03-26
+
+## Context
+
+Time tracking is a key differentiator for Planix vs. competitors (Plane, Taiga, Nextcloud Deck — none have time tracking). However, time tracking can be implemented at three levels of complexity:
+
+1. **Manual logging only** — user enters duration + date after the fact
+2. **Live timers** — start/stop timer on a task; auto-creates a time entry
+3. **External integrations** — sync with Toggl, Harvest, etc.
+
+## Decision
+
+MVP includes manual time logging only (TimeEntry entity with task, user, duration in minutes, date, and optional description). Live timers are deferred to V1. External integrations are Enterprise tier.
+
+**Time entries are per-task only, in all tiers.** `TimeEntry.task` is always required — there are no project-level time entries. Overhead work (meetings, planning, standups) is tracked as tasks. This keeps the data model simple and queryable with no special cases.
+
+## Consequences
+
+**Positive:**
+- Manual logging covers the primary use case (reporting hours per task) with minimal UI complexity
+- TimeEntry data model is identical whether entries are created manually or via a timer — V1 timer feature adds UI only, not schema changes
+- Faster MVP delivery; time tracking UI is a form, not a real-time widget
+
+**Negative / trade-offs:**
+- Users who prefer live timers must track time externally and log manually in MVP
+- No automatic time capture — relies on user discipline to log accurately
+
+## Alternatives Considered
+
+| Option | Reason not chosen |
+|--------|------------------|
+| Timers in MVP | Adds a persistent UI widget (active timer indicator in navigation/header), background state management, and edge cases (multiple active timers, browser close mid-timer) that significantly increase MVP scope |
+| Skip time tracking entirely until V1 | Time tracking is the primary competitive differentiator vs. Plane and Nextcloud Deck; even manual logging establishes the feature and data model in MVP |
diff --git a/openspec/changes/admin-user-settings/.openspec.yaml b/openspec/changes/admin-user-settings/.openspec.yaml
new file mode 100644
index 0000000..6a5db8c
--- /dev/null
+++ b/openspec/changes/admin-user-settings/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-02
diff --git a/openspec/changes/admin-user-settings/design.md b/openspec/changes/admin-user-settings/design.md
new file mode 100644
index 0000000..441dea4
--- /dev/null
+++ b/openspec/changes/admin-user-settings/design.md
@@ -0,0 +1,363 @@
+# Design: admin-user-settings
+
+**Change ID:** admin-user-settings
+**Status:** draft
+**Created:** 2026-04-02
+
+---
+
+## Context
+
+Planix is a thin-client Nextcloud app backed by OpenRegister. It has no custom database tables. Admin settings (`IAppConfig`) and user settings (`IConfig`) are stored in Nextcloud's native configuration storage, which both PHP and frontend can read/write through the `SettingsController`.
+
+The existing codebase has two stub entry points:
+- `lib/Settings/AdminSettings.php` — registered with Nextcloud's settings infrastructure; renders a template that is currently empty.
+- `src/views/settings/UserSettings.vue` — imported in the Vue app but renders nothing.
+
+This change implements both stubs fully, adds a `SettingsController` that exposes read/write endpoints for both admin and user settings, and integrates the user settings keys into `NotificationService`.
+
+---
+
+## Goals
+
+- Admin settings page: `CnVersionInfoCard`, editable default columns list, OpenRegister register initialization button.
+- User settings dialog: `NcAppSettingsDialog`, notification toggles, default view selector.
+- Pinia settings store: single source of truth for both admin and user settings in the frontend.
+- `ColumnListEditor` component: ordered, drag-to-reorder list for column name strings.
+- Backend endpoints: `GET /settings/admin`, `GET /settings/user`, `PUT /settings/user`, `POST /settings/admin/register-init`.
+- NotificationService: align `SUBJECT_SETTING_MAP` keys with user settings keys.
+- Full i18n coverage (en + nl).
+
+## Non-Goals
+
+- Procest bridge settings (`procest_bridge_enabled`, `procest_base_url`) — V1 feature; section placeholder may be rendered but fields are non-functional.
+- `allow_project_creation` access control setting — V1.
+- `notify_overdue`, `notify_commented`, `notify_status_changed`, `items_per_page` user settings — V1; keys may be declared in `SUBJECT_SETTING_MAP` but toggles are not shown in MVP dialog.
+- OAuth/SAML authentication settings — outside scope.
+- Custom PHP admin settings UI using Nextcloud's legacy `IAdmin` interface — using Vue SPA rendered in the template instead.
+
+---
+
+## Decisions
+
+### Decision 1: Admin settings use a Vue component rendered inside the Nextcloud admin template
+
+**Options considered:**
+1. Pure PHP template with HTML form elements (legacy Nextcloud pattern).
+2. Vue SPA rendered inside the PHP template via a mount point `` (chosen).
+
+**Rationale:** `CnVersionInfoCard` and `CnSettingsSection` are Vue components from `@conduction/nextcloud-vue`. Rendering them requires a Vue mount point. The PHP `AdminSettings.php` provides the initial data (version, update status, current settings values) as JSON on the page's `data-*` attributes so the Vue component can hydrate without an extra network request.
+
+The PHP template registers a dedicated admin settings entry script (`admin-settings.js`) that mounts `AdminSettings.vue` onto the `#planix-admin-settings` div.
+
+### Decision 2: User settings use NcAppSettingsDialog, not NcDialog
+
+**Options considered:**
+1. `NcDialog` — generic modal.
+2. `NcAppSettingsDialog` — Nextcloud's purpose-built settings dialog with sectioned navigation (chosen).
+
+**Rationale:** `NcAppSettingsDialog` provides a two-pane layout with a sidebar section list and content area. This matches the established Nextcloud UX pattern for per-app user settings (Deck, Talk, Calendar all use this). It also renders consistently across desktop and mobile viewports. Using `NcDialog` would require reimplementing the section navigation.
+
+The dialog is opened from a gear icon `NcAppNavigationItem` at the bottom of the Planix navigation sidebar (existing slot in `MainMenu.vue`).
+
+### Decision 3: Settings store handles both admin and user settings in one Pinia store
+
+**Options considered:**
+1. Two separate stores: `useAdminSettingsStore` and `useUserSettingsStore`.
+2. One store `useSettingsStore` with namespaced getters/actions (chosen).
+
+**Rationale:** Both settings types share the same controller (`SettingsController`) and the same fetch-on-mount / save-on-change pattern. A single store reduces boilerplate and makes it easy to read both in a single mount hook. State is namespaced internally (`adminSettings`, `userSettings`).
+
+### Decision 4: Column list editor uses SortableJS via vue-draggable-plus (or HTML5 DnD fallback)
+
+**Options considered:**
+1. `vue-draggable-plus` (SortableJS wrapper, used in other Conduction apps) — chosen if already in `package.json`.
+2. Plain HTML5 `draggable` attribute with `ondragstart`/`ondrop` (fallback if vue-draggable-plus is not available).
+
+**Rationale:** Drag-to-reorder is a critical UX requirement for the column list. `vue-draggable-plus` provides a declarative Vue 3 wrapper around SortableJS with built-in accessibility. If not available, the HTML5 DnD fallback is acceptable for MVP given the admin-only audience. The component exposes a `modelValue` prop (array of strings) and emits `update:modelValue` on change, making it a standard v-model component.
+
+The component also supports keyboard-accessible reordering (move up/down buttons) alongside drag-and-drop, satisfying WCAG AA requirement for keyboard accessibility.
+
+### Decision 5: Register initialization is an admin-only synchronous endpoint
+
+**Options considered:**
+1. Asynchronous: endpoint triggers a background job, admin polls for status.
+2. Synchronous: endpoint calls `ConfigurationService::importFromApp()` directly and returns success/error (chosen for MVP).
+
+**Rationale:** OpenRegister register initialization is a one-time operation that completes in under 2 seconds in typical environments. A synchronous endpoint keeps the implementation simple. The frontend shows a spinner while the request is in flight. If the operation takes longer in edge cases (large schema files, slow disk), the standard Nextcloud request timeout (30 s) is sufficient.
+
+The endpoint is protected by `OCP\AppFramework\Middleware\Security\SecurityMiddleware` admin check via the `@AdminRequired` annotation on the controller action.
+
+### Decision 6: User settings are read once on dialog open, not reactive
+
+**Options considered:**
+1. Reactive: settings subscribe to a server-sent event or poll every N seconds.
+2. Read-once: settings are fetched when the dialog opens; each toggle saves immediately via PUT (chosen).
+
+**Rationale:** User settings do not change from another session during the dialog's open state. Read-once on open + save-on-change is simpler, faster, and consistent with how all other Nextcloud settings dialogs work (Deck, Calendar, Talk). Each toggle calls `PUT /settings/user` with the changed key/value pair; the store updates optimistically.
+
+### Decision 7: NotificationService SUBJECT_SETTING_MAP keys use `notify_` prefix (matching user settings keys directly)
+
+**Options considered:**
+1. Map notification subjects to arbitrarily named setting keys.
+2. Map notification subjects to user setting keys with `notify_` prefix, matching the `IConfig` key names exactly (chosen).
+
+**Rationale:** This makes the relationship explicit and reduces the chance of mismatch. The `SUBJECT_SETTING_MAP` in `NotificationService` maps:
+
+| Notification subject | IConfig user key | Default |
+|---------------------|-----------------|---------|
+| `task_assigned` | `notify_assigned` | `true` |
+| `task_due_soon` | `notify_due_reminder` | `true` |
+| `task_overdue` (V1) | `notify_overdue` | `true` |
+| `task_commented` (V1) | `notify_commented` | `true` |
+| `task_status_changed` (V1) | `notify_status_changed` | `false` |
+
+Note: The `tasks` change used `notify_task_assigned` and `notify_task_due_soon` as key names in its design. This change adopts the shorter form (`notify_assigned`, `notify_due_reminder`) as defined in the base spec (`openspec/specs/admin-user-settings.md`). The `tasks` change's `NotificationService` must be updated to use these canonical key names.
+
+---
+
+## Component Architecture
+
+```
+src/
+ views/settings/
+ AdminSettings.vue # Vue component mounted in admin template
+ UserSettings.vue # NcAppSettingsDialog (replaces empty placeholder)
+ components/settings/
+ ColumnListEditor.vue # Drag-to-reorder column name list editor
+ store/
+ settings.js # Pinia store — useSettingsStore
+
+lib/
+ Settings/
+ AdminSettings.php # Modified — add page data (version, update, settings)
+ Controller/
+ SettingsController.php # Modified — add userIndex, userUpdate, adminRegisterInit
+ Service/
+ NotificationService.php # Modified — align SUBJECT_SETTING_MAP keys
+
+templates/
+ admin-settings.php # Modified — add mount point, load admin-settings.js
+
+appinfo/
+ routes.php # Modified — add user settings + register init routes
+ assets.php (or webpack.config)# Modified — add admin-settings.js entry point
+
+src/navigation/
+ MainMenu.vue # Modified — gear icon opens UserSettings.vue
+```
+
+---
+
+## Backend: SettingsController Endpoints
+
+| Method | URL | Auth | Description |
+|--------|-----|------|-------------|
+| `GET` | `/planix/settings/admin` | Admin | Read all admin settings |
+| `PUT` | `/planix/settings/admin` | Admin | Update admin settings (full or partial) |
+| `POST` | `/planix/settings/admin/register-init` | Admin | Trigger `ConfigurationService::importFromApp()` |
+| `GET` | `/planix/settings/user` | Any authenticated | Read current user's settings |
+| `PUT` | `/planix/settings/user` | Any authenticated | Update one or more of current user's settings |
+
+Admin settings read response schema:
+```json
+{
+ "default_columns": ["To Do", "In Progress", "Review", "Done"],
+ "allow_project_creation": "all",
+ "procest_bridge_enabled": false,
+ "procest_base_url": "",
+ "register_initialized": true,
+ "app_version": "0.1.0",
+ "update_available": false,
+ "update_version": null
+}
+```
+
+User settings read response schema:
+```json
+{
+ "notify_assigned": true,
+ "notify_due_reminder": true,
+ "notify_overdue": true,
+ "notify_commented": true,
+ "notify_status_changed": false,
+ "default_view": "my-work",
+ "items_per_page": 25
+}
+```
+
+---
+
+## Pinia Store: `useSettingsStore`
+
+```js
+// src/store/settings.js
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useSettingsStore = defineStore('settings', () => {
+ // State
+ const adminSettings = ref({})
+ const userSettings = ref({})
+ const adminLoading = ref(false)
+ const userLoading = ref(false)
+ const error = ref(null)
+
+ // Actions
+ async function fetchAdminSettings() { /* GET /planix/settings/admin */ }
+ async function updateAdminSettings(data) { /* PUT /planix/settings/admin */ }
+ async function initRegister() { /* POST /planix/settings/admin/register-init */ }
+ async function fetchUserSettings() { /* GET /planix/settings/user */ }
+ async function updateUserSetting(key, value) {
+ // Optimistic update: set locally first, then PUT; revert on failure
+ const prev = userSettings.value[key]
+ userSettings.value[key] = value
+ try {
+ await /* PUT /planix/settings/user with { [key]: value } */
+ } catch (e) {
+ userSettings.value[key] = prev
+ throw e
+ }
+ }
+
+ return {
+ adminSettings, userSettings, adminLoading, userLoading, error,
+ fetchAdminSettings, updateAdminSettings, initRegister,
+ fetchUserSettings, updateUserSetting,
+ }
+})
+```
+
+---
+
+## AdminSettings.vue Layout
+
+```
+┌─────────────────────────────────────────────────────┐
+│ CnVersionInfoCard │
+│ App name: Planix | Version: 0.1.0 | [Update avail] │
+├─────────────────────────────────────────────────────┤
+│ CnSettingsSection: Default Project Configuration │
+│ ┌─────────────────────────────────────────────┐ │
+│ │ ColumnListEditor │ │
+│ │ [≡] To Do [✕] │ │
+│ │ [≡] In Progress [✕] │ │
+│ │ [≡] Review [✕] │ │
+│ │ [≡] Done [✕] │ │
+│ │ [+ Add column] │ │
+│ └─────────────────────────────────────────────┘ │
+│ [Save changes] │
+├─────────────────────────────────────────────────────┤
+│ CnSettingsSection: Register Setup │
+│ Status: ✓ Initialized / ✗ Not initialized │
+│ [Initialize register] (spinner when in progress) │
+└─────────────────────────────────────────────────────┘
+```
+
+---
+
+## UserSettings.vue Layout (NcAppSettingsDialog)
+
+```
+┌──────────────────────────────────────────────────────┐
+│ Planix Settings [Close] │
+├──────────┬───────────────────────────────────────────┤
+│ Sections │ Content area │
+│ │ │
+│ Notific. │ Notifications │
+│ Display │ ────────────────────────────────────── │
+│ │ [toggle] Notify when a task is │
+│ │ assigned to me │
+│ │ │
+│ │ [toggle] Notify 1 day before a │
+│ │ task's due date │
+└──────────┴───────────────────────────────────────────┘
+```
+
+Second section (Display):
+```
+│ Display
+│ ──────────────────────────────────────
+│ Default view: [my-work ▼]
+│ Options: My Work | Kanban | Backlog
+```
+
+---
+
+## ColumnListEditor Component Anatomy
+
+```
+props: {
+ modelValue: { type: Array, required: true }, // string[]
+ disabled: { type: Boolean, default: false },
+}
+emits: ['update:modelValue']
+```
+
+Each item row:
+- Drag handle icon (`≡`) — cursor: grab
+- Text input (column name, min 1 char)
+- Remove button (`✕`) — disabled if only 1 item remains
+- Up/Down keyboard buttons for accessibility
+
+Add column: appends `''` (empty) to the list; focuses the new input.
+Save is triggered by the parent (`AdminSettings.vue` "Save changes" button), not per-keystroke.
+
+---
+
+## PHP: AdminSettings.php Data Injection
+
+```php
+class AdminSettings implements ISettings {
+ public function getForm(): TemplateResponse {
+ $appVersion = \OCP\App::getAppVersion('planix');
+ // Check for updates via IAppManager or app store API (cached)
+ $updateInfo = $this->appManager->getAppInfo('planix');
+ $registerInitialized = $this->configurationService->isInitialized();
+
+ return new TemplateResponse('planix', 'admin-settings', [
+ 'appVersion' => $appVersion,
+ 'updateAvailable' => $updateInfo['update_available'] ?? false,
+ 'updateVersion' => $updateInfo['update_version'] ?? null,
+ 'registerInitialized'=> $registerInitialized,
+ ]);
+ }
+}
+```
+
+The Vue `AdminSettings.vue` component reads these values from `data-*` attributes on the mount div and supplements with a `GET /settings/admin` call for the editable settings values.
+
+---
+
+## i18n String Inventory
+
+| Key context | Example string |
+|-------------|----------------|
+| Admin page title | `Planix Settings` |
+| Version card | `Version {version}`, `Update available: {version}`, `Up to date` |
+| Default columns section | `Default Project Configuration`, `Default columns for new projects` |
+| Column editor | `Add column`, `Remove column`, `Move up`, `Move down`, `Column name` |
+| Column editor validation | `Column name cannot be empty` |
+| Save button | `Save changes`, `Saving…`, `Changes saved` |
+| Register setup section | `Register Setup`, `Register initialized`, `Register not initialized`, `Initialize register`, `Initializing…`, `Register initialized successfully`, `Failed to initialize register` |
+| User dialog title | `Planix Settings` |
+| Notifications section | `Notifications` |
+| Notify assigned toggle | `Notify me when a task is assigned to me` |
+| Notify due reminder toggle | `Notify me 1 day before a task's due date` |
+| Display section | `Display` |
+| Default view label | `Default view` |
+| Default view options | `My Work`, `Kanban`, `Backlog` |
+| Save success | `Settings saved` |
+| Save error | `Failed to save settings` |
+
+---
+
+## Risks and Trade-offs
+
+| Risk | Likelihood | Mitigation |
+|------|-----------|-----------|
+| `NotificationService` key mismatch with `tasks` change | Medium | Canonical key names defined here; `tasks` change must adopt them. Document in both PRs. |
+| Vue mount inside PHP admin template conflicts with NC CSP | Low | Use `TemplateResponse` (not raw HTML output); NC admin templates already support script includes. |
+| `CnVersionInfoCard` update-check hits app store on every page load | Low | Cache update check result in `ICache` (TTL: 1 hour); serve cached value in `AdminSettings.php`. |
+| Drag-to-reorder not keyboard accessible by default | Medium | `ColumnListEditor` must include Up/Down buttons alongside drag handles. |
+| `ConfigurationService::importFromApp()` called multiple times (double-click) | Low | Disable "Initialize register" button after first click; re-enable only on error. |
diff --git a/openspec/changes/admin-user-settings/proposal.md b/openspec/changes/admin-user-settings/proposal.md
new file mode 100644
index 0000000..9966363
--- /dev/null
+++ b/openspec/changes/admin-user-settings/proposal.md
@@ -0,0 +1,72 @@
+# Change Proposal: admin-user-settings
+
+**Change ID:** admin-user-settings
+**Status:** proposed
+**Created:** 2026-04-02
+**Author:** Conduction Development Team
+
+---
+
+## Why
+
+Planix already has stub entry points for both admin and user settings (`lib/Settings/AdminSettings.php` exists; `src/views/settings/UserSettings.vue` is an empty placeholder), but neither is functional. Administrators have no way to configure app-level defaults (such as the default column set for new projects), verify the OpenRegister initialization status, or see the current app version from the Nextcloud administration interface. End users have no way to personalise their notification preferences or choose a default project view — meaning all notification emails are sent unconditionally and every project always opens in the system-default view regardless of user preference.
+
+The `tasks` change introduces `NotificationService` with a `SUBJECT_SETTING_MAP` pattern that is designed to check user settings before sending a notification. Without this change, those setting keys always resolve to the default (`true`) because no user can toggle them off. The `admin-user-settings` change closes both gaps: it makes the admin settings page genuinely useful and gives users a settings dialog that actually persists and influences app behaviour.
+
+---
+
+## What Changes
+
+Implement the admin settings page and user settings dialog for Planix:
+
+1. **Admin settings page** — enhance the existing `AdminSettings.php` template to render `CnVersionInfoCard` (app name, current version, update-available indicator), a "Default Project Configuration" section with an editable ordered column list (`default_columns`), and a "Register Setup" section showing OpenRegister initialization status with an "Initialize register" button. Settings stored via `IAppConfig`.
+2. **User settings dialog** — implement the currently empty `UserSettings.vue` as an `NcAppSettingsDialog` (not `NcDialog`) opened from the navigation gear icon. Dialog contains a Notification section (toggles for `notify_assigned`, `notify_due_reminder`) and a Display section (`default_view` selector). Settings stored via `OCP\IConfig`.
+3. **SettingsController enhancements** — add user settings read/write endpoints (`GET /settings/user`, `PUT /settings/user`) alongside the existing admin endpoints.
+4. **NotificationService integration** — align `SUBJECT_SETTING_MAP` keys in `NotificationService` with the user settings keys defined here (`notify_assigned`, `notify_due_reminder`, etc.) so toggling a preference immediately controls notification delivery.
+5. **Column list editor** — new `ColumnListEditor.vue` component for the admin settings page; supports add, remove, and drag-to-reorder operations on a list of column name strings.
+6. **Register initialization flow** — "Register Setup" section calls `ConfigurationService::importFromApp()` via a new admin-only endpoint; shows spinner during import and success/error feedback.
+7. **i18n** — all new strings added to `l10n/en.json` and `l10n/nl.json`.
+
+---
+
+## Capabilities
+
+### Modified Capabilities
+
+- **`admin-user-settings`** — implementing the full admin and user settings layer defined in `openspec/specs/admin-user-settings.md`. This change brings the capability from stub/placeholder state to fully functional: admin settings page with version card, column configuration, and register initialization; user settings dialog with notification toggles and view preference; backend endpoints; NotificationService integration.
+
+No new capabilities are introduced. The `admin-user-settings` capability was declared in the spec; this change completes the implementation.
+
+---
+
+## Impact
+
+### Files Changed
+
+| File | Change |
+|------|--------|
+| `lib/Settings/AdminSettings.php` | Modified — add template data: version, update status, default_columns, register init status |
+| `templates/admin-settings.php` | Modified — render `CnVersionInfoCard`, column list editor, register setup section |
+| `lib/Controller/SettingsController.php` | Modified — add `userIndex`, `userUpdate` actions; add `adminRegisterInit` action |
+| `appinfo/routes.php` | Modified — add user settings routes; add register init route |
+| `src/views/settings/UserSettings.vue` | Modified — implement full `NcAppSettingsDialog` with notification and display sections |
+| `src/views/settings/AdminSettings.vue` | New — Vue component rendering `CnVersionInfoCard` + `CnSettingsSection` groups |
+| `src/components/settings/ColumnListEditor.vue` | New — drag-to-reorder ordered column name list editor |
+| `src/store/settings.js` | New — Pinia store for user and admin settings (read/write via SettingsController) |
+| `src/navigation/MainMenu.vue` | Modified — wire gear icon to open `UserSettings.vue` dialog |
+| `lib/Service/NotificationService.php` | Modified — align `SUBJECT_SETTING_MAP` keys with user settings keys |
+| `l10n/en.json` | Modified — add all settings-related translation strings |
+| `l10n/nl.json` | Modified — add Dutch translations for all settings strings |
+
+### Risk
+
+Low-to-medium. Admin settings template modifications are additive. User settings dialog is a net-new implementation of an empty placeholder. The `SettingsController` enhancement adds actions without removing existing ones. The highest-risk step is aligning `NotificationService::SUBJECT_SETTING_MAP` keys with the user settings keys — if the key names diverge from what `tasks` change established, notifications will silently fail or fire unconditionally.
+
+The `ColumnListEditor` component uses drag-and-drop (Vue Draggable or similar). If `@conduction/nextcloud-vue` does not export a list-reorder primitive, a local component using the HTML5 Drag-and-Drop API must be used instead.
+
+### Dependencies
+
+- `register-schemas` must be applied first (OpenRegister register and schemas must exist for the init status check).
+- `tasks` change should be applied first or in parallel (to align `NotificationService::SUBJECT_SETTING_MAP` keys). If applied before `tasks`, the map is declared here and the `tasks` change imports it.
+- `@conduction/nextcloud-vue` must export `CnVersionInfoCard`, `CnSettingsSection`, `NcAppSettingsDialog` (or this change wraps `NcAppSettingsDialog` directly from `@nextcloud/vue`).
+- OpenRegister `ConfigurationService` must expose `importFromApp()` (confirmed in `register-schemas` spec).
diff --git a/openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md b/openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md
new file mode 100644
index 0000000..fd11868
--- /dev/null
+++ b/openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md
@@ -0,0 +1,300 @@
+# Delta Spec: admin-user-settings
+
+**Capability:** admin-user-settings
+**Change ID:** admin-user-settings
+**Delta type:** implementation
+**Base spec:** [openspec/specs/admin-user-settings.md](../../../../specs/admin-user-settings.md)
+**Status:** draft
+**Created:** 2026-04-02
+
+---
+
+## Summary
+
+This delta captures implementation-specific requirements added when building the admin settings page and user settings dialog. The base spec (`openspec/specs/admin-user-settings.md`) defines all business requirements, scenarios, user stories, and acceptance criteria. The delta below documents:
+
+1. Vue component patterns required by the implementation architecture (admin template mount strategy, `NcAppSettingsDialog` layout).
+2. `ColumnListEditor` component contract (drag-to-reorder, keyboard accessibility, v-model interface).
+3. `CnVersionInfoCard` integration requirements (data injection, update-check caching).
+4. Register initialization flow details (synchronous endpoint, double-click guard).
+5. `NotificationService` `SUBJECT_SETTING_MAP` key alignment.
+6. Pinia settings store patterns (optimistic update, error rollback).
+7. Loading and error state requirements.
+8. i18n requirements.
+
+All base spec requirements are implemented as-is. No base spec requirement is modified or removed.
+
+---
+
+## ADDED Requirements
+
+### Requirement: CnVersionInfoCard Integration [MVP]
+
+The admin settings page MUST use `CnVersionInfoCard` from `@conduction/nextcloud-vue` as the first rendered section.
+
+#### Scenario: Version card renders current version
+- GIVEN a Nextcloud admin opens Administration → Planix
+- WHEN `AdminSettings.vue` mounts
+- THEN `CnVersionInfoCard` MUST be the first visible element
+- AND it MUST display the app name ("Planix") and the current installed version (read from `data-app-version` attribute injected by `AdminSettings.php`)
+- AND the version MUST match the value returned by `\OCP\App::getAppVersion('planix')`
+
+#### Scenario: Version card — update available
+- GIVEN the PHP `AdminSettings.php` detects a newer version in the app store (cached, TTL 1 hour)
+- WHEN the admin page renders
+- THEN `CnVersionInfoCard` MUST receive `updateAvailable: true` and `updateVersion: "{newVersion}"`
+- AND the card MUST display an "Update available" indicator linking to the Nextcloud App Store entry for Planix
+- AND the update check result MUST be served from the `ICache` layer to avoid a live app store HTTP call on every page load
+
+#### Scenario: Version card — up to date
+- GIVEN no newer version is available
+- WHEN the admin page renders
+- THEN `CnVersionInfoCard` MUST show an "Up to date" indicator (no update link)
+
+---
+
+### Requirement: Admin Settings Vue Mount [MVP]
+
+The admin settings page MUST render the Vue `AdminSettings.vue` component inside the PHP template.
+
+#### Scenario: Vue component mounts in admin template
+- GIVEN a Nextcloud admin navigates to Administration → Planix
+- WHEN the PHP template `templates/admin-settings.php` is rendered
+- THEN the template MUST include a `` mount point
+- AND the template MUST load the `admin-settings.js` webpack entry point
+- AND `AdminSettings.vue` MUST mount onto `#planix-admin-settings` and read initial values from the `data-*` attributes (no additional network request for static values)
+
+#### Scenario: Admin settings fetch editable values on mount
+- GIVEN `AdminSettings.vue` has mounted
+- WHEN the component's `onMounted` hook runs
+- THEN the component MUST call `settingsStore.fetchAdminSettings()` to retrieve `default_columns` and other editable settings via `GET /planix/settings/admin`
+- AND a loading skeleton MUST be shown until the fetch resolves
+
+---
+
+### Requirement: Column List Editor Component [MVP]
+
+The `ColumnListEditor.vue` component MUST provide an accessible, ordered, editable list of column name strings.
+
+#### Scenario: Render column list
+- GIVEN `ColumnListEditor` receives `modelValue: ["To Do", "In Progress", "Review", "Done"]`
+- WHEN the component renders
+- THEN each column name MUST appear as a row with: a drag handle icon, an editable text input, and a remove button
+- AND the rows MUST be rendered in the order provided by `modelValue`
+
+#### Scenario: Add a column
+- GIVEN the `ColumnListEditor` is rendered
+- WHEN the admin clicks "Add column"
+- THEN a new empty text input row MUST be appended to the list
+- AND focus MUST move to the new input automatically
+- AND `update:modelValue` MUST be emitted with the new array including the empty string
+
+#### Scenario: Remove a column
+- GIVEN the list has 2 or more columns
+- WHEN the admin clicks the remove button on a row
+- THEN that row MUST be removed from the list
+- AND `update:modelValue` MUST be emitted with the updated array
+- AND the remove button MUST be disabled (visually and functionally) when only 1 column remains
+
+#### Scenario: Reorder via drag-and-drop
+- GIVEN the list has at least 2 columns
+- WHEN the admin drags a row to a new position using the drag handle
+- THEN the list order MUST update immediately (optimistic, no save button needed for visual update)
+- AND `update:modelValue` MUST be emitted with the reordered array
+
+#### Scenario: Reorder via keyboard (WCAG AA)
+- GIVEN the list has at least 2 columns
+- WHEN the admin focuses a row and clicks the "Move up" or "Move down" button
+- THEN the row MUST move one position in the indicated direction
+- AND `update:modelValue` MUST be emitted with the reordered array
+- AND focus MUST follow the moved row to maintain keyboard navigation context
+
+#### Scenario: Empty column name validation
+- GIVEN the admin clears a column name input (leaving it empty)
+- WHEN the "Save changes" button is clicked in the parent `AdminSettings.vue`
+- THEN the parent MUST validate that no column name is empty
+- AND if validation fails, an inline error MUST appear: `t('planix', 'Column name cannot be empty')`
+- AND the save request MUST NOT be sent
+
+---
+
+### Requirement: Register Initialization Flow [MVP]
+
+#### Scenario: Register already initialized
+- GIVEN OpenRegister has already been initialized for Planix (detected via `ConfigurationService::isInitialized()`)
+- WHEN the admin settings page renders
+- THEN the "Register Setup" section MUST show a green checkmark and the text "Register initialized"
+- AND the "Initialize register" button MUST NOT be shown (or shown as disabled with label "Already initialized")
+
+#### Scenario: Register not initialized
+- GIVEN OpenRegister is NOT initialized for Planix
+- WHEN the admin settings page renders
+- THEN the "Register Setup" section MUST show a warning indicator and the text "Register not initialized"
+- AND an "Initialize register" button MUST be visible and enabled
+
+#### Scenario: Trigger initialization
+- GIVEN the "Initialize register" button is enabled
+- WHEN the admin clicks it
+- THEN the button MUST immediately become disabled and show a spinner with label "Initializing…"
+- AND the frontend MUST call `settingsStore.initRegister()` which POSTs to `/planix/settings/admin/register-init`
+- AND the PHP endpoint MUST call `ConfigurationService::importFromApp()` synchronously
+- AND on success, the section status MUST update to "Register initialized"
+- AND a success toast MUST be shown: `t('planix', 'Register initialized successfully')`
+
+#### Scenario: Initialization failure
+- GIVEN the "Initialize register" request fails (API error or `importFromApp()` throws)
+- WHEN the store catches the error
+- THEN the button MUST re-enable with its original label
+- AND an error toast MUST be shown: `t('planix', 'Failed to initialize register')`
+- AND the section status indicator MUST remain in the "not initialized" state
+
+---
+
+### Requirement: NcAppSettingsDialog Layout [MVP]
+
+The user settings dialog MUST use `NcAppSettingsDialog` and be opened from the Planix navigation gear icon.
+
+#### Scenario: Open user settings dialog from gear icon
+- GIVEN a user is using Planix
+- WHEN the user clicks the gear icon in the Planix navigation sidebar (bottom navigation item)
+- THEN `UserSettings.vue` MUST open as an `NcAppSettingsDialog`
+- AND the dialog MUST show two sections in its sidebar: "Notifications" and "Display"
+- AND the first section ("Notifications") MUST be selected by default
+
+#### Scenario: Dialog navigation — switch section
+- GIVEN the user settings dialog is open on the "Notifications" section
+- WHEN the user clicks "Display" in the dialog's section sidebar
+- THEN the content area MUST transition to show the Display section content
+- AND the "Display" section item MUST be highlighted as active
+
+#### Scenario: Load user settings on open
+- GIVEN the user settings dialog is opened
+- WHEN `UserSettings.vue` mounts
+- THEN `settingsStore.fetchUserSettings()` MUST be called
+- AND while loading, each toggle and selector MUST render in a loading/skeleton state
+- AND after loading, each control MUST reflect the current saved value from `IConfig`
+
+---
+
+### Requirement: Notification Toggles [MVP]
+
+#### Scenario: Display notification toggles
+- GIVEN the user settings dialog is open on the "Notifications" section
+- WHEN the section content renders
+- THEN the following toggles MUST be present:
+ - "Notify me when a task is assigned to me" (`notify_assigned`, default: on)
+ - "Notify me 1 day before a task's due date" (`notify_due_reminder`, default: on)
+- AND each toggle MUST be an `NcCheckboxRadioSwitch` (toggle mode) from `@nextcloud/vue`
+- AND each toggle's state MUST reflect the value returned by `fetchUserSettings()`
+
+#### Scenario: Toggle notification preference — save immediately
+- GIVEN the "Notifications" section is rendered with toggles
+- WHEN the user clicks a toggle to change its state
+- THEN `settingsStore.updateUserSetting(key, value)` MUST be called immediately (no separate Save button for toggles)
+- AND the toggle MUST update optimistically (change is visible before server confirms)
+- AND on success, a brief confirmation indicator (or no feedback for toggle, per NC convention) is shown
+- AND on failure, the toggle MUST revert to its previous state and an error toast MUST appear
+
+#### Scenario: Notification service respects user preference
+- GIVEN a user has toggled `notify_assigned` to off
+- WHEN user B assigns a task to this user
+- THEN `NotificationService::notify('task_assigned', ...)` MUST check the user's `notify_assigned` IConfig value
+- AND MUST find `false` (or string `'no'`)
+- AND MUST NOT create or send the notification
+- AND the assignment MUST still succeed
+
+---
+
+### Requirement: Default View Selector [MVP]
+
+#### Scenario: Display default view selector
+- GIVEN the user settings dialog is open on the "Display" section
+- WHEN the section content renders
+- THEN a "Default view" label and a dropdown/radio selector MUST be present
+- AND the three options MUST be: "My Work" (value: `my-work`), "Kanban" (value: `kanban`), "Backlog" (value: `backlog`)
+- AND the selector MUST reflect the value returned by `fetchUserSettings()` (default: `my-work`)
+
+#### Scenario: Change default view
+- GIVEN the "Display" section is rendered
+- WHEN the user selects "Kanban" from the default view selector
+- THEN `settingsStore.updateUserSetting('default_view', 'kanban')` MUST be called immediately
+- AND the next time the user opens a project (without a saved route), Planix MUST navigate to the Kanban view
+- AND the setting MUST persist in `OCP\IConfig` across browser sessions
+
+---
+
+### Requirement: Settings Persistence and Backend [MVP]
+
+#### Scenario: Admin settings persist via IAppConfig
+- GIVEN the admin changes the default columns list and clicks "Save changes"
+- WHEN `settingsStore.updateAdminSettings({ default_columns: [...] })` calls `PUT /planix/settings/admin`
+- THEN the backend MUST call `IAppConfig::setValueArray('planix', 'default_columns', [...])` (or `setValueString` with JSON-encoded value)
+- AND subsequent reads via `GET /planix/settings/admin` MUST return the updated value
+- AND new projects created after the save MUST use the updated column set
+
+#### Scenario: User settings persist via IConfig
+- GIVEN a user toggles `notify_assigned` to off
+- WHEN `PUT /planix/settings/user` is called with `{ notify_assigned: false }`
+- THEN the backend MUST call `IConfig::setUserValue($uid, 'planix', 'notify_assigned', 'no')`
+- AND subsequent calls to `GET /planix/settings/user` MUST return `notify_assigned: false`
+- AND the toggle MUST still be off after the user closes and reopens the dialog
+
+#### Scenario: User settings survive browser restart
+- GIVEN a user has set `notify_assigned = false` and `default_view = kanban`
+- WHEN the user closes the browser, clears session cookies, and returns to Planix
+- THEN `GET /planix/settings/user` MUST still return `notify_assigned: false` and `default_view: "kanban"` (stored server-side in `IConfig`, not in browser storage)
+
+---
+
+### Requirement: Admin Access Control [MVP]
+
+#### Scenario: Admin endpoint blocked for regular users
+- GIVEN a regular (non-admin) Nextcloud user
+- WHEN they call `GET /planix/settings/admin`, `PUT /planix/settings/admin`, or `POST /planix/settings/admin/register-init`
+- THEN Nextcloud MUST return HTTP 403 Forbidden
+- AND the admin settings link MUST NOT appear in the user's Nextcloud settings navigation
+
+#### Scenario: User settings accessible to all authenticated users
+- GIVEN any authenticated Nextcloud user
+- WHEN they call `GET /planix/settings/user` or `PUT /planix/settings/user`
+- THEN the request MUST succeed (200)
+- AND the response MUST only contain settings for the calling user (uid from session)
+
+---
+
+### Requirement: i18n Coverage [MVP]
+
+#### Scenario: All user-visible strings use t()
+- GIVEN any Vue component or PHP file in this change
+- WHEN it contains a string visible to the end user
+- THEN the string MUST be wrapped in `t('planix', '...')` (Vue) or `$this->l10n->t('...')` (PHP)
+- AND the key MUST be present in both `l10n/en.json` and `l10n/nl.json`
+- AND NO English text MUST appear as a hardcoded string in templates or PHP output
+
+#### Scenario: Dutch translation completeness
+- GIVEN the `l10n/nl.json` file
+- WHEN checked against `l10n/en.json`
+- THEN every key present in `en.json` introduced by this change MUST also be present in `nl.json`
+- AND all Dutch translations MUST be human-readable Dutch (no English placeholders, no machine-translation artifacts)
+
+---
+
+### Requirement: Loading and Error States [MVP]
+
+#### Scenario: Admin settings loading
+- GIVEN `AdminSettings.vue` is mounted
+- WHEN `fetchAdminSettings()` is in progress
+- THEN the `ColumnListEditor` area MUST show a skeleton loading state
+- AND the "Save changes" button MUST be disabled until loading completes
+
+#### Scenario: User settings loading
+- GIVEN the `UserSettings.vue` dialog has opened
+- WHEN `fetchUserSettings()` is in progress
+- THEN all toggles and selectors MUST show a loading/disabled state
+- AND no stale values MUST be shown (no flash of default values before server values load)
+
+#### Scenario: Save failure — optimistic rollback
+- GIVEN the user changes a setting (toggle or selector)
+- WHEN `updateUserSetting()` applies the change optimistically AND the API call fails
+- THEN the control MUST revert to its previous state
+- AND an error toast MUST be shown: `t('planix', 'Failed to save settings')`
diff --git a/openspec/changes/admin-user-settings/tasks.md b/openspec/changes/admin-user-settings/tasks.md
new file mode 100644
index 0000000..04a5955
--- /dev/null
+++ b/openspec/changes/admin-user-settings/tasks.md
@@ -0,0 +1,346 @@
+# Tasks: admin-user-settings
+
+**Change ID:** admin-user-settings
+**Status:** draft
+**Created:** 2026-04-02
+
+---
+
+## Implementation Tasks
+
+### Task 1: Setup and Prerequisites
+- **spec_ref**: `openspec/specs/admin-user-settings.md`
+- **files**: `src/store/settings.js`, `appinfo/routes.php`
+- **acceptance_criteria**:
+ - GIVEN the developer inspects `@conduction/nextcloud-vue` WHEN checking exports THEN `CnVersionInfoCard` and `CnSettingsSection` are available
+ - GIVEN the developer checks `@nextcloud/vue` WHEN checking exports THEN `NcAppSettingsDialog` and `NcCheckboxRadioSwitch` are available
+ - GIVEN the developer verifies dependencies WHEN checking `lib/Settings/AdminSettings.php` THEN the file exists and is registered in `lib/AppInfo/Application.php`
+ - GIVEN the developer verifies WHEN checking `src/views/settings/UserSettings.vue` THEN the file exists (even if empty placeholder)
+- [ ] Confirm `@conduction/nextcloud-vue` exports: `CnVersionInfoCard`, `CnSettingsSection`
+- [ ] Confirm `@nextcloud/vue` exports: `NcAppSettingsDialog`, `NcCheckboxRadioSwitch`
+- [ ] Confirm `lib/Settings/AdminSettings.php` is registered in `Application.php`
+- [ ] Create directory `src/components/settings/` if not present
+- [ ] Create `src/store/settings.js` stub with empty state and no-op actions
+
+---
+
+### Task 2: SettingsController — Admin Endpoints
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-settings-persistence-and-backend`
+- **files**: `lib/Controller/SettingsController.php`, `appinfo/routes.php`
+- **acceptance_criteria**:
+ - GIVEN an admin calls `GET /planix/settings/admin` WHEN the controller handles the request THEN a JSON response with `default_columns`, `register_initialized`, `app_version`, `update_available`, `update_version` is returned
+ - GIVEN an admin calls `PUT /planix/settings/admin` with `{ default_columns: ["A","B"] }` WHEN the controller handles the request THEN `IAppConfig::setValueString('planix', 'default_columns', '["A","B"]')` is called and HTTP 200 is returned
+ - GIVEN a non-admin user calls any `/planix/settings/admin` endpoint WHEN Nextcloud processes the request THEN HTTP 403 is returned (enforced by `@AdminRequired` annotation)
+- [ ] Add `adminIndex(): JSONResponse` action to `SettingsController` (or create it if missing)
+ - Read `default_columns` from `IAppConfig` (default: `["To Do","In Progress","Review","Done"]`)
+ - Read `app_version` via `\OCP\App::getAppVersion('planix')`
+ - Read `register_initialized` from `ConfigurationService::isInitialized()`
+ - Read `update_available` and `update_version` from `IAppManager` / app info (cached)
+ - Annotate with `@AdminRequired`
+- [ ] Add `adminUpdate(array $settings): JSONResponse` action
+ - Accept `default_columns` (JSON-decode, validate is array of strings)
+ - Store via `IAppConfig::setValueString`
+ - Annotate with `@AdminRequired`
+- [ ] Add routes to `appinfo/routes.php`:
+ - `['name' => 'settings#adminIndex', 'url' => '/settings/admin', 'verb' => 'GET']`
+ - `['name' => 'settings#adminUpdate', 'url' => '/settings/admin', 'verb' => 'PUT']`
+- [ ] Run `composer check:strict`
+- [ ] Test
+
+---
+
+### Task 3: SettingsController — User Endpoints
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-settings-persistence-and-backend`
+- **files**: `lib/Controller/SettingsController.php`, `appinfo/routes.php`
+- **acceptance_criteria**:
+ - GIVEN any authenticated user calls `GET /planix/settings/user` WHEN the controller handles the request THEN a JSON response with all user setting keys and their values is returned for the calling user only
+ - GIVEN a user calls `PUT /planix/settings/user` with `{ notify_assigned: false }` WHEN the controller handles the request THEN `IConfig::setUserValue($uid, 'planix', 'notify_assigned', 'no')` is called and HTTP 200 is returned
+ - GIVEN user A calls `PUT /planix/settings/user` WHEN another user B reads their settings THEN user B's settings are unaffected
+- [ ] Add `userIndex(): JSONResponse` action to `SettingsController`
+ - Read all user settings from `IConfig::getUserValue($uid, 'planix', $key, $default)`
+ - Return boolean values as PHP booleans (not strings) in JSON
+ - No admin annotation required
+- [ ] Add `userUpdate(array $settings): JSONResponse` action
+ - Accept any subset of user setting keys; ignore unknown keys
+ - Store booleans as `'yes'`/`'no'` strings via `IConfig::setUserValue`
+ - Store `default_view` as string value directly
+- [ ] Add routes to `appinfo/routes.php`:
+ - `['name' => 'settings#userIndex', 'url' => '/settings/user', 'verb' => 'GET']`
+ - `['name' => 'settings#userUpdate', 'url' => '/settings/user', 'verb' => 'PUT']`
+- [ ] Run `composer check:strict`
+- [ ] Test
+
+---
+
+### Task 4: SettingsController — Register Init Endpoint
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-register-initialization-flow`
+- **files**: `lib/Controller/SettingsController.php`, `appinfo/routes.php`
+- **acceptance_criteria**:
+ - GIVEN an admin calls `POST /planix/settings/admin/register-init` WHEN the controller handles the request THEN `ConfigurationService::importFromApp()` is called and HTTP 200 with `{ success: true }` is returned
+ - GIVEN `importFromApp()` throws an exception WHEN the controller handles the error THEN HTTP 500 with `{ success: false, message: "..." }` is returned
+ - GIVEN a non-admin user calls this endpoint WHEN Nextcloud processes the request THEN HTTP 403 is returned
+- [ ] Add `adminRegisterInit(): JSONResponse` action to `SettingsController`
+ - Inject `ConfigurationService` via constructor
+ - Call `$this->configurationService->importFromApp()` in try/catch
+ - Return `JSONResponse(['success' => true])` on success
+ - Return `JSONResponse(['success' => false, 'message' => $e->getMessage()], 500)` on failure
+ - Annotate with `@AdminRequired`
+- [ ] Add route: `['name' => 'settings#adminRegisterInit', 'url' => '/settings/admin/register-init', 'verb' => 'POST']`
+- [ ] Run `composer check:strict`
+- [ ] Test
+
+---
+
+### Task 5: Pinia Settings Store
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-loading-and-error-states`
+- **files**: `src/store/settings.js`
+- **acceptance_criteria**:
+ - GIVEN `useSettingsStore()` is called WHEN the store is initialized THEN it exposes: `adminSettings`, `userSettings`, `adminLoading`, `userLoading`, `error`
+ - GIVEN `fetchUserSettings()` is called WHEN the API responds THEN `userSettings` is populated with the server values
+ - GIVEN `updateUserSetting('notify_assigned', false)` is called WHEN the API call is in progress THEN `userSettings.notify_assigned` immediately reflects `false` (optimistic update)
+ - GIVEN `updateUserSetting` is called and the API call fails WHEN the store catches the error THEN the key reverts to its previous value
+ - GIVEN `initRegister()` is called WHEN the API call succeeds THEN `adminSettings.register_initialized` is set to `true`
+- [ ] Implement `fetchAdminSettings()` — GET `/planix/settings/admin`, set `adminSettings`, handle loading/error states
+- [ ] Implement `updateAdminSettings(data)` — PUT `/planix/settings/admin`; update `adminSettings` on success
+- [ ] Implement `initRegister()` — POST `/planix/settings/admin/register-init`; set `adminSettings.register_initialized = true` on success
+- [ ] Implement `fetchUserSettings()` — GET `/planix/settings/user`, set `userSettings`, handle loading/error
+- [ ] Implement `updateUserSetting(key, value)` — optimistic update + PUT `/planix/settings/user`; revert on failure
+- [ ] Test
+
+---
+
+### Task 6: AdminSettings.php — Page Data Injection
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-cnversioninfocard-integration`
+- **files**: `lib/Settings/AdminSettings.php`, `templates/admin-settings.php`
+- **acceptance_criteria**:
+ - GIVEN an admin navigates to Administration → Planix WHEN the PHP template renders THEN the HTML contains ``
+ - GIVEN a newer version exists in the app store WHEN `AdminSettings.php` builds the template data THEN `update_available` is `true` and `update_version` is the new version string (served from ICache, TTL 1 hour)
+ - GIVEN the app store check fails WHEN the template renders THEN `update_available` defaults to `false` gracefully
+- [ ] Modify `lib/Settings/AdminSettings.php`:
+ - Inject `IAppManager`, `ICacheFactory`, `ConfigurationService` via constructor
+ - Read `appVersion` via `\OCP\App::getAppVersion('planix')`
+ - Check update status via `IAppManager::getAppInfo()` or app store endpoint; cache result for 1 hour
+ - Read `registerInitialized` via `ConfigurationService::isInitialized()`
+ - Pass all values to `TemplateResponse`
+- [ ] Modify `templates/admin-settings.php`:
+ - Output ``
+ - Include `admin-settings.js` script via `\OCP\Util::addScript('planix', 'admin-settings')`
+- [ ] Run `composer check:strict`
+- [ ] Test
+
+---
+
+### Task 7: AdminSettings.vue — Vue Component
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-admin-settings-vue-mount`
+- **files**: `src/views/settings/AdminSettings.vue`, webpack config / `appinfo/assets.php`
+- **acceptance_criteria**:
+ - GIVEN `AdminSettings.vue` mounts onto `#planix-admin-settings` WHEN the component initializes THEN it reads `data-app-version`, `data-update-available`, `data-register-initialized` from the mount div and passes them to sub-components
+ - GIVEN `fetchAdminSettings()` is loading WHEN the component renders the column editor section THEN a skeleton loading state is shown
+ - GIVEN settings are loaded WHEN the admin changes a column and clicks "Save changes" THEN `updateAdminSettings` is called; a "Saving…" state appears on the button; on success the button reverts to "Save changes"
+- [ ] Create `src/views/settings/AdminSettings.vue`
+ - On `onMounted`: read `data-*` from mount div; call `settingsStore.fetchAdminSettings()`
+ - Render `CnVersionInfoCard` with version and update props
+ - Render `CnSettingsSection` "Default Project Configuration" containing `ColumnListEditor`
+ - Render `CnSettingsSection` "Register Setup" with init status and button
+ - "Save changes" button calls `updateAdminSettings({ default_columns: columns.value })`
+- [ ] Add webpack entry `admin-settings.js` that imports and mounts `AdminSettings.vue`
+- [ ] Register the entry point so `\OCP\Util::addScript('planix', 'admin-settings')` resolves correctly
+- [ ] Test
+
+---
+
+### Task 8: ColumnListEditor Component
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-column-list-editor-component`
+- **files**: `src/components/settings/ColumnListEditor.vue`
+- **acceptance_criteria**:
+ - GIVEN `modelValue: ["To Do", "In Progress", "Done"]` WHEN `ColumnListEditor` renders THEN 3 rows appear with correct labels and a drag handle, editable text input, and remove button on each
+ - GIVEN the admin drags row 2 above row 1 WHEN the drag completes THEN `update:modelValue` is emitted with the reordered array
+ - GIVEN the admin clicks "Move up" on row 2 WHEN the action completes THEN `update:modelValue` is emitted with row 2 in position 1; focus follows the moved row
+ - GIVEN only 1 row remains WHEN the component renders THEN the remove button is disabled
+ - GIVEN an empty column name exists WHEN the parent validates before saving THEN the parent can check `modelValue.some(v => v.trim() === '')` and show an error
+- [ ] Create `src/components/settings/ColumnListEditor.vue`
+ - Props: `{ modelValue: Array, disabled: Boolean }`
+ - Emit: `['update:modelValue']`
+ - Use `vue-draggable-plus` (SortableJS wrapper) if available; otherwise HTML5 DnD API
+ - Each row: drag handle (`IconDragVertical`), ``, Move Up button, Move Down button, Remove button
+ - "Add column" button appends empty string and focuses the new input
+ - Remove button disabled when `modelValue.length <= 1`
+ - Move Up disabled for first row; Move Down disabled for last row
+- [ ] Keyboard accessibility: Move Up/Down buttons must be focusable and operable via Enter/Space
+- [ ] Use CSS variables for colors; no hardcoded color values
+- [ ] Test
+
+---
+
+### Task 9: UserSettings.vue — NcAppSettingsDialog
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-ncappsettingsdialog-layout`
+- **files**: `src/views/settings/UserSettings.vue`
+- **acceptance_criteria**:
+ - GIVEN the gear icon is clicked in Planix navigation WHEN `UserSettings.vue` is opened THEN it renders as an `NcAppSettingsDialog` (NOT `NcDialog`)
+ - GIVEN the dialog opens WHEN `onMounted` runs THEN `settingsStore.fetchUserSettings()` is called; toggles show loading state until resolved
+ - GIVEN settings are loaded WHEN the dialog renders THEN the "Notifications" section shows two toggles with correct initial values; "Display" section shows the default view selector with correct initial value
+ - GIVEN the user switches to the "Display" section WHEN the section nav item is clicked THEN the content area transitions to the Display content
+- [ ] Implement `src/views/settings/UserSettings.vue` (replaces empty placeholder)
+ - Use `NcAppSettingsDialog` with `sections` prop: `[{ id: 'notifications', name: t('planix', 'Notifications') }, { id: 'display', name: t('planix', 'Display') }]`
+ - On `onMounted`: call `settingsStore.fetchUserSettings()`
+ - Render Notifications section: two `NcCheckboxRadioSwitch` (type="switch") for `notify_assigned` and `notify_due_reminder`
+ - Render Display section: label + `NcSelect` or `NcCheckboxRadioSwitch` (type="radio") for `default_view`
+ - Each control binds to `settingsStore.userSettings[key]` and calls `settingsStore.updateUserSetting(key, value)` on change
+- [ ] Wire gear icon in `MainMenu.vue` to open the dialog (v-model or direct `open` call)
+- [ ] Test
+
+---
+
+### Task 10: Wire Gear Icon in MainMenu.vue
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-ncappsettingsdialog-layout`
+- **files**: `src/navigation/MainMenu.vue`
+- **acceptance_criteria**:
+ - GIVEN the user is in Planix WHEN they look at the bottom of the navigation sidebar THEN a gear icon (`NcAppNavigationItem` or `NcButton` with gear icon) is visible
+ - GIVEN the user clicks the gear icon WHEN the click handler fires THEN `showUserSettings.value = true` causes `UserSettings.vue` to render and open
+- [ ] Add a gear icon navigation item at the bottom of `MainMenu.vue` using the `#footer` slot or equivalent
+- [ ] Add `const showUserSettings = ref(false)` and toggle on gear icon click
+- [ ] Include ``
+- [ ] Test
+
+---
+
+### Task 11: NotificationService — Align SUBJECT_SETTING_MAP Keys
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-notification-toggles`
+- **files**: `lib/Service/NotificationService.php`
+- **acceptance_criteria**:
+ - GIVEN `NotificationService::SUBJECT_SETTING_MAP` is inspected WHEN the map is read THEN `'task_assigned'` maps to `'notify_assigned'` (not `'notify_task_assigned'`)
+ - GIVEN `NotificationService::SUBJECT_SETTING_MAP` is inspected WHEN the map is read THEN `'task_due_soon'` maps to `'notify_due_reminder'`
+ - GIVEN user B has `notify_assigned = 'no'` in `IConfig` WHEN `notify('task_assigned', ..., userBUid)` is called THEN no notification is sent
+ - GIVEN user B has `notify_due_reminder = 'no'` WHEN `notify('task_due_soon', ..., userBUid)` is called THEN no notification is sent
+- [ ] Update `SUBJECT_SETTING_MAP` in `lib/Service/NotificationService.php`:
+ ```php
+ private const SUBJECT_SETTING_MAP = [
+ 'task_assigned' => 'notify_assigned',
+ 'task_due_soon' => 'notify_due_reminder',
+ // V1 — declared but not triggered in MVP:
+ 'task_overdue' => 'notify_overdue',
+ 'task_commented' => 'notify_commented',
+ 'task_status_changed' => 'notify_status_changed',
+ ];
+ ```
+- [ ] Verify default value logic: `IConfig::getUserValue($uid, 'planix', 'notify_assigned', 'yes')` returns `'yes'` for users who have never toggled the setting (correct default-on behaviour)
+- [ ] Run `composer check:strict`
+- [ ] Test
+
+---
+
+### Task 12: i18n — English Strings
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-i18n-coverage`
+- **files**: `l10n/en.json`
+- **acceptance_criteria**:
+ - GIVEN the `l10n/en.json` file WHEN inspected THEN all strings listed in the i18n inventory in `design.md` are present as keys
+ - GIVEN any Vue template or PHP file in this change WHEN all user-visible strings are checked THEN each uses `t('planix', '...')` / `$this->l10n->t('...')` and the key exists in `en.json`
+- [ ] Add all admin and user settings strings to `l10n/en.json` (see i18n inventory in `design.md`)
+- [ ] Verify no hardcoded English strings remain in any new or modified component or PHP file
+- [ ] Test
+
+---
+
+### Task 13: i18n — Dutch Translations
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-i18n-coverage`
+- **files**: `l10n/nl.json`
+- **acceptance_criteria**:
+ - GIVEN the `l10n/nl.json` file WHEN compared to `l10n/en.json` THEN every key added by this change in `en.json` also exists in `nl.json`
+ - GIVEN the Dutch translations WHEN reviewed THEN they are natural Dutch (not literal translations or English placeholders)
+- [ ] Add Dutch translations for all settings strings to `l10n/nl.json`
+- [ ] Key translations:
+ - `Planix Settings` → `Planix instellingen`
+ - `Default Project Configuration` → `Standaard projectconfiguratie`
+ - `Default columns for new projects` → `Standaard kolommen voor nieuwe projecten`
+ - `Add column` → `Kolom toevoegen`
+ - `Remove column` → `Kolom verwijderen`
+ - `Move up` → `Omhoog verplaatsen`
+ - `Move down` → `Omlaag verplaatsen`
+ - `Save changes` → `Wijzigingen opslaan`
+ - `Register Setup` → `Register instellen`
+ - `Register initialized` → `Register geïnitialiseerd`
+ - `Register not initialized` → `Register niet geïnitialiseerd`
+ - `Initialize register` → `Register initialiseren`
+ - `Notifications` → `Meldingen`
+ - `Notify me when a task is assigned to me` → `Stuur een melding wanneer een taak aan mij is toegewezen`
+ - `Notify me 1 day before a task's due date` → `Stuur een melding 1 dag voor de vervaldatum van een taak`
+ - `Display` → `Weergave`
+ - `Default view` → `Standaardweergave`
+ - `My Work` → `Mijn werk`
+ - `Settings saved` → `Instellingen opgeslagen`
+ - `Failed to save settings` → `Instellingen konden niet worden opgeslagen`
+- [ ] Test
+
+---
+
+### Task 14: BUG — Fix /api/health and /api/metrics Endpoints (from test-app 2026-04-04)
+- **spec_ref**: `openspec/specs/admin-user-settings.md`
+- **files**: `lib/Controller/SettingsController.php`, `appinfo/routes.php`
+- **acceptance_criteria**:
+ - GIVEN an authenticated user calls `GET /index.php/apps/planix/api/health` WHEN the controller handles the request THEN HTTP 200 is returned with a JSON health status (not 500)
+ - GIVEN an authenticated user calls `GET /index.php/apps/planix/api/metrics` WHEN the controller handles the request THEN HTTP 200 is returned with a JSON metrics object (not 500)
+- **bug_details**: API test agent found both `/api/health` and `/api/metrics` return HTTP 500 Internal Server Error. Either the routes are not defined, the controller actions throw unhandled exceptions, or required dependencies are not injected.
+- **severity**: MEDIUM
+- [ ] Check if `/api/health` and `/api/metrics` routes exist in `appinfo/routes.php`
+- [ ] If routes exist: check the controller action for unhandled exceptions or missing dependencies
+- [ ] If routes don't exist: either add them or remove them from the app (if not part of the spec)
+- [ ] Test
+
+---
+
+### Task 15: BUG — Admin Settings Route Returns 404 (from test-app 2026-04-04)
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-admin-settings-vue-mount`
+- **files**: `lib/Settings/AdminSettings.php`, `lib/AppInfo/Application.php`
+- **acceptance_criteria**:
+ - GIVEN an admin navigates to `/settings/admin/planix` WHEN the page loads THEN the Planix admin settings panel renders (not a 404)
+ - GIVEN `AdminSettings.php` WHEN registered in `Application.php` THEN it is registered via `ISettingsManager::registerSettings()`
+- **bug_details**: All test agents found that `/settings/admin/planix` returns 404. The app has an internal settings page at `/index.php/apps/planix/settings`, but the standard Nextcloud admin settings integration is missing.
+- **severity**: MEDIUM
+- [ ] Verify `AdminSettings.php` implements `ISettings` and is registered in `Application.php` via `$context->registerSettings()`
+- [ ] Check that the section ID matches `planix` and the priority is set correctly
+- [ ] If AdminSettings.php doesn't exist yet: this is expected and will be implemented in Task 6-7 of this change
+- [ ] Test
+
+---
+
+### Task 16: BUG — Settings Form Labels Use div Instead of label Elements (from test-app 2026-04-04)
+- **spec_ref**: `openspec/changes/admin-user-settings/specs/admin-user-settings/spec.md#requirement-ncappsettingsdialog-layout`
+- **files**: `src/views/settings/AdminSettings.vue`, `src/views/settings/UserSettings.vue`
+- **acceptance_criteria**:
+ - GIVEN any form field in admin or user settings WHEN inspected in the DOM THEN it has a proper `