Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c266694
Merge remote-tracking branch 'origin/beta' into feature/i18n-complete…
rubenvdlinde May 7, 2026
2717813
fix(i18n): address PR 1273 review findings
WilcoLouwerse May 7, 2026
c4fd925
fix(i18n): restore 48 case-renamed Dutch translations in nl.json
WilcoLouwerse May 7, 2026
81ec614
fix(i18n): add missing {count} email and Successfully restored {count…
WilcoLouwerse May 7, 2026
174ebae
fix(i18n): align LLMConfigModal embedding-deletion confirm key with c…
WilcoLouwerse May 7, 2026
3aa4159
docs(controller): clarify TasksController::allUserTasks authorization…
WilcoLouwerse May 7, 2026
33503fa
fix(organisation): extract inline switcher into SwitchOrganisationMod…
WilcoLouwerse May 7, 2026
0ed1ad1
fix(db): remove duplicate linked-entity property declarations in Orga…
WilcoLouwerse May 7, 2026
ade7e7d
style(magic): align equals in MagicSearchHandler.php (phpcbf)
WilcoLouwerse May 7, 2026
cb52f6b
fix(eslint): clear Vue parse + duplicate-import errors
WilcoLouwerse May 7, 2026
2236181
chore(deps): bump webonyx/graphql-php v15.31.5 -> v15.32.3
WilcoLouwerse May 7, 2026
9358edf
style(quality): clear remaining phpcs + eslint errors
WilcoLouwerse May 7, 2026
81f8438
Merge branch 'development' into feature/i18n-complete-translations
WilcoLouwerse May 8, 2026
2708c1c
fix(i18n): address PR #1273 pre-merge review points (T5, T8, T13)
WilcoLouwerse May 8, 2026
7d6f08f
docs: collaborative editing patterns (subscribe-on-view + lock-on-edit)
rubenvdlinde May 10, 2026
cfedd68
chore(integration-xwiki): plan.json + GitHub issue checkboxes (opsx-p…
rubenvdlinde May 11, 2026
71ebebd
feat(chat-ai): SSE streaming + IMcpToolProvider + Message.context
rubenvdlinde May 11, 2026
09f473e
Merge remote-tracking branch 'origin/development' into feature/i18n-c…
WilcoLouwerse May 11, 2026
4297b13
fix(chat-ai): mirror Ollama type-guard in title generator + propagate…
rubenvdlinde May 11, 2026
e72e52c
feat(chat/stream): fall back to user's first agent when widget omits …
rubenvdlinde May 11, 2026
6adad8c
feat(notifications): idempotencyKey dedup (closes scholiq deps #4)
rubenvdlinde May 11, 2026
1a5dbb7
feat(calculations): add dateDiff primitive (closes scholiq deps #5)
rubenvdlinde May 11, 2026
c01c7aa
feat(audit-trail): add per-tenant HMAC key API (closes scholiq deps #2)
rubenvdlinde May 11, 2026
22d93a4
feat(aggregations): cross-schema joins with literal-filter (closes sc…
rubenvdlinde May 11, 2026
5a53aee
feat(notifications): add calculatedChange trigger with debounce seman…
rubenvdlinde May 11, 2026
170f51d
feat(manifest): inject runtime.user.* context from current-user-schem…
rubenvdlinde May 11, 2026
71baf76
feat(schema): add appendOnly schema flag (closes scholiq deps #1)
rubenvdlinde May 11, 2026
6e59605
Merge feat/scholiq-deps/date-diff into integration
rubenvdlinde May 11, 2026
4ef60fb
Merge feat/scholiq-deps/idempotency-key into integration
rubenvdlinde May 11, 2026
642f1a4
Merge feat/scholiq-deps/tenant-key-api into integration
rubenvdlinde May 11, 2026
6641691
Merge feat/scholiq-deps/cross-schema-aggregations into integration
rubenvdlinde May 11, 2026
19196f6
Merge feat/scholiq-deps/calculated-change into integration
rubenvdlinde May 11, 2026
ac25bcf
Merge feat/scholiq-deps/manifest-user-context into integration
rubenvdlinde May 11, 2026
3ae16a9
Merge feat/scholiq-deps/append-only into integration
rubenvdlinde May 11, 2026
5ae5248
chore(quality): clear phpcs/eslint/license CI failures
rubenvdlinde May 11, 2026
7b91720
chore(quality): clear remaining phpcs failures on full sweep
rubenvdlinde May 11, 2026
62bde84
Merge remote-tracking branch 'origin/development' into feature/ai-cha…
rubenvdlinde May 12, 2026
5504377
test: fix PHPUnit suite after development merge
rubenvdlinde May 12, 2026
7987a97
test(mcp): rewrite McpToolsServiceTest as the provider-aggregator tes…
rubenvdlinde May 12, 2026
da9805e
test(mcp): fix ObjectsToolProviderTest saveObject expectations
rubenvdlinde May 12, 2026
07e0e2f
Merge remote-tracking branch 'origin/feature/ai-chat-companion-orches…
rubenvdlinde May 12, 2026
92ad547
chore(quality): clear phpcs errors in notif_dispatch_log migration af…
rubenvdlinde May 12, 2026
47adcf4
fix(referential-integrity): null-safe platform detection in Referenti…
rubenvdlinde May 12, 2026
14eaca8
fix(tests): null-safe platform detection in RegisterService + unseal …
rubenvdlinde May 12, 2026
4b77837
test: fix scholiq-deps test mocks surfaced by the consolidation merge
rubenvdlinde May 12, 2026
8d17464
test: fix AppendOnlyTest validation + AnnotationNotificationDispatche…
rubenvdlinde May 12, 2026
2d69728
test: stub RenderObject::renderEntity in AppendOnlyTest so saveObject…
rubenvdlinde May 12, 2026
03cda6c
fix(i18n): unwrap numeric/URL placeholders from t() per PR #1273 review
WilcoLouwerse May 12, 2026
9293228
Merge remote-tracking branch 'origin/feature/integration-xwiki-plan-t…
rubenvdlinde May 12, 2026
29713b2
Merge remote-tracking branch 'origin/docs/collaborative-editing-patte…
rubenvdlinde May 12, 2026
435b332
Merge remote-tracking branch 'origin/feature/declarative-engines-adr0…
rubenvdlinde May 12, 2026
17e32ee
Merge remote-tracking branch 'origin/fix/header-info-email' into inte…
rubenvdlinde May 12, 2026
73f24d7
Merge remote-tracking branch 'origin/feature/i18n-complete-translatio…
rubenvdlinde May 12, 2026
4c4a96a
Merge remote-tracking branch 'origin/feat/1435/entity-relation-gronds…
rubenvdlinde May 12, 2026
3b183a1
Merge remote-tracking branch 'origin/feat/1438/text-extraction-eml' i…
rubenvdlinde May 12, 2026
b29b6ab
chore(eslint): --fix vue/html-indent + padded-blocks after i18n merge
rubenvdlinde May 12, 2026
ec4578a
Merge remote-tracking branch 'origin/development' into integration/sc…
rubenvdlinde May 12, 2026
d68d676
build(llphant): apply think:false + keep_alive:-1 Ollama patch via co…
rubenvdlinde May 12, 2026
864dd83
fix(review): address PR #1496 blockers/concerns from security review
WilcoLouwerse May 12, 2026
ffe4321
fix(review): address bbrands02 PR #1496 review comments
WilcoLouwerse May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@
['name' => 'Settings\ValidationSettings#validateAllObjects', 'url' => '/api/settings/validate-all-objects', 'verb' => 'POST'],
['name' => 'Settings\ValidationSettings#massValidateObjects', 'url' => '/api/settings/mass-validate', 'verb' => 'POST'],
['name' => 'Settings\ValidationSettings#predictMassValidationMemory', 'url' => '/api/settings/mass-validate/memory-prediction', 'verb' => 'POST'],
// Manifest endpoint — returns host-app manifest enriched with runtime.user context.
['name' => 'manifest#index', 'url' => '/api/manifest/{appId}', 'verb' => 'GET', 'requirements' => ['appId' => '[^/]+']],
// Heartbeat - Keep-alive endpoint for long-running operations.
['name' => 'heartbeat#heartbeat', 'url' => '/api/heartbeat', 'verb' => 'GET'],
// Prometheus metrics endpoint.
Expand Down Expand Up @@ -559,6 +561,12 @@
['name' => 'chat#clearHistory', 'url' => '/api/chat/history', 'verb' => 'DELETE'],
['name' => 'chat#getChatStats', 'url' => '/api/chat/stats', 'verb' => 'GET'],
['name' => 'chat#sendFeedback', 'url' => '/api/conversations/{conversationUuid}/messages/{messageId}/feedback', 'verb' => 'POST', 'requirements' => ['conversationUuid' => '[^/]+', 'messageId' => '\\d+']],

// Chat - Health probe (PublicPage — no auth required).
['name' => 'chatHealth#health', 'url' => '/api/chat/health', 'verb' => 'GET'],

// Chat - SSE streaming endpoint (authenticated).
['name' => 'chatStream#stream', 'url' => '/api/chat/stream', 'verb' => 'POST'],

// Conversations - AI Conversation management.
['name' => 'conversation#index', 'url' => '/api/conversations', 'verb' => 'GET'],
Expand Down
12 changes: 11 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"php": "^8.1",
"adbario/php-dot-notation": "^3.3.0",
"bamarni/composer-bin-plugin": "^1.8",
"cweagans/composer-patches": "^1.7",
"dompdf/dompdf": "^3.1",
"elasticsearch/elasticsearch": "^v8.14.0",
"guzzlehttp/guzzle": "^7.0",
Expand Down Expand Up @@ -126,12 +127,21 @@
"bamarni/composer-bin-plugin": true,
"php-http/discovery": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"cyclonedx/cyclonedx-php-composer": true
"cyclonedx/cyclonedx-php-composer": true,
"cweagans/composer-patches": true
},
"optimize-autoloader": true,
"sort-packages": true,
"platform": {
"php": "8.1"
}
},
"extra": {
"composer-exit-on-patch-failure": true,
"patches": {
"theodo-group/llphant": {
"Forward think:false + keep_alive:-1 to the Ollama API (qwen3 ~5x speedup, no idle-unload wedge) — drop when LLPhant adds a per-call keep_alive/think model option": "patches/llphant-ollama-think-keepalive.patch"
}
}
}
}
50 changes: 49 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions docs/Patterns/collaborative-editing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: Collaborative Editing Semantics
sidebar_position: 1
description: Subscribe-on-view + lock-on-edit — the canonical pattern that ties OpenRegister's push events together with its pessimistic-lock APIs.
keywords:
- collaborative editing
- locks
- notify_push
- real-time
- pessimistic lock
- subscribe
---

# Collaborative Editing Semantics

OpenRegister gives consumer apps the primitives for collaborative editing without requiring a CRDT or a custom merge engine: a per-object **push channel** and a per-object **pessimistic lock**. Used together, they cover the 95% case — "two users opened the same record, who wins?" — with predictable, easy-to-reason-about UX.

This page is the canonical pattern doc. The lib (`@conduction/nextcloud-vue`) implements it as defaults so consumer apps inherit the right behavior without per-app code.

## The two primitives

### 1. Subscribe-on-view (push events)

When a user opens any detail page, the frontend subscribes to that object's `or-object-{uuid}` push channel. Every time the object changes — including when another user acquires or releases a lock — the subscriber's local cache invalidates and the page re-renders with the new state.

- **Wire format**: see [OpenRegister Push Events](../Integrations/OpenRegister.md).
- **Server side**: events fire from `OCA\OpenRegister\Listener\NotifyPushListener` on every `ObjectCreatedEvent`, `ObjectUpdatedEvent`, and `ObjectDeletedEvent`.
- **Lock state on the wire**: payloads are UUID-only; the client refetches through the normal REST path, which always returns the authoritative `@self.locked` block.

### 2. Lock-on-edit (pessimistic locks)

When a user enters edit mode, the frontend acquires a server-side lock with a short TTL (default 30 minutes, renewed every 10 while the user is active). Other users opening the same object see the locked state in real time via their subscription, and their Edit affordance is disabled with a "Locked by X" banner.

- **Wire format**: see [Object lifecycle — locking](../Features/objects.md#locking).
- **Endpoints**: `POST /apps/openregister/api/objects/{register}/{schema}/{id}/lock` to acquire; `DELETE` on the same path to release.
- **TTL safety net**: locks expire automatically if the holder's tab closes without a clean release.

## How they complement

The two primitives are independent — you can subscribe without locking (read-only viewer) or lock without subscribing (a bulk-edit script) — but the universal case wants both:

- **Subscribe alone**: you see remote changes, but two users can still simultaneously edit and silently overwrite each other.
- **Lock alone**: you block concurrent writes, but the second user only finds out when they hit Save (a poor UX).
- **Both**: the second user sees the lock the moment the first user clicks Edit, and the page disables its Edit affordance with a banner. No surprise, no overwrite.

This is why the lib enables both by default on every detail surface.

## Lib defaults (`@conduction/nextcloud-vue`)

The library's [`CnDetailPage`](https://github.com/ConductionNL/nextcloud-vue/blob/beta/docs/components/cn-detail-page.md) and [`CnObjectSidebar`](https://github.com/ConductionNL/nextcloud-vue/blob/beta/docs/components/cn-object-sidebar.md) auto-wire `useObjectSubscription` and (in v1) reactively read lock state from the cached `@self.locked` block. When a remote lock is detected, `CnDetailPage` mounts a `CnLockedBanner` automatically.

Two manifest opt-out flags on `pages[].config`:

```json
{
"id": "MeetingDetail",
"type": "detail",
"config": {
"register": "decidesk",
"schema": "meeting",
"subscribe": true, // default; opt-out for read-only / archive views
"lock": true // default; opt-out for surfaces that don't acquire on edit
}
}
```

In v1 the lock auto-acquire on Edit-mode toggle is intentionally NOT yet wired into the form dialogs — the composables (`useObjectLock`, `LockConflictError`, `PermissionError`) are public so early adopters can wire it themselves. The follow-up cycle integrates it into `CnAdvancedFormDialog` / `CnFormDialog`.

## End-to-end flow

```
User A opens detail page User B opens detail page
│ │
useObjectSubscription useObjectSubscription
│ │
└──────────── notify_push WebSocket ────┘
User A clicks Edit │
│ │
POST .../lock {duration:1800} │
│ │
(200 OK) │
│ │
fires ObjectUpdatedEvent ──┤
│ │
notify_push delivers
or-object-{uuid}
User B's plugin
refetches the object
@self.locked populated
locked.value flips true
CnLockedBanner mounts
Edit toggle disabled
```

## Failure modes

| Scenario | Detection | UX |
|---|---|---|
| `notify_push` unreachable | Plugin falls back to polling | Subscriptions still work, latency increases |
| Lock POST 401/403 | `useObjectLock.acquire()` rejects with `PermissionError` | Toast: "Concurrent edits are not blocked on this schema." Edit allowed without lock. |
| Lock POST 409/423 (conflict) | rejects with `LockConflictError` | Banner: "Locked by X until <expiry>." Edit disabled. |
| Network failure on release | `beforeunload` falls back to `navigator.sendBeacon`; OR's TTL expires the lock automatically | No UX impact. |
| Lock holder inactive | Renew skipped while document hidden | Lock TTL elapses; on next focus, `acquire()` runs again. |

## When NOT to use this pattern

- **Optimistic / CRDT editing.** OpenRegister is not a Yjs-style sync engine; pessimistic locks are deliberate.
- **Bulk import surfaces.** Hundreds of locks per second are wasteful — use the dedicated import endpoints which bypass the per-object event stream (see batch mode in [Push Events](../Integrations/OpenRegister.md#batch-mode-for-bulk-imports)).
- **Read-only audit / log views.** Set `subscribe: false` and `lock: false` on the manifest page.

## Related

- [OpenRegister Push Events](../Integrations/OpenRegister.md) — the wire format used by the subscription channel.
- [Object lifecycle — locking](../Features/objects.md) — the lock REST endpoints and behavior.
- [`useObjectSubscription`](https://github.com/ConductionNL/nextcloud-vue/blob/beta/docs/utilities/composables/use-object-subscription.md) — the lib composable that wires the subscription.
- [`useObjectLock`](https://github.com/ConductionNL/nextcloud-vue/blob/beta/docs/utilities/composables/use-object-lock.md) — the lib composable that wraps the lock endpoints.
- [`CnLockedBanner`](https://github.com/ConductionNL/nextcloud-vue/blob/beta/docs/components/cn-locked-banner.md) — the default "Locked by X" UI.
Loading
Loading