Skip to content

Commit 29afda4

Browse files
authored
Merge branch 'development' into release/v0.1.141-unstable.20260818154749
2 parents 0fc967e + 398d870 commit 29afda4

10 files changed

Lines changed: 1070 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-08-18
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Design — adopt-integration-leaves
2+
3+
## 1. Leaf-per-schema mapping and why each pairing is the right one
4+
5+
| Schema | Leaf | Grounding (real fields / services) |
6+
|---|---|---|
7+
| `contactPerson` | `contacts` | `contactsUid` — "Verwijzing (UID) naar de Nextcloud-contactpersoon in het adresboek (OCP\Contacts\IManager)". The catalogue record is explicitly only the ROLE; identity lives in NC Contacts. The leaf makes the vCard (display name, email, avatar) visible and linkable on `ContactpersoonDetail` instead of a bare UID string. |
8+
| `organization` | `contacts` | `organization.contactsUid` — same convention, "contactpersoon van het type organisatie". |
9+
| `contract` | `calendar` | `contract.startDate` / `contract.endDate` ("De einddatum van het contract (indien van toepassing)"). End dates drive renewal planning; the leaf gives every contract a Meetings/Events tab plus the synced end-date event (section 3). |
10+
| `moduleVersion` | `calendar` | `dateEndSupport` ("Startdatum einde ondersteuning") and `dateWithdrawn`. `dateEndSupport` is machine-maintained by the EOL sync (`eolSource`, `eolUpdatedOn` — "Alleen gezet door de EOL-matcher", `EolSyncService::run()`), so the synced event tracks upstream endoflife.date data. |
11+
| `assessment` | `deck` | Reviews carry moderation state (`status` pending/approved/rejected, forced to `pending` server-side by `ReviewService::submit()`, transitioned only by `ModerationService::approve()/reject()` — see `register.d/catalog-ratings.json`). Follow-ups ("moderate this review", "discuss rating 2/10 for module X with the vendor") are card-shaped work; `DeckProvider` supports both link-existing (`{cardId}`) and create-and-link (`{boardId, stackId, title}`). |
12+
| `module` | `bookmarks` | `module.website` is one URL; real applications have docs, changelog, security advisories, pricing pages. `BookmarksProvider` stores links in OR's own `openregister_bookmark_links` table (survives Bookmarks tag edits, caches title/url for the sidebar). |
13+
| `service` | `bookmarks` | `service.website` — same reasoning for supplier service offerings. |
14+
15+
Leaf ids verified against openregister at HEAD:
16+
`LinkedEntityService::legacyLinkedTypeIds()` = `files`, `mail`, `contacts`,
17+
`notes`, `todos`, `calendar`, `talk`, `deck`; registered
18+
`IntegrationProvider::getId()` values include `contacts`, `calendar`,
19+
`deck`, `bookmarks` (`openregister/lib/Service/Integration/Providers/`).
20+
`LinkedEntityService::validateType()` throws on anything else, and
21+
`Repair/LogDanglingLinkedTypes` logs schemas whose `linkedTypes` name an
22+
unregistered integration — both act as loud guards against a typo in the
23+
fragment.
24+
25+
## 2. Fragment mechanics — the `contract` array hazard
26+
27+
`contract` is the ONE schema that already carries `linkedTypes`
28+
(`["decidesk-decisions"]`, in the monolith). ADR-037 fragments are
29+
deep-merged by `SettingsService::loadSettings()` (`deepMergeConfig()`); for
30+
scalar/object keys the merge is a union, but array-of-scalar semantics
31+
(union vs replace) must not be assumed. The fragment therefore declares the
32+
FULL intended array — `["decidesk-decisions", "calendar"]` — which is
33+
correct under either semantic:
34+
35+
- replace → the merged value is exactly the full array;
36+
- union → `decidesk-decisions` deduplicates, `calendar` is added.
37+
38+
Task 1.3 verifies the merged output (`/api/settings/load`) contains both
39+
entries exactly once. Losing `decidesk-decisions` would silently break the
40+
ContractApprovalPanel's decision leaf — this is the highest-risk line of the
41+
whole change, hence its own task and scenario.
42+
43+
## 3. Lifecycle-date calendar sync
44+
45+
`CalendarProvider` is a read/render surface: it lists CalDAV VEVENTs that
46+
carry `X-OPENREGISTER-*` properties identifying the owning object
47+
(persistence is owned by the Calendar app; creation flows via OR's
48+
`CalendarEventService`). Declaring `calendar` in `linkedTypes` gives the
49+
tab and manual link/create, but nobody will hand-create "contract ends"
50+
events for every contract — so this change adds a thin app-side sync:
51+
52+
- `lib/Service/LifecycleCalendarService.php``syncContract(objectData)`
53+
and `syncModuleVersion(objectData)`; upserts (creates, moves, or
54+
deletes) one all-day linked event per tracked date field via OR's
55+
calendar link path. Event titles are English per fleet convention
56+
(`feedback_english-code`): "Contract ends: {contractNumber}" /
57+
"End of support: {module name} {version}".
58+
- `lib/Listener/LifecycleCalendarListener.php` — subscribes to
59+
OpenRegister's object-saved event for the voorzieningen register,
60+
filters on the `contract` / `moduleVersion` schema slugs resolved
61+
through `SettingsService` (never hard-coded register ids), and delegates
62+
to the service. Deletion of the object removes the linked event (OR's
63+
`ObjectCleanupListener` already unlinks leaf rows; the listener only
64+
needs to handle date-cleared-on-save).
65+
- Idempotency: the event is looked up by its object link + a
66+
deterministic marker (one tracked field = one event), so re-saves and
67+
EOL re-stamps move the single event instead of accumulating duplicates.
68+
- Fail-soft: calendar unavailable (app disabled, no writable calendar) is
69+
logged and never blocks the object save — same graceful-degradation
70+
posture the register's other integrations use.
71+
72+
**Which calendar?** The events are personal CalDAV objects; the sync runs
73+
in the saving user's session and writes to that user's default calendar
74+
(the same calendar OR's create-event leaf flow targets). A shared
75+
"portfolio calendar" is a legitimate future improvement, deferred — it
76+
needs an ownership/config decision (`declared-config-enforced-nowhere` is
77+
the failure mode to avoid: no config key is introduced here until
78+
something reads it).
79+
80+
## 4. Deferred (explicitly out of scope)
81+
82+
- `connection.dateEndSupport` / `dateWithdrawn` — same calendar shape as
83+
`moduleVersion`, deferred until the koppeling detail surface is
84+
reviewed; adding it later is one fragment line + one listener case.
85+
- `compliancy.url` / `evidenceReference` as bookmarks — compliance
86+
evidence is file/reference-shaped and already has `allowFiles: true` +
87+
`evidenceReference`; forcing it into bookmarks would duplicate an
88+
existing surface.
89+
- `usage` deck leaf (TIME-classification review follow-ups via
90+
`timeReviewDate`) — plausible, but the assessment leaf should prove the
91+
pattern first.
92+
- NC Mail (`configuration.linkedTypes: ["mail"]` sidebar target +
93+
`mailObjectTemplate`) — a separate comms-rule discussion; the manifest
94+
`_note`s document a deliberate "comms hard-rule" that email widgets stay
95+
off these detail pages, and this change does not reopen it.
96+
97+
## 5. Manifest touch-points
98+
99+
`src/manifest.json` detail pages affected: `ContactpersoonDetail`,
100+
`ContractDetail`, `ModuleDetail`, `Diensten`/`DienstDetail` equivalent,
101+
`ModuleversieDetail`, `ReviewDetail`, `OrganisatieDetail`. The leaf tabs
102+
render from schema `linkedTypes` via the shared detail-page sidebar
103+
(`CnObjectSidebar` — "so the CnObjectSidebar and dashboard widgets can
104+
render a … tab without per-app glue", per `CalendarProvider`'s own
105+
docblock); no per-page widget wiring is expected, but the two `_note`
106+
strings that assert "declares NO email/calendar linkedType" become false
107+
for `contract`/`contactPerson` and MUST be rewritten to describe the new
108+
state, so the next audit doesn't read a stale premise
109+
(`reference_design-system-adoption-silent-failures`: notes that lie are
110+
worse than no notes).
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
kind: code
3+
depends_on: []
4+
---
5+
6+
# softwarecatalog — adopt OpenRegister integration leaves (contacts, calendar, deck, bookmarks)
7+
8+
## Why
9+
10+
OpenRegister ships an app-agnostic integration-leaf registry
11+
(`openregister/lib/Service/Integration/IntegrationRegistry.php` +
12+
`Providers/`): a schema that declares a leaf id in
13+
`configuration.linkedTypes` gets that Nextcloud app's link surface (sidebar
14+
tab / widgets on the object detail page) with zero per-app glue —
15+
`ContactsProvider` (id `contacts`), `CalendarProvider` (`calendar`),
16+
`DeckProvider` (`deck`), `BookmarksProvider` (`bookmarks`) all exist today,
17+
alongside the legacy allow-list ids in
18+
`LinkedEntityService::legacyLinkedTypeIds()` (`files`, `mail`, `contacts`,
19+
`notes`, `todos`, `calendar`, `talk`, `deck`).
20+
21+
Software Catalog consumes almost none of this. Verified against
22+
`lib/Settings/softwarecatalogus_register.json` at HEAD:
23+
24+
- `allowFiles: true` on exactly 6 schemas (`suite`, `service`,
25+
`organization`, `usage`, `module`, `compliancy`) — the files leaf.
26+
- `linkedTypes` on exactly one schema: `contract`
27+
`["decidesk-decisions"]` (the ADR-066 approval projection).
28+
- No schema declares `contacts`, `calendar`, `deck`, or `bookmarks`.
29+
30+
That leaves four gaps the domain data is already shaped for:
31+
32+
1. **Contacts**`contactPerson.contactsUid` is literally "Verwijzing
33+
(UID) naar de Nextcloud-contactpersoon in het adresboek
34+
(`OCP\Contacts\IManager`)", and `organization.contactsUid` mirrors it.
35+
The identity IS a Nextcloud contact by design (the
36+
`ContactpersoonDetail` manifest note says communication happens
37+
"through the linked Nextcloud contact via contactsUid"), yet the detail
38+
page renders no contacts leaf — the vCard link exists only as a bare
39+
string property.
40+
2. **Calendar**`contract.endDate` ("De einddatum van het contract") and
41+
`moduleVersion.dateEndSupport` ("Startdatum einde ondersteuning",
42+
stamped by the EOL matcher per `eolSource`/`eolUpdatedOn`) are the two
43+
dates portfolio managers plan around, and neither is visible in any
44+
calendar. The `ContractDetail` manifest `_note` even hard-codes the
45+
current state: "The contract schema declares NO email/calendar
46+
linkedType".
47+
3. **Deck**`assessment` records (reviews, live since the
48+
`catalog-ratings` fragment added `auteur` + moderation `status`
49+
pending/approved/rejected, enforced by `ReviewService::submit()` and
50+
`ModerationService::approve()/reject()`) generate follow-up work
51+
(moderate a pending review, chase a vendor about a bad rating) that has
52+
no task surface.
53+
4. **Bookmarks**`module.website` ("Een URL naar uw applicatie") and
54+
`service.website` are single URL strings; vendors accumulate more than
55+
one relevant link (docs, changelog, status page, pricing) and today
56+
have nowhere structured to put them.
57+
58+
## What Changes
59+
60+
- Add a new ADR-037 register fragment
61+
`lib/Settings/register.d/catalog-integration-leaves.json` (never editing
62+
the `softwarecatalogus_register.json` monolith) that declares
63+
`configuration.linkedTypes`:
64+
- `contactPerson`: `["contacts"]`
65+
- `organization`: `["contacts"]`
66+
- `contract`: `["decidesk-decisions", "calendar"]` — restating the
67+
existing `decidesk-decisions` entry so the merged array is correct
68+
regardless of whether the ADR-037 deep-merge unions or replaces
69+
arrays (verified behaviour recorded in `design.md`).
70+
- `moduleVersion`: `["calendar"]`
71+
- `assessment`: `["deck"]`
72+
- `module`: `["bookmarks"]`
73+
- `service`: `["bookmarks"]`
74+
- Add a lifecycle-date calendar sync (`lib/Service/LifecycleCalendarService.php`
75+
+ an OR object-saved listener): when `contract.endDate` or
76+
`moduleVersion.dateEndSupport` is set or changed, upsert a linked
77+
all-day VEVENT through OpenRegister's calendar link path (the same
78+
`X-OPENREGISTER-*`-marked events `CalendarProvider::list()` renders),
79+
so the calendar leaf tab shows the end-of-contract / end-of-support
80+
event without manual linking; remove the event when the date is
81+
cleared. EOL-matcher re-stamps of `dateEndSupport`
82+
(`EolSyncService::run()``EolMatcherService`) move the event.
83+
- Update the stale `src/manifest.json` detail-page `_note` prose on
84+
`ContractDetail` and `ContactpersoonDetail` (both currently assert "NO
85+
email/calendar linkedType" as the reason no comms widgets are placed)
86+
and verify the leaf tabs render on the `ContractDetail`,
87+
`ContactpersoonDetail`, `ModuleversieDetail`, `ReviewDetail`,
88+
`ModuleDetail`, and `Diensten` detail surfaces.
89+
- No new leaf providers and no OpenRegister changes: everything consumed
90+
here (`contacts`, `calendar`, `deck`, `bookmarks`) is already a
91+
registered `IntegrationProvider` at openregister HEAD.
92+
93+
Not BREAKING: purely additive configuration plus one new sync service; no
94+
existing route, response shape, or schema property changes. The
95+
`connection.dateEndSupport` and `compliancy.url` fields are deliberately
96+
out of scope (see `design.md` deferrals).

0 commit comments

Comments
 (0)