From 3366064fa27a6a7c4f12bce038a9d0184f9beee9 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 3 May 2026 17:13:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(openspec):=20softwarecatalog-adopt-or-abst?= =?UTF-8?q?ractions=20=E2=80=94=20manifest=20+=20register-resolver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the OR-abstraction audit (2026-05-03). Spec-only — no code changes. Drafts the per-app adoption openspec change so each app can run /opsx-apply against it when ready. References .claude/audit-2026-05-03/ research, Phase 2 OR/nc-vue/ hydra specs (#1420, #113, #218), and ADRs 022/024/025. --- .../design.md | 200 ++++++++++++ .../proposal.md | 146 +++++++++ .../spec.md | 291 ++++++++++++++++++ .../tasks.md | 130 ++++++++ 4 files changed, 767 insertions(+) create mode 100644 openspec/changes/softwarecatalog-adopt-or-abstractions/design.md create mode 100644 openspec/changes/softwarecatalog-adopt-or-abstractions/proposal.md create mode 100644 openspec/changes/softwarecatalog-adopt-or-abstractions/specs/softwarecatalog-adopt-or-abstractions/spec.md create mode 100644 openspec/changes/softwarecatalog-adopt-or-abstractions/tasks.md diff --git a/openspec/changes/softwarecatalog-adopt-or-abstractions/design.md b/openspec/changes/softwarecatalog-adopt-or-abstractions/design.md new file mode 100644 index 00000000..1d82e80c --- /dev/null +++ b/openspec/changes/softwarecatalog-adopt-or-abstractions/design.md @@ -0,0 +1,200 @@ +# Design — softwarecatalog-adopt-or-abstractions + +## Reuse analysis + +| Capability | Reuse from | Why | +|------------|-----------|-----| +| App-manifest schema + loader | `@conduction/nextcloud-vue` (`src/schemas/app-manifest.schema.json`, `useAppManifest`) | Single source of truth per `hydra/openspec/changes/adopt-app-manifest/`. | +| Index / detail page-type renderers | nc-vue `CnIndexPage`, `CnDetailPage` (already used) | Existing views already wrap these. Manifest declares which schema each consumes. | +| Register / schema ID resolution | OR `RegisterResolverService` (`openregister/openspec/changes/register-resolver-service/`) | Eliminates 5 duplicated `getValueString(...register/schema...)` call shapes. | +| Tenant context | `useTenantContext()` from nc-vue (`nextcloud-vue/openspec/changes/multi-tenancy-context/`) | Cache invalidation + write header stamping. | +| i18n source-of-truth metadata | OR `sourceLanguage` (`openregister/openspec/changes/i18n-source-of-truth/`) | Read-only consumption — display badge. | +| API language negotiation | OR `?_lang=` + `X-Translation-Target-Language` (`openregister/openspec/changes/i18n-api-language-negotiation/`) | Wire into single `orClient.js` composable. | +| Pinia stores | SoftwareCatalog's existing entity-typed Pinia stores | Keep. Migrate fetch URL building into `orClient.js`. | +| Sidebars / dialogs / modals | Existing `src/sidebars/`, `src/dialogs/`, `src/modals/` | Register via manifest's `slots` and `customComponents`. No reorganisation needed. | + +### What we deliberately do NOT reuse + +- **OR's lifecycle annotations** — applications and components do + not have a status state machine that benefits from + `x-openregister-lifecycle`. Procurement / publication workflow + is out of scope here. +- **OR's notification engine** — no notification triggers in + SoftwareCatalog today. If product reasons emerge, follow-up + change adopts `x-openregister-notifications`. +- **VNG `Softwarecatalogus/` client repo** — read-only, separate. + +### What's deferred + +- **Concept Organisations** consumption — the + `conceptOrganisatiesWidget.js` integrates with an external feed. + Phase 1 declares the page in the manifest with + `type: "custom"`; future change may model it as `type: "index"` + if the feed exposes a register-shaped contract. + +## Public API / migration shape + +### `src/manifest.json` (new file) + +```json +{ + "$schema": "https://unpkg.com/@conduction/nextcloud-vue@latest/dist/schemas/app-manifest.schema.json", + "version": "0.1.0", + "dependencies": ["openregister"], + "menu": [ + { "id": "apps", "label": "softwarecatalog.menu.apps", "icon": "icon-apps", "route": "/apps", "section": "main", "order": 10 }, + { "id": "components", "label": "softwarecatalog.menu.components", "icon": "icon-component", "route": "/components", "section": "main", "order": 20 }, + { "id": "organisations", "label": "softwarecatalog.menu.organisations", "icon": "icon-organisation", "route": "/organisations", "section": "main", "order": 30 }, + { "id": "catalogs", "label": "softwarecatalog.menu.catalogs", "icon": "icon-catalog", "route": "/catalogs", "section": "main", "order": 40 }, + { "id": "concept-organisations", "label": "softwarecatalog.menu.conceptOrganisations", "icon": "icon-concept", "route": "/concept-organisations", "section": "main", "order": 50 }, + { "id": "settings", "label": "softwarecatalog.menu.settings", "icon": "icon-settings", "route": "/settings", "section": "settings", "permission": "admin" } + ], + "pages": [ + { + "id": "apps-index", + "route": "/apps", + "type": "index", + "title": "softwarecatalog.pages.apps", + "config": { + "register": "@resolve:apps_register", + "schema": "@resolve:apps_schema", + "columns": ["name", "organisation", "status", "version"] + } + }, + { + "id": "apps-detail", + "route": "/apps/:id", + "type": "detail", + "title": "softwarecatalog.pages.app", + "config": { + "register": "@resolve:apps_register", + "schema": "@resolve:apps_schema" + }, + "slots": { "sidebar": "AppSidebar" } + } + // ... components, organisations, catalogs (similar shape) + , { + "id": "concept-organisations-index", + "route": "/concept-organisations", + "type": "custom", + "title": "softwarecatalog.pages.conceptOrganisations", + "component": "ConceptOrganisationsPage" + } + , { + "id": "settings", + "route": "/settings", + "type": "custom", + "title": "softwarecatalog.pages.settings", + "component": "SettingsPage" + } + ] +} +``` + +Notes: +- `@resolve:{key}` sentinel: same convention as LarpingApp adoption + change. The renderer pre-processor runs `RegisterResolverService` + (or its frontend equivalent) at render time. +- `slots: { sidebar: "AppSidebar" }` registers + `src/sidebars/AppSidebar.vue` for the apps detail page. + +### `src/composables/orClient.js` (new file) + +Identical contract to LarpingApp's `orClient.js`: + +```js +export function useOrClient () { + const baseUrl = '/index.php/apps/openregister/api' + + async function fetchObject ({ register, schema, uuid }) { + const lang = OC.getLocale().split('_')[0] + const url = `${baseUrl}/objects/${register}/${schema}/${uuid}?_lang=${lang}` + return axios.get(url, { headers: buildHeaders() }) + } + + async function patchObject ({ register, schema, uuid, body, targetLang }) { + const url = `${baseUrl}/objects/${register}/${schema}/${uuid}` + const headers = buildHeaders() + if (targetLang) headers['X-Translation-Target-Language'] = targetLang + return axios.patch(url, body, { headers }) + } + + return { fetchObject, patchObject } +} +``` + +### Service migrations (Phase 2) + +All five files follow the same pattern: inject +`RegisterResolverService` and replace the resolver call. + +`lib/Service/ModuleComplianceService.php`: + +```php +// before +$register = $this->config->getValueString('softwarecatalog', 'modules_register', ''); +$schema = $this->config->getValueString('softwarecatalog', 'modules_schema', ''); + +// after +$pair = $this->resolver->resolveForObjectType('modules'); +[$register, $schema] = [$pair->registerId, $pair->schemaId]; +``` + +Same shape for the other four classes with their respective +object types: `gebruik`, `organisations`, `views`, +`user-profile-organisation`. + +### Migration risk surface + +| Risk | Mitigation | +|------|-----------| +| `RegisterResolverService` not yet deployed | DI null-check fallback to legacy `getValueString` (Phase 2) plus deprecation log. | +| 5-file resolver migration introduces typo / incorrect object-type string | Unit tests assert each service's `resolveForObjectType` argument matches the existing config-key naming convention. | +| Manifest validation fails CI on first introduction | Tier 2 keeps router hand-wired; failed validation does not break runtime. | +| Tenant switch on detail page may interrupt user mid-edit | Detail navigates back; pending edits are lost (existing UX). Document in spec; future change MAY add an unsaved-changes guard. | +| External feeds (GEMMA, GitHub) used by sync services have their own auth context — confused with NC tenant context | Sync services run server-side; `useTenantContext()` is frontend-only. No conflict. | +| `concept-organisations` page differs from other entity pages | `type: "custom"` covers the difference; widget keeps its existing implementation. | + +## Open design questions + +1. **Q1 — `@resolve:{key}` sentinel.** Same as LarpingApp Q1. + Local pre-processor for now; upstream when ≥2 apps need it. + +2. **Q2 — Multi-tenancy gating.** Same as LarpingApp Q2. Ship + Phases 1-3 first; Phase 4 trails the nc-vue release. + +3. **Q3 — Sidebar slot conventions.** Apps detail page registers + `slot.sidebar = AppSidebar`. Should organisations / catalogs + also expose sidebars by default, or keep them off until the user + opts in? Recommend default-on for the entities that have sidebars + today (apps, organisations); off for the others. + +4. **Q4 — Concept-organisations page.** The widget pulls from an + external feed (GEMMA / GitHub). It's modelled as `type: "custom"` + in this change. Should we declare it as `type: "index"` with a + pseudo-source URN (e.g. + `softwarecatalog:concept-organisations`) so the page-type + contract is uniform? Recommend: stay `custom` until nc-vue grows + a "data source URN" concept. + +5. **Q5 — User-profile event listener resolver injection.** + `UserProfileUpdatedEventListener` reacts to a Nextcloud user + profile event and writes a SoftwareCatalog organisation + membership row. Should the listener inject the resolver in + its constructor (DI-via-app-container) or fetch it lazily from + the container? Recommend constructor injection for testability. + +6. **Q6 — Sync service interval keys.** Each sync service has + non-register `getValueString` keys for cron interval, retry + policy, feature flags. Phase 2.4 keeps these on `IAppConfig`. + Should we collect them under a `softwarecatalog/openspec/specs/ + sync-tunables/spec.md` so a future audit doesn't re-flag them as + "hardcoded keys"? Recommend yes — small extra capability spec + that documents the intentional separation. + +7. **Q7 — Translation target on sync writes.** When sync services + write to OR (e.g. updating an application's description from + GitHub), should they stamp `X-Translation-Target-Language`? + GitHub README content is typically English. Recommend: yes, + stamp `en` so OR's i18n source-of-truth tracks where the + content came from. diff --git a/openspec/changes/softwarecatalog-adopt-or-abstractions/proposal.md b/openspec/changes/softwarecatalog-adopt-or-abstractions/proposal.md new file mode 100644 index 00000000..22125023 --- /dev/null +++ b/openspec/changes/softwarecatalog-adopt-or-abstractions/proposal.md @@ -0,0 +1,146 @@ +# SoftwareCatalog — adopt OR abstractions (manifest, register-resolver, multi-tenancy) + +## Why + +The 2026-05-03 OR-abstraction audit (`.claude/audit-2026-05-03/`) +identified the same three adoption gaps in SoftwareCatalog as in +LarpingApp: + +1. **No architectural manifest** — SoftwareCatalog wires its router + by hand and has no `src/manifest.json`. Per **ADR-024** + (`hydra/openspec/architecture/`) and the migration order in + `hydra/openspec/changes/adopt-app-manifest/`, SoftwareCatalog is + in the second-wave cohort (small, schema-driven) — adopt after + MyDash (the pilot). +2. **`getValueString(...register/schema...)` consolidation** — five + service classes (`ModuleComplianceService`, `GebruikSyncService`, + `OrganizationSyncService`, `ViewService`, plus the + `UserProfileUpdatedEventListener`) resolve register / schema + IDs from `IAppConfig::getValueString` per-call. The new + `RegisterResolverService` from + `openregister/openspec/changes/register-resolver-service/` + consolidates the pattern. SoftwareCatalog has more call sites + than LarpingApp. +3. **No multi-tenancy wiring** — SoftwareCatalog manages applications + and organisations across municipalities. Frontend has no + `useTenantContext()` wiring. When `multi-tenancy-context` ships + in nc-vue, SoftwareCatalog adopts it for refetch and write + header stamping. + +> **Note**: This change concerns the **internal Conduction +> SoftwareCatalog app** at `/softwarecatalog/` (lowercase), NOT the +> VNG client repo at `Softwarecatalogus/` (capitalised). Per project +> memory the VNG repo is read-only and MUST NOT be committed to. + +## What Changes + +### Manifest adoption (Tier 2 → Tier 3) + +- Add `src/manifest.json` with: + - top-level menu entries: Apps, Components, Organisations, + Catalogs, Concept Organisations, Settings + - per-entity `index` pages (`type: "index"`) and `detail` pages + (`type: "detail"`) + - sidebars (currently in `src/sidebars/`) registered via the + `slots` map for relevant pages + - dialogs (currently in `src/dialogs/`) registered via + `customComponents` for any `type: "custom"` pages +- Set `dependencies: ["openregister"]` (SoftwareCatalog's ADR-001 + already requires OR per its config.yaml). +- Tier 2 first; Tier 3 (manifest-driven nav) tracked as follow-up. + +### `RegisterResolverService` consumption + +- Replace `IAppConfig::getValueString` calls that resolve + register/schema pairs in: + - `lib/Service/ModuleComplianceService.php` + - `lib/Service/GebruikSyncService.php` + - `lib/Service/OrganizationSyncService.php` + - `lib/Service/ViewService.php` + - `lib/EventListener/UserProfileUpdatedEventListener.php` +- DI `RegisterResolverService` into each constructor. +- Keep `getValueString` calls for non-register keys (sync + intervals, feature flags, admin tunables) on `IAppConfig` + directly. + +### Multi-tenancy wiring + +- Adopt `useTenantContext()` in: + - `src/views/` apps / components / organisations index views + - and corresponding detail views +- Refetch on tenant switch. +- Stamp `X-OpenRegister-Organisation` on writes. + +### i18n wiring + +- Pass `?_lang=` on OR fetches. +- Pass `X-Translation-Target-Language` on writes when editing + non-default-language content. +- Display "(translated from {lang})" badge on lists where the + served language differs from `sourceLanguage`. + +## Problem + +SoftwareCatalog already complies with ADR-001 (data in OR) and +ADR-012 (nc-vue components only). The remaining adoption gap is +purely operational: + +- **Hand-wired routes** — adding a new entity type means editing + three places (`router/index.js`, `navigation/...`, + `views/{type}/...`). +- **Five duplicated resolver call shapes** — each service that + needs an OR object resolves register and schema IDs identically. + A typo in one call returns silent empty results. +- **No tenant switch reactivity** — the frontend cannot tell when + the active organisation changes. Lists show stale data on + switch. +- **No language negotiation** — translatable application + descriptions silently overwrite source language on edit. + +The cohort solution exists; SoftwareCatalog adopts it. + +## Proposed Solution + +A single `softwarecatalog-adopt-or-abstractions` change with five +phases (see `tasks.md`): + +1. Manifest at Tier 2. +2. `RegisterResolverService` consumption (5 files). +3. i18n wiring (`?_lang=`, `X-Translation-Target-Language`, + `sourceLanguage` display). +4. Multi-tenancy wiring (gated on nc-vue release). +5. Manifest Tier 3 graduation (follow-up tracking). + +Each phase is independently shippable. + +## Out of Scope + +- The VNG `Softwarecatalogus/` client repo. Read-only per project + memory. +- Sync engine refactors. The three sync services + (`Gebruik`, `Organization`, `Module`) keep their integration + with external GEMMA / GitHub feeds; this change only consolidates + their register/schema resolution. +- Custom-icon / image-upload paths. Untouched. +- Newman / API test suite reorganisation. Tracked in a separate + swc-test concern. + +## See also + +- `openregister/openspec/changes/register-resolver-service/` — the + service this change consumes. +- `openregister/openspec/changes/pluggable-integration-registry/` + (ADR-019) — future GEMMA / GitHub sync sources may register as + integration providers. +- `openregister/openspec/changes/i18n-source-of-truth/` (ADR-025). +- `openregister/openspec/changes/i18n-api-language-negotiation/` + (ADR-025). +- `nextcloud-vue/openspec/changes/multi-tenancy-context/`. +- `hydra/openspec/changes/adopt-app-manifest/` — fleet-wide + manifest convention (ADR-024). +- ADR-001 — All data in OR. +- ADR-012 — nc-vue components only. +- ADR-022 — Apps consume OR abstractions. +- ADR-024 — App manifest fleet-wide adoption. +- ADR-025 — i18n source-of-truth + API language negotiation. +- `.claude/audit-2026-05-03/` — source audit. diff --git a/openspec/changes/softwarecatalog-adopt-or-abstractions/specs/softwarecatalog-adopt-or-abstractions/spec.md b/openspec/changes/softwarecatalog-adopt-or-abstractions/specs/softwarecatalog-adopt-or-abstractions/spec.md new file mode 100644 index 00000000..846e2da5 --- /dev/null +++ b/openspec/changes/softwarecatalog-adopt-or-abstractions/specs/softwarecatalog-adopt-or-abstractions/spec.md @@ -0,0 +1,291 @@ +--- +status: draft +--- + +# SoftwareCatalog — adopt OR abstractions + +## Purpose + +Specify the requirements for SoftwareCatalog's adoption of: + +1. The fleet-wide app-manifest contract from + `@conduction/nextcloud-vue` (per ADR-024 and + `hydra/openspec/changes/adopt-app-manifest/`). +2. OpenRegister's `RegisterResolverService` for register / schema + ID resolution (per + `openregister/openspec/changes/register-resolver-service/`). +3. OpenRegister's i18n source-of-truth + API language-negotiation + conventions (per ADR-025 and the two i18n changes). +4. nc-vue's `useTenantContext()` composable for multi-tenancy + awareness (per + `nextcloud-vue/openspec/changes/multi-tenancy-context/`). + +This change concerns the internal Conduction SoftwareCatalog app +at `softwarecatalog/` (lowercase). The VNG `Softwarecatalogus/` +client repo (capitalised) is a separate, read-only repository and +is OUT OF SCOPE. + +## ADDED Requirements + +### Requirement: SoftwareCatalog MUST ship an architectural manifest at `src/manifest.json` + +SoftwareCatalog MUST add `src/manifest.json` conforming to the +JSON Schema published by `@conduction/nextcloud-vue` at +`src/schemas/app-manifest.schema.json`. The manifest MUST be loaded +via `useAppManifest('softwarecatalog', bundledManifest)` in +`src/main.js`. + +The manifest MUST set: +- `$schema` to the published nc-vue schema URL +- `version` to a semver string +- `dependencies: ["openregister"]` +- a `menu` array including all six top-level entries (Apps, + Components, Organisations, Catalogs, Concept Organisations, + Settings) +- a `pages` array including index and detail pages for the four + schema-driven entity types (apps, components, organisations, + catalogs) plus the two `type: "custom"` pages + (concept-organisations, settings) + +#### Scenario: Manifest loads on app boot + +- GIVEN SoftwareCatalog is installed and OR is enabled +- WHEN a user navigates to + `/index.php/apps/softwarecatalog` +- THEN `useAppManifest('softwarecatalog', bundledManifest)` MUST + be called before vue-router mounts +- AND on async-fetch of + `/index.php/apps/softwarecatalog/api/manifest` the loader MUST + silently fall back to bundled on non-200 + +#### Scenario: Manifest validation fails build + +- GIVEN a developer commits `src/manifest.json` with a missing + `pages[].type` field +- WHEN `npm run check:manifest` runs +- THEN it MUST exit non-zero +- AND CI MUST fail + +#### Scenario: Manifest declares OR dependency + +- GIVEN `src/manifest.json` +- WHEN reading `manifest.dependencies` +- THEN it MUST contain `"openregister"` +- AND `CnAppRoot` (Tier 4, future) MUST render + `CnDependencyMissing` if OR is disabled + +#### Scenario: Sidebar slot wiring + +- GIVEN the manifest declares + `pages[id="apps-detail"].slots.sidebar = "AppSidebar"` +- AND `customComponents` registers `AppSidebar` to + `src/sidebars/AppSidebar.vue` +- WHEN the apps detail page renders +- THEN the sidebar slot MUST resolve to `AppSidebar.vue` +- AND existing sidebar functionality (org info, related apps) + MUST continue to work + +### Requirement: SoftwareCatalog MUST consume `RegisterResolverService` for register / schema resolution + +The five PHP classes that currently resolve register / schema IDs +via `IAppConfig::getValueString` MUST migrate to +`OCA\OpenRegister\Service\RegisterResolverService::resolveForObjectType()`: + +- `lib/Service/ModuleComplianceService.php` +- `lib/Service/GebruikSyncService.php` +- `lib/Service/OrganizationSyncService.php` +- `lib/Service/ViewService.php` +- `lib/EventListener/UserProfileUpdatedEventListener.php` + +Non-register `getValueString` calls (sync intervals, retry policy, +feature flags) MUST remain on `IAppConfig`. + +#### Scenario: ModuleComplianceService uses resolver + +- GIVEN `ModuleComplianceService` checks compliance for the + `modules` register +- WHEN it resolves register / schema IDs +- THEN it MUST call `$this->resolver->resolveForObjectType('modules')` +- AND MUST NOT call `$this->config->getValueString(...)` for + the `_register` / `_schema` suffixes + +#### Scenario: Sync services use resolver + +- GIVEN `GebruikSyncService` and `OrganizationSyncService` run + their cron jobs +- WHEN each resolves its target register / schema +- THEN both MUST use `RegisterResolverService` +- AND non-register tunables (cron interval, retry attempts, + external feed URL) MUST remain on `IAppConfig` + +#### Scenario: Resolver fallback during upgrade window + +- GIVEN OR is installed but the + `RegisterResolverService` class is not yet present +- WHEN any of the five migrated classes is instantiated +- THEN the constructor MUST detect resolver absence via DI + null-check +- AND MUST fall back to legacy `getValueString` path +- AND MUST log a deprecation warning + +### Requirement: SoftwareCatalog OR fetches MUST pass `?_lang={user locale}` + +All OR object fetches issued from SoftwareCatalog's frontend MUST +include `?_lang={BCP47}` set to the user's Nextcloud locale (region +tag stripped). + +#### Scenario: Lang stamping on app fetch + +- GIVEN the user's Nextcloud locale is `en_GB` +- WHEN `useOrClient().fetchObject({register: 7, schema: 21, uuid: 'xyz'})` + is called +- THEN the URL MUST be + `/index.php/apps/openregister/api/objects/7/21/xyz?_lang=en` + +#### Scenario: Locale region tag stripped + +- GIVEN `OC.getLocale()` returns `nl_NL` +- WHEN `orClient.js` builds the URL +- THEN `_lang=nl` MUST be the parameter value + +### Requirement: SoftwareCatalog OR writes MUST stamp `X-Translation-Target-Language` when editing a non-default language + +When a user edits a translatable property in a non-default +language, the PATCH/PUT request MUST include +`X-Translation-Target-Language: {target}`. + +When sync services write content known to be in a specific +language (e.g. GitHub README content in English), they MUST also +stamp the header. + +#### Scenario: User edits English description on Dutch-default register + +- GIVEN an application object with translatable property + `description` and `sourceLanguage: "nl"` +- AND the user edits the English variant from an English UI +- WHEN the PATCH is issued +- THEN the body MUST be `{ "description": "..." }` +- AND the request MUST include + `X-Translation-Target-Language: en` + +#### Scenario: Sync service stamps source language + +- GIVEN `GebruikSyncService` pulls a README from GitHub (English) +- WHEN it writes the content to OR +- THEN the request MUST include + `X-Translation-Target-Language: en` +- AND OR MUST store the value under the `en` slot + +### Requirement: SoftwareCatalog lists MUST display "(translated from {lang})" badge when served language differs from source + +Index views (Apps, Components, Organisations, Catalogs) MUST show +a small "(translated from {sourceLanguage})" badge next to the +primary display field when the served language differs from the +object's `sourceLanguage` metadata. The badge MUST use the +canonical nc-vue badge style. + +#### Scenario: Badge on translated row + +- GIVEN an application with `sourceLanguage: "nl"` and English + translation +- AND the user's locale is `en_GB` +- WHEN the application appears in the apps index +- THEN the row MUST show the English name +- AND a badge MUST appear with text `(translated from Dutch)` + (i18n-keyed) + +#### Scenario: No badge when served = source + +- GIVEN an application with `sourceLanguage: "nl"` and the user's + locale is `nl_NL` +- WHEN the application appears in the apps index +- THEN no translated-from badge MUST be rendered + +### Requirement: SoftwareCatalog MUST consume `useTenantContext()` from nc-vue when surfacing tenant-scoped OR data + +Once `useTenantContext()` is exported from a versioned nc-vue +release, SoftwareCatalog views that surface OR data MUST adopt +the composable. + +#### Scenario: Tenant switch refetches apps list + +- GIVEN the user is viewing the apps index in tenant A +- WHEN the user switches to tenant B +- THEN `useTenantContext().activeOrganisationUuid` MUST update +- AND the Pinia apps store MUST clear its collection cache +- AND a fresh fetch MUST issue with B's session +- AND the rendered list MUST contain only apps scoped to B + +#### Scenario: Tenant switch on detail navigates back + +- GIVEN the user is viewing an application detail in tenant A +- WHEN the user switches to tenant B +- THEN the detail view MUST navigate back to the apps index +- AND the index MUST refetch with B's session + +#### Scenario: Pre-release fallback + +- GIVEN nc-vue's exported version does not yet include + `useTenantContext` +- WHEN SoftwareCatalog imports it (try/catch guarded) +- THEN absence MUST NOT crash the app +- AND views MUST behave as single-tenant + +### Requirement: SoftwareCatalog write paths MUST stamp `X-OpenRegister-Organisation` when a tenant is active + +When a user writes (POST/PATCH/PUT) to an OR object via the +`orClient.js` composable, the request MUST include +`X-OpenRegister-Organisation: {activeOrganisationUuid}` when +`useTenantContext().activeOrganisationUuid` is non-null. + +#### Scenario: Header stamping on write + +- GIVEN `useTenantContext().activeOrganisationUuid` is + `tenant-b-uuid` +- WHEN a user PATCHes an application +- THEN the request MUST include + `X-OpenRegister-Organisation: tenant-b-uuid` +- AND OR's server-side multi-tenancy trait MUST validate the + header against the session and reject on mismatch + +#### Scenario: No header when no tenant active + +- GIVEN `useTenantContext().activeOrganisationUuid` is null +- WHEN a user PATCHes an application +- THEN the request MUST NOT include + `X-OpenRegister-Organisation` +- AND OR MUST stamp the active organisation from session + (existing behaviour) + +### Requirement: SoftwareCatalog PHP code MUST pass `composer check:strict` + +All SoftwareCatalog PHP files MUST pass `composer check:strict` +(PHPCS, PHPMD, Psalm, PHPStan). This change MUST NOT introduce +new warnings, and SHOULD fix any pre-existing warnings in the +five files it touches. + +#### Scenario: Strict check passes + +- GIVEN the change is applied +- WHEN `composer check:strict` runs in the SoftwareCatalog + container +- THEN exit code MUST be 0 +- AND no new warnings MUST appear + +### Requirement: SoftwareCatalog PHPUnit tests MUST run inside the Nextcloud container + +Per project policy, unit tests MUST be invoked via: + +``` +docker exec -w /var/www/html/custom_apps/softwarecatalog nextcloud \ + php vendor/bin/phpunit -c phpunit-unit.xml +``` + +#### Scenario: Container test invocation + +- GIVEN the developer wants to run unit tests +- WHEN they invoke the container command above +- THEN tests for each migrated service MUST run +- AND each test MUST assert resolver-injection is exercised +- AND the legacy `getValueString` fallback path MUST be covered + by a separate test that mocks resolver absence diff --git a/openspec/changes/softwarecatalog-adopt-or-abstractions/tasks.md b/openspec/changes/softwarecatalog-adopt-or-abstractions/tasks.md new file mode 100644 index 00000000..86e2a0b9 --- /dev/null +++ b/openspec/changes/softwarecatalog-adopt-or-abstractions/tasks.md @@ -0,0 +1,130 @@ +# Tasks — softwarecatalog-adopt-or-abstractions + +> Spec-only change. No PR / merge / archive tasks here. + +## Phase 1 — Manifest pilot (Tier 2) + +- [ ] 1.1 Add `src/manifest.json`: + - `$schema` to published nc-vue app-manifest schema URL + - `version: "0.1.0"` + - `dependencies: ["openregister"]` + - top-level `menu` entries: Apps, Components, Organisations, + Catalogs, Concept Organisations, Settings + - `pages`: + - `apps-index` — `type: "index"`, route `/apps`, + `config.{register, schema, columns}` + - `apps-detail` — `type: "detail"`, route `/apps/:id` + - `components-index` / `components-detail` + - `organisations-index` / `organisations-detail` + - `catalogs-index` / `catalogs-detail` + - `concept-organisations-index` (driven by + `conceptOrganisatiesWidget.js`) + - `settings` — `type: "custom"`, + `component: "SettingsPage"` +- [ ] 1.2 Map existing `src/dialogs/*.vue` and `src/modals/*.vue` to + the manifest's `customComponents` registry via + `src/customComponents.js` (new file). +- [ ] 1.3 Map existing `src/sidebars/*.vue` to the manifest's + per-page `slots.sidebar` overrides where applicable. +- [ ] 1.4 Add `npm run check:manifest` script to `package.json`. +- [ ] 1.5 Wire `useAppManifest('softwarecatalog', bundled)` in + `src/main.js` after pinia setup, before router mount. +- [ ] 1.6 Wire `npm run check:manifest` into existing CI lint job. + +## Phase 2 — `RegisterResolverService` consumption + +- [ ] 2.1 Inventory of register/schema-resolving `getValueString` + calls (file:line + key name + register/schema pair?): + - `lib/Service/ModuleComplianceService.php` + - `lib/Service/GebruikSyncService.php` + - `lib/Service/OrganizationSyncService.php` + - `lib/Service/ViewService.php` + - `lib/EventListener/UserProfileUpdatedEventListener.php` +- [ ] 2.2 Inject `OCA\OpenRegister\Service\RegisterResolverService` + into each of the five classes' constructors. +- [ ] 2.3 Replace each register/schema-resolving `getValueString` + pair with a single `$this->resolver->resolveForObjectType(...)` + call. +- [ ] 2.4 Verify non-register `getValueString` calls (sync + intervals, feature flags) STAY on `IAppConfig` directly. Document + each in tasks.md as "kept". +- [ ] 2.5 Add unit tests asserting resolver injection in each + service. +- [ ] 2.6 Run `composer check:strict` — fix any pre-existing + PHPCS / PHPMD / Psalm / PHPStan warnings touched by edits. + +## Phase 3 — i18n wiring + +- [ ] 3.1 Centralise OR fetch URL building in + `src/composables/orClient.js` exposing + `fetchObject({register, schema, uuid, lang})` and + `patchObject({register, schema, uuid, body, targetLang})`. +- [ ] 3.2 The composable MUST set `?_lang={user locale}` (region + tag stripped) on every fetch. +- [ ] 3.3 The composable MUST set `X-Translation-Target-Language` + on writes when caller passes `targetLang`. +- [ ] 3.4 Migrate `src/store/applications.js`, + `src/store/components.js`, `src/store/organisations.js`, + `src/store/catalogs.js` (plus any other Pinia stores) onto the + composable. +- [ ] 3.5 Add "(translated from {lang})" badge to list rows where + served language ≠ `sourceLanguage`. Use canonical nc-vue badge + style. +- [ ] 3.6 Add Cypress / Playwright e2e: switch user locale to + `en_GB`, open an application with Dutch source, assert badge + reads "(translated from Dutch)". + +## Phase 4 — Multi-tenancy wiring (gated on nc-vue release) + +- [ ] 4.1 Pin nc-vue version in `package.json` to the release + exporting `useTenantContext`. Until released, guard import with + try/catch. +- [ ] 4.2 In each index view (apps, components, organisations, + catalogs): import composable, watch + `activeOrganisationUuid`, on change call store + `clearAllSubResources()` and refetch. +- [ ] 4.3 In each detail view: watch + `activeOrganisationUuid`, on change navigate back to index. +- [ ] 4.4 In `orClient.js`, add option to stamp + `X-OpenRegister-Organisation` on writes. Default ON when + `activeOrganisationUuid` is non-null. +- [ ] 4.5 e2e: switch tenants, assert apps list refetches and + excludes apps from previous tenant. + +## Phase 5 — Manifest Tier 3 graduation (follow-up tracking) + +- [ ] 5.1 Track prerequisites for Tier 3: + - `type: "index"` and `type: "detail"` page-type contracts + stable in nc-vue + - SoftwareCatalog dialogs / modals / sidebars compatible with + nc-vue's `customComponents` and `slots` resolution + - Pinia stores compatible with manifest-driven `CnPageRenderer` + data fetching +- [ ] 5.2 Open follow-up `softwarecatalog-manifest-tier-3` change + once prerequisites met. + +## Phase 6 — Documentation + +- [ ] 6.1 Update / create `docs/architecture.md` covering: + - manifest adoption + - resolver consumption + - i18n flow + - multi-tenancy wiring +- [ ] 6.2 Add screenshots of the "(translated from)" badge to + `docs/features/applications.md`. +- [ ] 6.3 Cross-link new docs from app's README. + +## Phase 7 — Verification + +- [ ] 7.1 `composer check:strict` passes. +- [ ] 7.2 `npm run lint` passes. +- [ ] 7.3 `npm run check:manifest` passes. +- [ ] 7.4 PHPUnit unit tests pass via container invocation: + `docker exec -w /var/www/html/custom_apps/softwarecatalog + nextcloud php vendor/bin/phpunit -c phpunit-unit.xml` +- [ ] 7.5 e2e tests for i18n badge and tenant-switch refetch + pass. +- [ ] 7.6 Manual smoke: enable OR + SoftwareCatalog on a clean + dev Nextcloud, list applications, switch tenants, edit a + Dutch-source application from an English UI, confirm UX matches + spec.