|
| 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). |
0 commit comments