diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..000962b
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,26 @@
+# 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.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/appinfo/info.xml b/appinfo/info.xml
index ea167ee..158458b 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -34,7 +34,7 @@ Vrij en open source onder de EUPL-1.2-licentie.
**Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl.
]]>
- 0.1.0
+ 0.2.0
eupl
Conduction
Planix
@@ -69,4 +69,13 @@ Vrij en open source onder de EUPL-1.2-licentie.
OCA\Planix\Settings\AdminSettings
OCA\Planix\Sections\SettingsSection
+
+
+
+ OCA\Planix\Repair\InitializeSettings
+
+
+ OCA\Planix\Repair\InitializeSettings
+
+
diff --git a/docs/features/README.md b/docs/features/README.md
new file mode 100644
index 0000000..103873c
--- /dev/null
+++ b/docs/features/README.md
@@ -0,0 +1,9 @@
+# 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) |
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/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/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/Service/SettingsService.php b/lib/Service/SettingsService.php
index 16f55e3..60974c6 100644
--- a/lib/Service/SettingsService.php
+++ b/lib/Service/SettingsService.php
@@ -139,8 +139,37 @@ 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');
diff --git a/lib/Settings/planix_register.json b/lib/Settings/planix_register.json
index 004cffe..61ea938 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.0"
},
"x-openregister": {
"type": "application",
@@ -13,30 +13,530 @@
},
"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"
+ }
+ },
"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/changes/register-schemas/.openspec.yaml b/openspec/changes/archive/2026-04-03-register-schemas/.openspec.yaml
similarity index 100%
rename from openspec/changes/register-schemas/.openspec.yaml
rename to openspec/changes/archive/2026-04-03-register-schemas/.openspec.yaml
diff --git a/openspec/changes/register-schemas/design.md b/openspec/changes/archive/2026-04-03-register-schemas/design.md
similarity index 100%
rename from openspec/changes/register-schemas/design.md
rename to openspec/changes/archive/2026-04-03-register-schemas/design.md
diff --git a/openspec/changes/archive/2026-04-03-register-schemas/plan.json b/openspec/changes/archive/2026-04-03-register-schemas/plan.json
new file mode 100644
index 0000000..9155bc2
--- /dev/null
+++ b/openspec/changes/archive/2026-04-03-register-schemas/plan.json
@@ -0,0 +1,153 @@
+{
+ "change": "register-schemas",
+ "project": "planix",
+ "repo": "ConductionNL/planix",
+ "created": "2026-04-02",
+ "tracking_issue": 2,
+ "tasks": [
+ {
+ "id": 1,
+ "title": "Define task schema in planix_register.json",
+ "description": "Remove the example placeholder entry and add the task schema with all properties from design.md.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["All properties from design.md included", "required: [\"title\", \"status\"]"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 2,
+ "title": "Define project schema in planix_register.json",
+ "description": "Add the project schema with all properties.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["All properties included", "required: [\"title\", \"status\"]"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 3,
+ "title": "Define column schema in planix_register.json",
+ "description": "Add the column schema with all properties.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["All properties included", "required: [\"title\", \"project\", \"order\"]"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 4,
+ "title": "Define timeEntry schema in planix_register.json",
+ "description": "Add the timeEntry schema with all properties.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["All properties included", "required: [\"task\", \"user\", \"duration\", \"date\"]"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 5,
+ "title": "Define label schema in planix_register.json",
+ "description": "Add the label schema with all properties.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["All properties included", "required: [\"title\", \"color\"]"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 6,
+ "title": "Add seed data — Labels (5 objects)",
+ "description": "Add 5 Label seed objects: Bug, Feature, Docs, Design, Infrastructure.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["5 label objects with correct colors"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 7,
+ "title": "Add seed data — Projects (3 objects)",
+ "description": "Add 3 Project seed objects.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["3 project objects with realistic data"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 8,
+ "title": "Add seed data — Columns (12 objects)",
+ "description": "Add 4 Column seed objects per project (12 total).",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["12 column objects, 4 per project", "Correct WIP limits and order"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 9,
+ "title": "Add seed data — Tasks (5 objects)",
+ "description": "Add 5 Task seed objects as specified in design.md.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["5 task objects with assignees, priorities, due dates"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 10,
+ "title": "Add seed data — Time Entries (3 objects)",
+ "description": "Add 3 TimeEntry seed objects.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["3 time entries referencing task seeds"],
+ "files_likely_affected": ["lib/Settings/planix_register.json"]
+ },
+ {
+ "id": 11,
+ "title": "Bump register version from 0.1.0 to 0.2.0",
+ "description": "Update info.version and SettingsService version constant.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["Version 0.2.0 in planix_register.json", "SettingsService triggers re-import"],
+ "files_likely_affected": ["lib/Settings/planix_register.json", "lib/Service/SettingsService.php"]
+ },
+ {
+ "id": 12,
+ "title": "Remove example schema reference from DeepLinkRegistrationListener",
+ "description": "Remove or replace references to the example schema slug.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["No code references to example schema slug"],
+ "files_likely_affected": ["lib/Listener/DeepLinkRegistrationListener.php"]
+ },
+ {
+ "id": 13,
+ "title": "Verify import in local dev environment",
+ "description": "Confirm all schemas and seed objects are created correctly.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["5 schemas present", "All seed objects created", "Version 0.2.0 in appconfig"],
+ "files_likely_affected": []
+ },
+ {
+ "id": 14,
+ "title": "Verify idempotency",
+ "description": "Confirm re-import does not create duplicates.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["Object counts unchanged after second import"],
+ "files_likely_affected": []
+ },
+ {
+ "id": 15,
+ "title": "Verify schema validation",
+ "description": "Confirm invalid data is rejected with HTTP 400.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["POST without title returns 400", "Invalid status enum returns 400"],
+ "files_likely_affected": []
+ },
+ {
+ "id": 16,
+ "title": "Run composer check:strict",
+ "description": "Fix any quality check issues.",
+ "status": "pending",
+ "spec_ref": "register-schemas",
+ "acceptance_criteria": ["All PHPCS, PHPMD, Psalm, PHPStan checks pass"],
+ "files_likely_affected": []
+ }
+ ]
+}
diff --git a/openspec/changes/register-schemas/proposal.md b/openspec/changes/archive/2026-04-03-register-schemas/proposal.md
similarity index 100%
rename from openspec/changes/register-schemas/proposal.md
rename to openspec/changes/archive/2026-04-03-register-schemas/proposal.md
diff --git a/openspec/changes/register-schemas/specs/register-schemas/spec.md b/openspec/changes/archive/2026-04-03-register-schemas/specs/register-schemas/spec.md
similarity index 100%
rename from openspec/changes/register-schemas/specs/register-schemas/spec.md
rename to openspec/changes/archive/2026-04-03-register-schemas/specs/register-schemas/spec.md
diff --git a/openspec/changes/register-schemas/tasks.md b/openspec/changes/archive/2026-04-03-register-schemas/tasks.md
similarity index 81%
rename from openspec/changes/register-schemas/tasks.md
rename to openspec/changes/archive/2026-04-03-register-schemas/tasks.md
index da83905..49e60e3 100644
--- a/openspec/changes/register-schemas/tasks.md
+++ b/openspec/changes/archive/2026-04-03-register-schemas/tasks.md
@@ -8,53 +8,53 @@
## Tasks
-- [ ] 1. **Define `task` schema in `planix_register.json`**
+- [x] 1. **Define `task` schema in `planix_register.json`**
Remove the `example` placeholder entry from `components.schemas` and add the `task` schema with all properties from design.md: `title`, `description`, `status` (enum + default), `priority` (enum + default), `project`, `zaakUuid`, `column`, `columnOrder`, `assignedTo`, `dueDate`, `startDate`, `estimatedDuration`, `percentComplete`, `labels`, `parent`, `calendarEventUid`, `completedAt`. Set `required: ["title", "status"]`.
-- [ ] 2. **Define `project` schema in `planix_register.json`**
+- [x] 2. **Define `project` schema in `planix_register.json`**
Add the `project` schema with all properties: `title`, `description`, `status` (enum + default), `color`, `icon`, `members`, `defaultAssignee`, `caseReference`, `labels`. Set `required: ["title", "status"]`.
-- [ ] 3. **Define `column` schema in `planix_register.json`**
+- [x] 3. **Define `column` schema in `planix_register.json`**
Add the `column` schema with all properties: `title`, `project`, `order`, `wipLimit`, `color`, `type` (enum + default). Set `required: ["title", "project", "order"]`.
-- [ ] 4. **Define `timeEntry` schema in `planix_register.json`**
+- [x] 4. **Define `timeEntry` schema in `planix_register.json`**
Add the `timeEntry` schema with all properties: `task`, `user`, `duration`, `date`, `description`. Set `required: ["task", "user", "duration", "date"]`.
-- [ ] 5. **Define `label` schema in `planix_register.json`**
+- [x] 5. **Define `label` schema in `planix_register.json`**
Add the `label` schema with all properties: `title`, `color` (default `"#4376FC"`), `description`. Set `required: ["title", "color"]`.
-- [ ] 6. **Add seed data — Labels (5 objects)**
+- [x] 6. **Add seed data — Labels (5 objects)**
Add seed data section to `planix_register.json` (or the OpenRegister seed mechanism used by the project) with 5 Label objects: Bug (#E74C3C), Feature (#4376FC), Docs (#27AE60), Design (#9B59B6), Infrastructure (#F39C12). Each object uses the `@self` envelope with `register: planix`, `schema: label`, and a slug.
-- [ ] 7. **Add seed data — Projects (3 objects)**
+- [x] 7. **Add seed data — Projects (3 objects)**
Add 3 Project seed objects: `client-portal-v2`, `infrastructure-migration`, `onboarding-automation`. Include realistic titles, descriptions, member lists, colors, and icons per design.md.
-- [ ] 8. **Add seed data — Columns (12 objects — 4 per project)**
+- [x] 8. **Add seed data — Columns (12 objects — 4 per project)**
Add 4 Column seed objects per project (12 total): To Do (order 0, active), In Progress (order 1, wipLimit 3, active), Review (order 2, wipLimit 2, active), Done (order 3, done). Use hard-coded UUIDs from the design.md reference table for project cross-references.
-- [ ] 9. **Add seed data — Tasks (5 objects)**
+- [x] 9. **Add seed data — Tasks (5 objects)**
Add 5 Task seed objects as specified in design.md: `fix-login-redirect`, `design-dashboard-widgets`, `write-api-docs`, `k8s-namespace-setup`, `onboarding-n8n-workflow`. Include realistic assignees, priorities, due dates, and label references.
-- [ ] 10. **Add seed data — Time Entries (3 objects)**
+- [x] 10. **Add seed data — Time Entries (3 objects)**
Add 3 TimeEntry seed objects referencing the task seeds: two entries for `fix-login-redirect` (jdoe, 90 min and 60 min) and one for `k8s-namespace-setup` (ksmits, 45 min).
-- [ ] 11. **Bump register version from `0.1.0` to `0.2.0`**
+- [x] 11. **Bump register version from `0.1.0` to `0.2.0`**
Update `info.version` in `planix_register.json` from `0.1.0` to `0.2.0`. Also update `SettingsService.php` if it holds a separate version constant that gates the import. Verify that the version-check logic in `SettingsService` will detect the mismatch and trigger a re-import on next app load.
-- [ ] 12. **Remove `example` schema reference from `DeepLinkRegistrationListener`**
+- [x] 12. **Remove `example` schema reference from `DeepLinkRegistrationListener`**
Search `lib/Listener/DeepLinkRegistrationListener.php` (and any other PHP or JS files) for references to the slug `"example"` pointing to the Planix register. Remove or replace those references so no code depends on the deleted schema.
-- [ ] 13. **Verify import in local dev environment**
+- [x] 13. **Verify import in local dev environment**
Boot the Docker dev environment, enable Planix, and confirm via the OpenRegister admin UI or API that:
- All 5 schemas are present in the `planix` register.
- All seed objects are created (5 labels, 3 projects, 12 columns, 5 tasks, 3 time entries).
- The register version stored in `appconfig` matches `0.2.0`.
-- [ ] 14. **Verify idempotency — re-import does not create duplicates**
+- [x] 14. **Verify idempotency — re-import does not create duplicates**
Manually trigger a second import (e.g. by temporarily resetting the stored version in `appconfig`, then reloading the app). Confirm that object counts remain unchanged after the second import.
-- [ ] 15. **Verify schema validation**
+- [x] 15. **Verify schema validation**
Using curl or Postman, send a POST request to the OpenRegister API to create a Task without a `title` field. Confirm HTTP 400 is returned. Repeat with `status: "unknown"` and confirm HTTP 400.
-- [ ] 16. **Run `composer check:strict`**
+- [x] 16. **Run `composer check:strict`**
Run `composer check:strict` in the Planix app directory. Fix any PHPCS, PHPMD, Psalm, or PHPStan issues introduced by or encountered during this change.
diff --git a/openspec/specs/register-schemas/spec.md b/openspec/specs/register-schemas/spec.md
new file mode 100644
index 0000000..4524f04
--- /dev/null
+++ b/openspec/specs/register-schemas/spec.md
@@ -0,0 +1,174 @@
+# Spec: Register Schemas
+
+**Capability:** register-schemas
+**Status:** draft
+**Change:** register-schemas
+**Feature tier:** MVP
+
+---
+
+## Purpose
+
+This spec defines the requirements for the Planix OpenRegister schema definitions. The register file declares the data model that all Planix features are built upon. Correct schema definitions, seed data, and import behaviour are prerequisites for every other Planix capability.
+
+---
+
+## Requirements
+
+### Requirement: All 5 schemas defined [MVP]
+
+The register file MUST declare exactly the schemas `task`, `project`, `column`, `timeEntry`, and `label` in `components/schemas`. The placeholder `example` schema MUST NOT be present.
+
+#### Scenario: Register file contains required schemas
+
+- GIVEN the file `lib/Settings/planix_register.json` is loaded
+- WHEN its `components.schemas` keys are enumerated
+- THEN the keys MUST include `task`, `project`, `column`, `timeEntry`, and `label`
+- AND the key `example` MUST NOT be present
+
+#### Scenario: Task schema has required fields declared
+
+- GIVEN the `task` schema definition
+- WHEN its `required` array is inspected
+- THEN it MUST contain `title` and `status`
+- AND the `status` property MUST declare the enum `["open","in_progress","blocked","done","cancelled"]` with default `"open"`
+
+#### Scenario: Project schema has required fields declared
+
+- GIVEN the `project` schema definition
+- WHEN its `required` array is inspected
+- THEN it MUST contain `title` and `status`
+- AND the `status` property MUST declare the enum `["active","archived","completed"]` with default `"active"`
+
+#### Scenario: Column schema has required fields declared
+
+- GIVEN the `column` schema definition
+- WHEN its `required` array is inspected
+- THEN it MUST contain `title`, `project`, and `order`
+- AND the `type` property MUST declare the enum `["active","done"]` with default `"active"`
+
+#### Scenario: TimeEntry schema has required fields declared
+
+- GIVEN the `timeEntry` schema definition
+- WHEN its `required` array is inspected
+- THEN it MUST contain `task`, `user`, `duration`, and `date`
+
+#### Scenario: Label schema has required fields declared
+
+- GIVEN the `label` schema definition
+- WHEN its `required` array is inspected
+- THEN it MUST contain `title` and `color`
+- AND the `color` property MUST declare the default `"#4376FC"`
+
+---
+
+### Requirement: Schema validation enforced by OpenRegister [MVP]
+
+OpenRegister MUST enforce `required` constraints declared in each schema when objects are created or updated via the API.
+
+#### Scenario: Task creation without title is rejected
+
+- GIVEN the `task` schema is registered in OpenRegister
+- WHEN the API receives a POST request to create a task without a `title` field
+- THEN OpenRegister MUST return HTTP 400
+- AND the response MUST contain a validation error referencing `title`
+
+#### Scenario: Task creation with invalid status enum is rejected
+
+- GIVEN the `task` schema is registered in OpenRegister
+- WHEN the API receives a POST request with `status: "unknown"`
+- THEN OpenRegister MUST return HTTP 400
+- AND the response MUST contain a validation error referencing `status`
+
+#### Scenario: Label creation without color defaults to #4376FC
+
+- GIVEN the `label` schema is registered in OpenRegister
+- WHEN the API receives a POST request with only a `title` field (no `color`)
+- THEN OpenRegister MUST store the label with `color: "#4376FC"`
+- AND the response MUST include the default value
+
+---
+
+### Requirement: Seed data loaded on install [MVP]
+
+On a fresh Planix install, the seed objects defined in the register file MUST be created in OpenRegister automatically.
+
+#### Scenario: Seed objects present after fresh install
+
+- GIVEN Planix is installed for the first time on a Nextcloud instance
+- WHEN the app is first activated (triggering `SettingsService` import)
+- THEN at least 3 Label objects MUST exist in the `planix` register
+- AND at least 3 Project objects MUST exist
+- AND at least 4 Column objects MUST exist
+- AND at least 5 Task objects MUST exist
+- AND at least 3 TimeEntry objects MUST exist
+
+#### Scenario: Seed labels have correct colors
+
+- GIVEN the seed data has been loaded
+- WHEN the `Bug` label is retrieved via the OpenRegister API
+- THEN its `color` field MUST be `"#E74C3C"`
+
+#### Scenario: Seed tasks reference seed projects
+
+- GIVEN the seed data has been loaded
+- WHEN the task with slug `fix-login-redirect` is retrieved
+- THEN its `project` field MUST reference the `client-portal-v2` project object
+- AND its `column` field MUST reference the `portal-in-progress` column object
+
+---
+
+### Requirement: Idempotent import [MVP]
+
+Re-importing the register file MUST NOT create duplicate schema definitions or duplicate seed objects.
+
+#### Scenario: Re-import does not duplicate schemas
+
+- GIVEN Planix has been installed and the register has been imported once
+- WHEN the app is reloaded and `SettingsService` runs the import again (same version)
+- THEN the number of schemas in the `planix` register MUST remain exactly 5
+- AND no additional schema versions MUST be created
+
+#### Scenario: Re-import does not duplicate seed objects
+
+- GIVEN the seed data has been loaded on first install
+- WHEN a re-import is triggered (e.g. by a version bump in a subsequent release)
+- THEN the number of Label objects MUST NOT increase beyond the original seed count
+- AND OpenRegister MUST use the slug as the idempotency key when upserting seed objects
+
+---
+
+### Requirement: Version-based skip logic [MVP]
+
+The import MUST be skipped when the stored register version matches the file version, to avoid unnecessary processing on every app request.
+
+#### Scenario: Import skipped when version unchanged
+
+- GIVEN the `planix` register is already stored in OpenRegister with version `0.2.0`
+- AND the file `lib/Settings/planix_register.json` declares version `0.2.0`
+- WHEN `SettingsService` checks whether to import
+- THEN the import MUST be skipped
+- AND no API calls to OpenRegister MUST be made
+
+#### Scenario: Import triggered when version bumped
+
+- GIVEN the `planix` register is stored with version `0.1.0`
+- AND the file `lib/Settings/planix_register.json` declares version `0.2.0`
+- WHEN `SettingsService` checks whether to import
+- THEN the import MUST be executed
+- AND all 5 schemas MUST be upserted in OpenRegister
+- AND the stored version MUST be updated to `0.2.0` after successful import
+
+---
+
+## Acceptance Criteria
+
+- [ ] `lib/Settings/planix_register.json` contains schemas `task`, `project`, `column`, `timeEntry`, `label` and no `example` schema
+- [ ] Each schema has the correct `required` array and property definitions as specified in design.md
+- [ ] Register file version is `0.2.0`
+- [ ] `SettingsService` bumps trigger an import when `0.1.0` is stored and `0.2.0` is in the file
+- [ ] After a fresh install, all seed objects are present and queryable via the OpenRegister API
+- [ ] Re-importing does not create duplicate objects (verified by checking object count before and after a manual re-import)
+- [ ] Task creation via the API without `title` returns HTTP 400
+- [ ] Task creation with `status: "unknown"` returns HTTP 400
+- [ ] `DeepLinkRegistrationListener` no longer references the `example` schema slug