@terreno/mcp 0.31.0 → 56.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +15 -16
- package/dist/bootstrap.js.map +1 -1
- package/dist/docs/templates/bootstrap/backend/types/models/userTypes.template.md +4 -5
- package/dist/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
- package/dist/docs/templates/tools/model.template.md +2 -4
- package/dist/docs/versioned/next/README.md +1 -0
- package/dist/docs/versioned/next/explanation/README.md +1 -7
- package/dist/docs/versioned/next/how-to/README.md +2 -8
- package/dist/docs/versioned/next/how-to/deploy-to-gcp.md +228 -20
- package/dist/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
- package/dist/docs/versioned/next/implementationPlans/PLAN_INDEX.md +4 -57
- package/dist/docs/versioned/next/implementationPlans/README.md +30 -56
- package/dist/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +1 -2
- package/dist/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/build-terreno-app-validation.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/deploy-to-gcp.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/deploy-to-vercel.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/deployment-foundation.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/docs-reference-coverage.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/docs-tutorials-ai-first.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/examples-demo-coverage.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/oss-governance-baseline.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/public-roadmap-github.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
- package/dist/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
- package/dist/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
- package/dist/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
- package/dist/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
- package/dist/docs/versioned/next/implementationPlans/upgrade-guides-and-skill.md +0 -1
- package/dist/docs/versioned/next/implementationPlans/web-ssr-and-admin-spa.md +0 -1
- package/dist/docs/versioned/next/reference/README.md +1 -4
- package/dist/docs/versioned/next/reference/environment-variables.md +382 -220
- package/dist/docs/versioned/next/reference/syncdb.md +456 -0
- package/dist/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
- package/dist/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
- package/dist/docs/versioned/next/tasks/agentic-sdlc-plugin.md +5 -5
- package/dist/docs/versioned/next/tasks/deploy-to-gcp.md +11 -11
- package/dist/docs/versioned/next/tasks/deployment-foundation.md +12 -12
- package/dist/docs/versioned/next/tasks/docs-reference-coverage.md +4 -4
- package/dist/docs/versioned/next/tasks/oss-governance-baseline.md +18 -18
- package/dist/docs/versioned/next/tasks/positioning-django-rails-universal.md +4 -4
- package/dist/docs/versioned/next/tasks/public-roadmap-github.md +15 -15
- package/dist/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
- package/dist/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
- package/dist/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
- package/package.json +4 -5
- package/src/docs/templates/bootstrap/backend/types/models/userTypes.template.md +4 -5
- package/src/docs/templates/bootstrap/frontend/app/_layout.template.tsx +1 -1
- package/src/docs/templates/tools/model.template.md +2 -4
- package/src/docs/versioned/next/README.md +1 -0
- package/src/docs/versioned/next/explanation/README.md +1 -7
- package/src/docs/versioned/next/how-to/README.md +2 -8
- package/src/docs/versioned/next/how-to/deploy-to-gcp.md +228 -20
- package/src/docs/versioned/next/how-to/migrate-rtk-to-syncdb.md +445 -0
- package/src/docs/versioned/next/implementationPlans/PLAN_INDEX.md +4 -57
- package/src/docs/versioned/next/implementationPlans/README.md +30 -56
- package/src/docs/versioned/next/implementationPlans/agentic-sdlc-plugin.md +1 -2
- package/src/docs/versioned/next/implementationPlans/ai-dev-loop-boost.md +0 -1
- package/src/docs/versioned/next/implementationPlans/build-terreno-app-validation.md +0 -1
- package/src/docs/versioned/next/implementationPlans/deploy-to-gcp.md +0 -1
- package/src/docs/versioned/next/implementationPlans/deploy-to-vercel.md +0 -1
- package/src/docs/versioned/next/implementationPlans/deployment-foundation.md +0 -1
- package/src/docs/versioned/next/implementationPlans/docs-reference-coverage.md +0 -1
- package/src/docs/versioned/next/implementationPlans/docs-tutorials-ai-first.md +0 -1
- package/src/docs/versioned/next/implementationPlans/examples-demo-coverage.md +0 -1
- package/src/docs/versioned/next/implementationPlans/oss-governance-baseline.md +0 -1
- package/src/docs/versioned/next/implementationPlans/positioning-django-rails-universal.md +0 -1
- package/src/docs/versioned/next/implementationPlans/public-roadmap-github.md +0 -1
- package/src/docs/versioned/next/implementationPlans/rtk-to-syncdb-migration-docs.md +52 -47
- package/src/docs/versioned/next/implementationPlans/syncdb-api-inventory.md +303 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-codegen.md +252 -0
- package/src/docs/versioned/next/implementationPlans/syncdb-local-first.md +468 -420
- package/src/docs/versioned/next/implementationPlans/syncdb-phase-c-design.md +425 -0
- package/src/docs/versioned/next/implementationPlans/terreno-syncdb-2.md +800 -0
- package/src/docs/versioned/next/implementationPlans/upgrade-guides-and-skill.md +0 -1
- package/src/docs/versioned/next/implementationPlans/web-ssr-and-admin-spa.md +0 -1
- package/src/docs/versioned/next/reference/README.md +1 -4
- package/src/docs/versioned/next/reference/environment-variables.md +382 -220
- package/src/docs/versioned/next/reference/syncdb.md +456 -0
- package/src/docs/versioned/next/superpowers/plans/2026-07-28-admin-mobile-navigation.md +97 -0
- package/src/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md +36 -0
- package/src/docs/versioned/next/tasks/agentic-sdlc-plugin.md +5 -5
- package/src/docs/versioned/next/tasks/deploy-to-gcp.md +11 -11
- package/src/docs/versioned/next/tasks/deployment-foundation.md +12 -12
- package/src/docs/versioned/next/tasks/docs-reference-coverage.md +4 -4
- package/src/docs/versioned/next/tasks/oss-governance-baseline.md +18 -18
- package/src/docs/versioned/next/tasks/positioning-django-rails-universal.md +4 -4
- package/src/docs/versioned/next/tasks/public-roadmap-github.md +15 -15
- package/src/docs/versioned/next/tasks/rtk-to-syncdb-migration-docs.md +10 -10
- package/src/docs/versioned/next/tasks/syncdb-codegen.md +65 -0
- package/src/docs/versioned/next/tasks/syncdb-local-first.md +434 -117
- package/LICENSE +0 -21
- package/dist/__tests__/bootstrap.test.d.ts +0 -2
- package/dist/__tests__/bootstrap.test.d.ts.map +0 -1
- package/dist/__tests__/bootstrap.test.js +0 -299
- package/dist/__tests__/bootstrap.test.js.map +0 -1
- package/dist/__tests__/databaseQuery.test.d.ts +0 -2
- package/dist/__tests__/databaseQuery.test.d.ts.map +0 -1
- package/dist/__tests__/databaseQuery.test.js +0 -83
- package/dist/__tests__/databaseQuery.test.js.map +0 -1
- package/dist/__tests__/docsRoot.test.d.ts +0 -2
- package/dist/__tests__/docsRoot.test.d.ts.map +0 -1
- package/dist/__tests__/docsRoot.test.js +0 -59
- package/dist/__tests__/docsRoot.test.js.map +0 -1
- package/dist/__tests__/inferPackages.test.d.ts +0 -2
- package/dist/__tests__/inferPackages.test.d.ts.map +0 -1
- package/dist/__tests__/inferPackages.test.js +0 -35
- package/dist/__tests__/inferPackages.test.js.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/dist/__tests__/preload.js +0 -12
- package/dist/__tests__/preload.js.map +0 -1
- package/dist/__tests__/prompts.test.d.ts +0 -2
- package/dist/__tests__/prompts.test.d.ts.map +0 -1
- package/dist/__tests__/prompts.test.js +0 -295
- package/dist/__tests__/prompts.test.js.map +0 -1
- package/dist/__tests__/resources.test.d.ts +0 -2
- package/dist/__tests__/resources.test.d.ts.map +0 -1
- package/dist/__tests__/resources.test.js +0 -352
- package/dist/__tests__/resources.test.js.map +0 -1
- package/dist/__tests__/search.test.d.ts +0 -2
- package/dist/__tests__/search.test.d.ts.map +0 -1
- package/dist/__tests__/search.test.js +0 -163
- package/dist/__tests__/search.test.js.map +0 -1
- package/dist/__tests__/tools.test.d.ts +0 -2
- package/dist/__tests__/tools.test.d.ts.map +0 -1
- package/dist/__tests__/tools.test.js +0 -540
- package/dist/__tests__/tools.test.js.map +0 -1
- package/dist/__tests__/upgradeGuide.test.d.ts +0 -2
- package/dist/__tests__/upgradeGuide.test.d.ts.map +0 -1
- package/dist/__tests__/upgradeGuide.test.js +0 -22
- package/dist/__tests__/upgradeGuide.test.js.map +0 -1
- package/dist/docs/upgrades/0.31.0.md +0 -60
- package/dist/docs/versioned/next/explanation/deployment-architecture-gcp.md +0 -65
- package/dist/docs/versioned/next/explanation/deployment-baseline.md +0 -78
- package/dist/docs/versioned/next/explanation/positioning.md +0 -164
- package/dist/docs/versioned/next/explanation/repository-settings.md +0 -44
- package/dist/docs/versioned/next/explanation/roadmap-process.md +0 -330
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +0 -866
- package/dist/docs/versioned/next/how-to/build-for-web.md +0 -69
- package/dist/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +0 -115
- package/dist/docs/versioned/next/how-to/deploy-web-to-gcs-cdn.md +0 -183
- package/dist/docs/versioned/next/implementationPlans/b2b-platform-program.md +0 -114
- package/dist/docs/versioned/next/implementationPlans/billing-stripe.md +0 -158
- package/dist/docs/versioned/next/implementationPlans/comms-abstraction.md +0 -240
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-expo-push.md +0 -107
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +0 -107
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +0 -102
- package/dist/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +0 -93
- package/dist/docs/versioned/next/implementationPlans/infra-mcp-research.md +0 -188
- package/dist/docs/versioned/next/implementationPlans/infra-mcp.md +0 -269
- package/dist/docs/versioned/next/implementationPlans/native-module-baseline.md +0 -105
- package/dist/docs/versioned/next/implementationPlans/orgs-and-teams.md +0 -158
- package/dist/docs/versioned/next/implementationPlans/password-reset-and-email-verification.md +0 -147
- package/dist/docs/versioned/next/reference/admin-spa.md +0 -130
- package/dist/docs/versioned/next/reference/ai.md +0 -519
- package/dist/docs/versioned/next/reference/test.md +0 -108
- package/dist/docs/versioned/next/roadmap.md +0 -9
- package/dist/docs/versioned/next/tasks/README.md +0 -42
- package/dist/docs/versioned/next/tasks/billing-stripe.md +0 -75
- package/dist/docs/versioned/next/tasks/comms-abstraction.md +0 -77
- package/dist/docs/versioned/next/tasks/comms-adapter-expo-push.md +0 -29
- package/dist/docs/versioned/next/tasks/comms-adapter-sendgrid.md +0 -29
- package/dist/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +0 -29
- package/dist/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +0 -19
- package/dist/docs/versioned/next/tasks/infra-mcp.md +0 -43
- package/dist/docs/versioned/next/tasks/native-module-baseline.md +0 -42
- package/dist/docs/versioned/next/tasks/orgs-and-teams.md +0 -70
- package/dist/docs/versioned/next/tasks/password-reset-and-email-verification.md +0 -60
- package/src/docs/upgrades/0.31.0.md +0 -60
- package/src/docs/versioned/next/explanation/deployment-architecture-gcp.md +0 -65
- package/src/docs/versioned/next/explanation/deployment-baseline.md +0 -78
- package/src/docs/versioned/next/explanation/positioning.md +0 -164
- package/src/docs/versioned/next/explanation/repository-settings.md +0 -44
- package/src/docs/versioned/next/explanation/roadmap-process.md +0 -330
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +0 -866
- package/src/docs/versioned/next/how-to/build-for-web.md +0 -69
- package/src/docs/versioned/next/how-to/deploy-backend-to-cloud-run.md +0 -115
- package/src/docs/versioned/next/how-to/deploy-web-to-gcs-cdn.md +0 -183
- package/src/docs/versioned/next/implementationPlans/b2b-platform-program.md +0 -114
- package/src/docs/versioned/next/implementationPlans/billing-stripe.md +0 -158
- package/src/docs/versioned/next/implementationPlans/comms-abstraction.md +0 -240
- package/src/docs/versioned/next/implementationPlans/comms-adapter-expo-push.md +0 -107
- package/src/docs/versioned/next/implementationPlans/comms-adapter-sendgrid.md +0 -107
- package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-sms.md +0 -102
- package/src/docs/versioned/next/implementationPlans/comms-adapter-twilio-verify.md +0 -93
- package/src/docs/versioned/next/implementationPlans/infra-mcp-research.md +0 -188
- package/src/docs/versioned/next/implementationPlans/infra-mcp.md +0 -269
- package/src/docs/versioned/next/implementationPlans/native-module-baseline.md +0 -105
- package/src/docs/versioned/next/implementationPlans/orgs-and-teams.md +0 -158
- package/src/docs/versioned/next/implementationPlans/password-reset-and-email-verification.md +0 -147
- package/src/docs/versioned/next/reference/admin-spa.md +0 -130
- package/src/docs/versioned/next/reference/ai.md +0 -519
- package/src/docs/versioned/next/reference/test.md +0 -108
- package/src/docs/versioned/next/roadmap.md +0 -9
- package/src/docs/versioned/next/tasks/README.md +0 -42
- package/src/docs/versioned/next/tasks/billing-stripe.md +0 -75
- package/src/docs/versioned/next/tasks/comms-abstraction.md +0 -77
- package/src/docs/versioned/next/tasks/comms-adapter-expo-push.md +0 -29
- package/src/docs/versioned/next/tasks/comms-adapter-sendgrid.md +0 -29
- package/src/docs/versioned/next/tasks/comms-adapter-twilio-sms.md +0 -29
- package/src/docs/versioned/next/tasks/comms-adapter-twilio-verify.md +0 -19
- package/src/docs/versioned/next/tasks/infra-mcp.md +0 -43
- package/src/docs/versioned/next/tasks/native-module-baseline.md +0 -42
- package/src/docs/versioned/next/tasks/orgs-and-teams.md +0 -70
- package/src/docs/versioned/next/tasks/password-reset-and-email-verification.md +0 -60
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# @terreno/syncdb
|
|
2
|
+
|
|
3
|
+
Local-first data layer for Terreno frontends. A TinyBase `MergeableStore` on device is the UI source of truth: reads come from the local store, writes apply optimistically into a durable outbox, and the server reconciles asynchronously over a socket delta protocol with HTTP snapshot catch-up. Supersedes `@terreno/rtk` for data-synchronization concerns.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Architecture](#architecture)
|
|
9
|
+
- [Backend setup](#backend-setup)
|
|
10
|
+
- [createSyncDb configuration](#createsyncdb-configuration)
|
|
11
|
+
- [SyncDb client methods](#syncdb-client-methods)
|
|
12
|
+
- [React hooks](#react-hooks)
|
|
13
|
+
- [Conflict API](#conflict-api)
|
|
14
|
+
- [Sync status API](#sync-status-api)
|
|
15
|
+
- [Stream scoping](#stream-scoping)
|
|
16
|
+
- [Sync protocol](#sync-protocol)
|
|
17
|
+
- [Encryption at rest](#encryption-at-rest)
|
|
18
|
+
- [Testing](#testing)
|
|
19
|
+
- [Environment variables](#environment-variables)
|
|
20
|
+
- [Conventions](#conventions)
|
|
21
|
+
|
|
22
|
+
## Key exports
|
|
23
|
+
|
|
24
|
+
- `createSyncDb`, `SyncDb`, `SyncDbConfig`, `MutateArgs`
|
|
25
|
+
- `betterAuthAdapter`, `AuthProvider`
|
|
26
|
+
- `listConflicts`, `wipeLocalData`, `generateMutationId`
|
|
27
|
+
- React (`@terreno/syncdb/react`): `SyncDbProvider`, `useEntity`, `useQuery`, `useEntityIds`, `useMutate`, `useSyncStatus`, `useConflicts`, `useSyncDebugLog`
|
|
28
|
+
- Testing (`@terreno/syncdb/testing`): `createFakeTransport`
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun install @terreno/syncdb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Peer dependencies:** `react` (optional — only for `@terreno/syncdb/react`), `luxon`, `tinybase`.
|
|
37
|
+
|
|
38
|
+
**Native persistence:** install `expo-sqlite` in the **app** (not only in a library):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bunx expo install expo-sqlite
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Expo autolinking walks the app's own dependencies; a nested copy leaves the native module out of the build. Rebuild the native project after adding it.
|
|
45
|
+
|
|
46
|
+
**Why `@terreno/syncdb/react` is a separate subpath:** `react` is an optional peer dependency. Keeping React bindings off the main entry lets non-React consumers (Node tests, scripts) import `@terreno/syncdb` without loading React.
|
|
47
|
+
|
|
48
|
+
## Architecture
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
FRONTEND (@terreno/syncdb) BACKEND (@terreno/api)
|
|
52
|
+
┌───────────────────────────────────┐ ┌────────────────────────────────────┐
|
|
53
|
+
│ React hooks (useQuery, useEntity, │ │ modelRouter(path, Model, {sync}) │
|
|
54
|
+
│ useMutate, useSyncStatus, ...) │ │ └─ sync registry + validation │
|
|
55
|
+
│ │ ▲ │ │ │
|
|
56
|
+
│ ▼ │ │ │ syncPlugin (schema) │
|
|
57
|
+
│ TinyBase MergeableStore │ │ └─ stamps _syncSeq per write │
|
|
58
|
+
│ {collection} tables + _outbox │ │ │
|
|
59
|
+
│ + _cursors + _conflicts │ │ SyncApp (HTTP) RealtimeApp │
|
|
60
|
+
│ │ ▲ │ │ /sync/snapshot (Socket.io + │
|
|
61
|
+
│ ▼ │ │ │ /sync/mutate change streams)│
|
|
62
|
+
│ Persister (AES-GCM IndexedDB on │ │ /sync/key │ │
|
|
63
|
+
│ web, expo-sqlite on native) │ └─────────┬───────────────┼──────────┘
|
|
64
|
+
└──────┬─────────────────▲──────────┘ │ │
|
|
65
|
+
│ │ │ │
|
|
66
|
+
│ sync:mutate ─────────────────────────────►│ │
|
|
67
|
+
│ sync:ack / sync:nack ◄────────────────────┘ │
|
|
68
|
+
│ sync:delta ◄──────────────────────────────────────────────┘
|
|
69
|
+
│ GET /sync/snapshot (bootstrap + catch-up, HTTP)
|
|
70
|
+
└── POST /sync/mutate (fallback while the socket is down)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Every mutation executes the same `@terreno/api` modelRouter write path as REST — identical permissions, hooks, and validation.
|
|
74
|
+
|
|
75
|
+
## Backend setup
|
|
76
|
+
|
|
77
|
+
Register sync on the backend before the frontend client can sync a collection.
|
|
78
|
+
|
|
79
|
+
### Required schema plugins
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import {isDeletedPlugin, syncPlugin} from "@terreno/api";
|
|
83
|
+
|
|
84
|
+
todoSchema.plugin(isDeletedPlugin); // soft delete — required for tombstone catch-up
|
|
85
|
+
todoSchema.plugin(syncPlugin); // stamps per-stream _syncSeq on every write
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Apply plugins **before** the model compiles. Registration validates their presence.
|
|
89
|
+
|
|
90
|
+
### modelRouter sync config
|
|
91
|
+
|
|
92
|
+
Use the three-argument `modelRouter` form:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import {modelRouter, OwnerQueryFilter, Permissions} from "@terreno/api";
|
|
96
|
+
|
|
97
|
+
const todoRouter = modelRouter("/todos", Todo, {
|
|
98
|
+
permissions: {
|
|
99
|
+
create: [Permissions.IsAuthenticated],
|
|
100
|
+
delete: [Permissions.IsOwner],
|
|
101
|
+
list: [Permissions.IsAuthenticated],
|
|
102
|
+
read: [Permissions.IsOwner],
|
|
103
|
+
update: [Permissions.IsOwner],
|
|
104
|
+
},
|
|
105
|
+
preCreate: (body, req) => ({...body, ownerId: (req.user as unknown as UserDocument)?._id}),
|
|
106
|
+
queryFilter: OwnerQueryFilter,
|
|
107
|
+
sync: {
|
|
108
|
+
scope: {type: "owner"}, // stream = todos|owner:{ownerId}
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### SyncApp and RealtimeApp
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import {RealtimeApp, SyncApp, TerrenoApp} from "@terreno/api";
|
|
117
|
+
|
|
118
|
+
new TerrenoApp({userModel: User})
|
|
119
|
+
.register(todoRouter)
|
|
120
|
+
.register(new SyncApp()) // GET /sync/snapshot, POST /sync/mutate, GET /sync/key, ...
|
|
121
|
+
.register(new RealtimeApp()) // Socket.io; sync:subscribe, sync:delta, sync:mutate
|
|
122
|
+
.start();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- **`SyncApp`** — HTTP sync routes (`/sync/snapshot`, `/sync/mutate`, `/sync/mutate/batch`, `/sync/key`, `/sync/streams`, `/sync/entities`).
|
|
126
|
+
- **`RealtimeApp`** — Socket.io server with change-stream-driven `sync:delta` emission. Requires a **MongoDB replica set** (change streams).
|
|
127
|
+
- **`ensureSyncIndexes`** — `TerrenoApp.start()` awaits index builds for snapshot queries and sync bookkeeping (`SyncMutation.mutationId` unique, `SyncCounter.stream` unique, etc.). Hosts that build Express without `TerrenoApp.start()` should await `ensureSyncIndexes()` themselves.
|
|
128
|
+
|
|
129
|
+
Socket auth accepts legacy JWTs by default. For Better Auth sessions:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
new RealtimeApp({betterAuth: {auth, userModel: User}})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## createSyncDb configuration
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import {betterAuthAdapter, createSyncDb} from "@terreno/syncdb";
|
|
139
|
+
|
|
140
|
+
export const syncDb = createSyncDb({
|
|
141
|
+
name: "myapp",
|
|
142
|
+
collections: ["todos"],
|
|
143
|
+
authProvider: betterAuthAdapter(authClient),
|
|
144
|
+
baseUrl: "http://localhost:4000",
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
| Option | Type | Default | Description |
|
|
149
|
+
|--------|------|---------|-------------|
|
|
150
|
+
| `name` | `string` | — (required) | Persisted database name |
|
|
151
|
+
| `collections` | `string[]` | — (required) | Collection names to sync (local tables + subscriptions) |
|
|
152
|
+
| `authProvider` | `AuthProvider` | — (required) | `{getToken, getUserId, onAuthChange, refresh?}` |
|
|
153
|
+
| `baseUrl` | `string` | — | Server origin; required unless both `transport` and `httpChannel` are injected |
|
|
154
|
+
| `transport` | `SyncTransport` | socket transport from `baseUrl` | Override for tests or custom wiring |
|
|
155
|
+
| `httpChannel` | `HttpChannel` | built from `baseUrl` | HTTP fallback channel override |
|
|
156
|
+
| `persisterFactory` | `PersisterFactory` | platform default (IndexedDB web, expo-sqlite native) | Storage backend override |
|
|
157
|
+
| `keyProvider` | `KeyProvider` | server-derived via `GET /sync/key` | Web encryption key provider |
|
|
158
|
+
| `idbGetImpl` / `idbSetImpl` | test hooks | — | Simulate IndexedDB failures (tests only) |
|
|
159
|
+
| `reconcileIntervalMs` | `number` | `300000` (5 min); `0` disables | Periodic reconcile + outbox replay timer |
|
|
160
|
+
| `seqJumpReconcileMinIntervalMs` | `number` | `30000` (30s) | Per-stream rate limit for seq-jump-triggered reconciles |
|
|
161
|
+
| `now` | `() => number` | `Date.now` | Injectable clock (tests) |
|
|
162
|
+
| `random` | `() => number` | `Math.random` | Injectable RNG for backoff jitter (tests) |
|
|
163
|
+
| `debug` | `boolean \| SyncDebugLogOptions` | `false` | In-memory debug event log |
|
|
164
|
+
| `onAuthRequired` | `() => void` | — | Fires once per auth-pause episode (prompt re-login) |
|
|
165
|
+
| `wipeOnSignOut` | `boolean` | `false` | `signOut()` also wipes local data |
|
|
166
|
+
| `batchSize` | `number` | `50` (server caps at 100) | Max mutations per batched drain send |
|
|
167
|
+
| `haltQueueOnConflict` | `boolean` | `false` | `true` = conflict halts entire drain; `false` = per-entity blocking |
|
|
168
|
+
| `onDecryptFailure` | `() => void` | wipe + re-bootstrap (with `console.warn`) | Override web decrypt-failure recovery |
|
|
169
|
+
| `tombstoneRetentionMs` | `number` | `7776000000` (90 days); `0` disables | Client tombstone compaction after successful reconcile |
|
|
170
|
+
| `startAuthRetryAttempts` | `number` | `3`; `1` disables | `start()` attempts to resolve `getUserId()` |
|
|
171
|
+
| `startAuthRetryDelayMs` | `number` | `250` | Delay between those attempts |
|
|
172
|
+
|
|
173
|
+
`start()` needs an authenticated user. It retries `getUserId()` briefly (transient session races after login) before rejecting.
|
|
174
|
+
|
|
175
|
+
## SyncDb client methods
|
|
176
|
+
|
|
177
|
+
| Method | Description |
|
|
178
|
+
|--------|-------------|
|
|
179
|
+
| `start()` | Resolve user, run wipe-on-user-change check, start persistence, connect transport, subscribe collections, start reconcile timer. Resolves even when offline. Idempotent while already started. |
|
|
180
|
+
| `stop()` | Disconnect, stop persistence, clear timers and listeners. |
|
|
181
|
+
| `goOffline()` | Simulated outage: disconnect transport, pause replay/reconcile/timer; local mutations keep queueing. |
|
|
182
|
+
| `goOnline()` | End simulated outage; reconnect triggers reconcile + outbox replay. |
|
|
183
|
+
| `mutate({collection, operation, id?, data?})` | Optimistic local write + durable outbox enqueue + fire-and-forget replay. Returns `{mutationId, id}`. |
|
|
184
|
+
| `reconcile()` | HTTP snapshot catch-up for every known stream; runs tombstone compaction on success. |
|
|
185
|
+
| `forceResync()` | Purge every known stream locally and re-bootstrap from cursor 0 (outbox/conflicts untouched). Returns `{ok, reason?, streams, purged, repaired}`. |
|
|
186
|
+
| `replayOutbox()` | Drain queued mutations for the current user now. |
|
|
187
|
+
| `resolveConflict({mutationId, strategy})` | Apply `"useServer"` or `"keepMine"` to a recorded conflict. |
|
|
188
|
+
| `retryFailed({entityId})` | Re-enable an entity's queued successors after a terminal validation failure. |
|
|
189
|
+
| `signOut()` | Explicit teardown like `stop()`; wipes local data only when `wipeOnSignOut: true`. |
|
|
190
|
+
| `getSyncStatus()` | Snapshot of aggregate sync state (see [Sync status API](#sync-status-api)). |
|
|
191
|
+
| `onStatusChange(callback)` | Subscribe to connectivity/syncing changes; returns unsubscribe. |
|
|
192
|
+
| `store` | Read surface over the local MergeableStore (`SyncStore`). |
|
|
193
|
+
| `outbox` | Durable mutation outbox (`Outbox`). |
|
|
194
|
+
| `debug` | `SyncDebugLog` when `debug` is enabled; otherwise `undefined`. |
|
|
195
|
+
|
|
196
|
+
## React hooks
|
|
197
|
+
|
|
198
|
+
Import from `@terreno/syncdb/react`:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import {
|
|
202
|
+
SyncDbProvider,
|
|
203
|
+
useConflicts,
|
|
204
|
+
useEntity,
|
|
205
|
+
useEntityIds,
|
|
206
|
+
useMutate,
|
|
207
|
+
useQuery,
|
|
208
|
+
useSyncDbClient,
|
|
209
|
+
useSyncDebugLog,
|
|
210
|
+
useSyncStatus,
|
|
211
|
+
} from "@terreno/syncdb/react";
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Wrap the app (or a subtree) in `SyncDbProvider`:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
<SyncDbProvider client={syncDb}>
|
|
218
|
+
<TodoList />
|
|
219
|
+
</SyncDbProvider>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### `useEntity(collection, id)`
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const {data, deleted, seq, isPending} = useEntity<Todo>("todos", id);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Subscribe to a single entity. Re-renders when that row changes. `isPending` is true while an outbox mutation protects optimistic state.
|
|
229
|
+
|
|
230
|
+
### `useQuery(collection, options?)`
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
const todos = useQuery<Todo>("todos", {
|
|
234
|
+
filter: (t) => !t.completed,
|
|
235
|
+
sort: (a, b) => b.created.localeCompare(a.created),
|
|
236
|
+
includeDeleted: false,
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Returns decoded entity data arrays. Tombstones excluded unless `includeDeleted: true`. Filter and sort run in JS — memoize callbacks when collections are large.
|
|
241
|
+
|
|
242
|
+
### `useEntityIds(collection, options?)`
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
const ids = useEntityIds<Todo>("todos", {filter: (t) => !t.completed, sort: byCreatedDesc});
|
|
246
|
+
return ids.map((id) => <TodoRow key={id} id={id} />);
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Same options as `useQuery`, but returns only ordered ids with **referential stability** — the array identity changes only when membership or order changes, not on field updates. Pair with per-row `useEntity` for large lists.
|
|
250
|
+
|
|
251
|
+
### `useMutate(collection)`
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const {create, update, remove} = useMutate("todos");
|
|
255
|
+
|
|
256
|
+
const {id, mutationId} = create({data: {title: "Milk", completed: false}});
|
|
257
|
+
update({id, data: {completed: true}});
|
|
258
|
+
remove({id});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Each call applies locally, enqueues in the outbox, and kicks off replay.
|
|
262
|
+
|
|
263
|
+
### `useSyncStatus()`
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const status = useSyncStatus();
|
|
267
|
+
// status.isOnline, status.isSyncing, status.queuedCount, status.conflictCount, ...
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Reactive aggregate status; re-renders on outbox, conflict, cursor, connectivity, and drain-progress changes.
|
|
271
|
+
|
|
272
|
+
### `useConflicts()`
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
const {conflicts, resolve} = useConflicts();
|
|
276
|
+
resolve({mutationId, strategy: "useServer"});
|
|
277
|
+
resolve({mutationId, strategy: "keepMine"});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### `useSyncDebugLog()`
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const {enabled, events, stats, log, clear} = useSyncDebugLog();
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Available when the client was created with `debug: true`. `log?.snapshot()` returns a JSON-serializable object suitable for debug UIs or tooling.
|
|
287
|
+
|
|
288
|
+
### `useSyncDbClient()`
|
|
289
|
+
|
|
290
|
+
Returns the `SyncDb` instance from context (escape hatch for imperative calls like `forceResync()`).
|
|
291
|
+
|
|
292
|
+
## Conflict API
|
|
293
|
+
|
|
294
|
+
A **conflict** occurs when the client's `baseVersion` (last seen `_syncSeq`) does not match the server's current seq. The server responds with a `conflict` nack (HTTP **409** on `POST /sync/mutate`, or `sync:nack` with `code: "conflict"` on the socket) carrying the canonical server document.
|
|
295
|
+
|
|
296
|
+
The client records conflicts in the local `_conflicts` table. Read them with:
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import {listConflicts} from "@terreno/syncdb";
|
|
300
|
+
|
|
301
|
+
const conflicts = listConflicts({store: client.store});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
In React, prefer `useConflicts()`.
|
|
305
|
+
|
|
306
|
+
### Resolution strategies
|
|
307
|
+
|
|
308
|
+
| Strategy | Behavior |
|
|
309
|
+
|----------|----------|
|
|
310
|
+
| `"useServer"` | Overwrite local entity with canonical server data/seq; clear pending state; discard conflicted outbox row. Writes a **tombstone** when the server side is deleted. |
|
|
311
|
+
| `"keepMine"` | Re-enqueue the mutation under a **fresh** `mutationId` with `baseVersion` set to the server's seq; local optimistic data is kept. |
|
|
312
|
+
|
|
313
|
+
Resolve via `client.resolveConflict({mutationId, strategy})` or `useConflicts().resolve(...)`.
|
|
314
|
+
|
|
315
|
+
### Conflict handling modes
|
|
316
|
+
|
|
317
|
+
- **Default (`haltQueueOnConflict: false`)** — per-entity blocking. Other entities keep draining.
|
|
318
|
+
- **`haltQueueOnConflict: true`** — any conflict halts the entire drain until resolved.
|
|
319
|
+
|
|
320
|
+
`getSyncStatus().blockedEntities` counts entities blocked by conflicts or terminal validation failures.
|
|
321
|
+
|
|
322
|
+
## Sync status API
|
|
323
|
+
|
|
324
|
+
`getSyncStatus()` / `useSyncStatus()` return `SyncStatus`:
|
|
325
|
+
|
|
326
|
+
| Field | Type | Description |
|
|
327
|
+
|-------|------|-------------|
|
|
328
|
+
| `isOnline` | `boolean` | Transport connected |
|
|
329
|
+
| `isSyncing` | `boolean` | Reconcile or replay in flight |
|
|
330
|
+
| `queuedCount` | `number` | Outbox rows awaiting send |
|
|
331
|
+
| `conflictCount` | `number` | Unresolved conflicts |
|
|
332
|
+
| `failedCount` | `number` | Terminal `failed` outbox rows |
|
|
333
|
+
| `paused` | `"auth"?` | Replay paused for auth failure |
|
|
334
|
+
| `blockedEntities` | `number` | Entities blocked (conflict or validation) |
|
|
335
|
+
| `draining` | `boolean` | Outbox drain actively running |
|
|
336
|
+
| `sentThisDrain` | `number` | Mutations sent in current/most recent drain |
|
|
337
|
+
| `totalThisDrain` | `number` | Queue length when drain began |
|
|
338
|
+
| `streams` | `Record<string, number>` | Per-stream cursors (stream → highest applied seq) |
|
|
339
|
+
| `collections` | `Record<string, SyncCollectionStatus>` | Per-collection queued/conflict/failed counts |
|
|
340
|
+
| `persistence` | `"durable" \| "memory" \| "error"` | Local persistence health (web) |
|
|
341
|
+
|
|
342
|
+
`SyncCollectionStatus`: `{queuedCount, conflictCount, failedCount}`.
|
|
343
|
+
|
|
344
|
+
## Stream scoping
|
|
345
|
+
|
|
346
|
+
Streams are the unit of ordered delivery, keyed `{collection}|{scope}`:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
sync: {scope: {type: "owner"}} // todos|owner:{ownerId}
|
|
350
|
+
sync: {scope: {type: "owner", field: "userId"}} // todos|owner:{userId}
|
|
351
|
+
sync: {scope: {type: "tenant", field: "organizationId"}} // todos|tenant:{orgId}
|
|
352
|
+
sync: {scope: {type: "broadcast"}} // todos|all
|
|
353
|
+
sync: {
|
|
354
|
+
scope: (doc) => String(doc.workspaceId),
|
|
355
|
+
snapshotFilter: (user) => ({workspaceId: {$in: [...]}}), // required for custom
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
- **Owner** streams use the authenticated socket's user id (client cannot pick another user's stream).
|
|
360
|
+
- **Tenant/custom** scopes resolve memberships via `SyncApp` `getUserScopes`.
|
|
361
|
+
- **`snapshotFilter`** restricts `GET /sync/snapshot` server-side. Auto-derived for owner/tenant; required for custom resolver scopes.
|
|
362
|
+
|
|
363
|
+
## Sync protocol
|
|
364
|
+
|
|
365
|
+
### HTTP routes (`SyncApp`, all authenticated)
|
|
366
|
+
|
|
367
|
+
| Endpoint | Purpose |
|
|
368
|
+
|----------|---------|
|
|
369
|
+
| `GET /sync/snapshot?collection=&stream=&cursor=&limit=` | Bootstrap + catch-up per stream |
|
|
370
|
+
| `GET /sync/streams` | Current stream membership for the user |
|
|
371
|
+
| `GET /sync/entities` | Point lookup for entity repair |
|
|
372
|
+
| `POST /sync/mutate` | Single mutation (HTTP fallback) |
|
|
373
|
+
| `POST /sync/mutate/batch` | Batched mutations (max 100, strict order, stop at first non-ack) |
|
|
374
|
+
| `GET /sync/key` | Per-user encryption key material (web) |
|
|
375
|
+
|
|
376
|
+
Conflict responses on mutate: **409** with `{nack}` body (`code: "conflict"`).
|
|
377
|
+
|
|
378
|
+
### Socket events (`RealtimeApp`)
|
|
379
|
+
|
|
380
|
+
| Event | Direction | Payload |
|
|
381
|
+
|-------|-----------|---------|
|
|
382
|
+
| `sync:subscribe` / `sync:unsubscribe` | client → server | `{collections: string[]}` |
|
|
383
|
+
| `sync:subscribed` | server → client | `{collection, streams}` |
|
|
384
|
+
| `sync:error` | server → client | `{collection, message}` |
|
|
385
|
+
| `sync:delta` | server → client | `{collection, id, method, data?, seq, stream, deleted?, frontierSeq?}` |
|
|
386
|
+
| `sync:mutate` | client → server | `{mutationId, collection, operation, id?, data?, baseVersion?}` |
|
|
387
|
+
| `sync:ack` | server → client | `{mutationId, id, seq}` |
|
|
388
|
+
| `sync:nack` | server → client | `{mutationId, code, serverDoc?, serverSeq?, serverDeleted?, message?, retryAfterMs?}` |
|
|
389
|
+
| `sync:mutateBatch` | client → server | `{mutations: SyncMutateRequest[], batchId?}` |
|
|
390
|
+
| `sync:auth-expired` | server → client | `{reason: "expired" \| "disabled"}` — session re-validation sweep; client enters auth-pause |
|
|
391
|
+
|
|
392
|
+
Limits: 50 collection subscriptions per socket; 100 mutations/second per socket (each batch mutation counts); batches capped at 100.
|
|
393
|
+
|
|
394
|
+
## Encryption at rest
|
|
395
|
+
|
|
396
|
+
**Web:** AES-256-GCM encrypted IndexedDB by default via pluggable `KeyProvider`:
|
|
397
|
+
|
|
398
|
+
- **`createServerKeyProvider`** (default) — derives key from `GET /sync/key` material (HKDF); server can rotate/revoke.
|
|
399
|
+
- **`createLocalKeyProvider`** — device-local random key; no server copy.
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import {createLocalKeyProvider, createSyncDb} from "@terreno/syncdb";
|
|
403
|
+
|
|
404
|
+
const syncDb = createSyncDb({
|
|
405
|
+
name: "myapp",
|
|
406
|
+
collections,
|
|
407
|
+
authProvider,
|
|
408
|
+
baseUrl,
|
|
409
|
+
keyProvider: createLocalKeyProvider(),
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Undecryptable persisted data triggers wipe + re-bootstrap by default (`onDecryptFailure` to override). Storage read errors surface `persistence: "error"` without overwriting the blob.
|
|
414
|
+
|
|
415
|
+
**Native:** plaintext SQLite in the OS sandbox (by design).
|
|
416
|
+
|
|
417
|
+
## Testing
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
import {createFakeTransport} from "@terreno/syncdb/testing";
|
|
421
|
+
import {createSyncDb} from "@terreno/syncdb";
|
|
422
|
+
|
|
423
|
+
const transport = createFakeTransport();
|
|
424
|
+
const client = createSyncDb({
|
|
425
|
+
name: "test",
|
|
426
|
+
collections: ["todos"],
|
|
427
|
+
authProvider: fakeAuth,
|
|
428
|
+
transport,
|
|
429
|
+
httpChannel: fakeHttp,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
transport.respondWithAck();
|
|
433
|
+
transport.deliverDelta({...});
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`createFakeTransport` records sent mutations, simulates connectivity, delivers deltas, and queues ack/nack responders.
|
|
437
|
+
|
|
438
|
+
## Environment variables
|
|
439
|
+
|
|
440
|
+
Syncdb reads no environment variables directly. Host apps typically set:
|
|
441
|
+
|
|
442
|
+
| Variable | Used by | Description |
|
|
443
|
+
|----------|---------|-------------|
|
|
444
|
+
| `EXPO_PUBLIC_API_URL` | `@terreno/rtk` `baseUrl` | Backend origin passed to `createSyncDb({baseUrl})` |
|
|
445
|
+
|
|
446
|
+
Backend sync requires a MongoDB replica set (`MONGO_URI` with `replicaSet=`) for `RealtimeApp` change streams.
|
|
447
|
+
|
|
448
|
+
## Conventions
|
|
449
|
+
|
|
450
|
+
- **Local-first only** — there is no server-first mode. The local store is always the read source.
|
|
451
|
+
- **Synced models need `String` `_id`** — offline creates mint client ids (UUIDs); default ObjectId `_id` cast-fails.
|
|
452
|
+
- **Soft delete only** on synced models — `isDeletedPlugin` required; hard deletes break tombstone catch-up.
|
|
453
|
+
- **No `bulkWrite` / `updateMany` / `deleteMany`** on synced models — use per-document loops.
|
|
454
|
+
- **Do not write reserved tables** (`_outbox`, `_cursors`, `_conflicts`) directly — use `client.mutate`, hooks, and `resolveConflict`.
|
|
455
|
+
- **User switch wipes local data** — confirmed different-user login clears the previous user's store; bare logout/401 does not (INV-2).
|
|
456
|
+
- **Keep `@terreno/rtk` for non-synced routes** — generated OpenAPI hooks remain the right tool for custom REST endpoints, admin, and auth configuration. See [How to migrate from RTK to syncdb](../how-to/migrate-rtk-to-syncdb.md).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Admin Mobile Navigation Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** On viewports below 768px, replace the admin shell’s fixed sidebar with a hamburger-triggered left slide-over drawer.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Keep navigation content in one shared internal component. `AdminShell` uses `useWindowDimensions` to switch between desktop rail and mobile header + overlay drawer. Drawer closes on backdrop, close button, and nav selection.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** React Native / Expo, `@terreno/ui` (`Box`, `Heading`, `Text`, `IconButton`), bun test + `@testing-library/react-native`.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Breakpoint: below 768px → mobile; 768px and above → desktop sidebar.
|
|
14
|
+
- Menu style: left slide-over drawer with dismissible backdrop.
|
|
15
|
+
- Accessibility: hamburger labeled "Open navigation menu"; close labeled "Close navigation menu".
|
|
16
|
+
- Do not change public `AdminShellProps` API unless required.
|
|
17
|
+
- Always support React Native Web.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
### Task 1: Failing AdminShell responsive tests
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Create: `admin-frontend/src/AdminShell.test.tsx`
|
|
25
|
+
|
|
26
|
+
**Interfaces:**
|
|
27
|
+
- Consumes: `AdminShell` from `./AdminShell`; mocked `./useAdminConfig` and `expo-router`
|
|
28
|
+
- Produces: failing tests that assert desktop sidebar at ≥768, mobile header below 768, drawer open/close, nav closes drawer, a11y labels
|
|
29
|
+
|
|
30
|
+
- [ ] **Step 1: Write the failing test file**
|
|
31
|
+
|
|
32
|
+
Mock `useWindowDimensions` via a mutable width, mock `useAdminConfig` with a small config (Home + one model), mock `expo-router.router.push`. Assert:
|
|
33
|
+
|
|
34
|
+
1. width 1024 → `admin-shell-sidebar` present; hamburger absent
|
|
35
|
+
2. width 500 → sidebar absent; `admin-shell-menu-button` present with accessibilityLabel "Open navigation menu"
|
|
36
|
+
3. press menu → `admin-shell-drawer` and backdrop appear; close button has "Close navigation menu"
|
|
37
|
+
4. press backdrop / close → drawer gone
|
|
38
|
+
5. open drawer, press Home nav → `router.push` called and drawer closed
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 2: Run tests and confirm failure**
|
|
41
|
+
|
|
42
|
+
Run: `cd admin-frontend && bun test src/AdminShell.test.tsx`
|
|
43
|
+
Expected: FAIL (missing mobile UI / testIDs)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### Task 2: Implement responsive AdminShell
|
|
48
|
+
|
|
49
|
+
**Files:**
|
|
50
|
+
- Modify: `admin-frontend/src/AdminShell.tsx`
|
|
51
|
+
- Test: `admin-frontend/src/AdminShell.test.tsx`
|
|
52
|
+
|
|
53
|
+
**Interfaces:**
|
|
54
|
+
- Consumes: nav data from `useAdminConfig` (existing)
|
|
55
|
+
- Produces: `MOBILE_BREAKPOINT = 768`; shared sidebar body; mobile header; overlay drawer with backdrop
|
|
56
|
+
|
|
57
|
+
- [ ] **Step 1: Extract shared sidebar body**
|
|
58
|
+
|
|
59
|
+
Create an internal `AdminShellSidebarNav` that receives navigate callbacks, grouped models, scripts, screens, configurationPath, footer, sidebarVariant, and optional `onNavigate` that runs after each nav press (used to close the drawer).
|
|
60
|
+
|
|
61
|
+
- [ ] **Step 2: Wire responsive chrome**
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
const MOBILE_BREAKPOINT = 768;
|
|
65
|
+
const {width} = useWindowDimensions();
|
|
66
|
+
const isMobileLayout = width < MOBILE_BREAKPOINT;
|
|
67
|
+
const [isNavOpen, setIsNavOpen] = useState(false);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Desktop: existing fixed 280px sidebar.
|
|
71
|
+
Mobile: compact header (`admin-shell-mobile-header`) with `IconButton` (`iconName="bars"`, testID `admin-shell-menu-button`) + "Admin" heading; main content full width.
|
|
72
|
+
When `isNavOpen`: absolute full-screen overlay (`admin-shell-drawer`) with backdrop (`admin-shell-drawer-backdrop`) and 280px panel containing close `IconButton` (`iconName="xmark"`, testID `admin-shell-drawer-close`) + shared nav. Close on backdrop, close button, and after navigate.
|
|
73
|
+
|
|
74
|
+
- [ ] **Step 3: Close drawer when resizing to desktop**
|
|
75
|
+
|
|
76
|
+
`useEffect` clears `isNavOpen` when `!isMobileLayout`.
|
|
77
|
+
|
|
78
|
+
- [ ] **Step 4: Run tests**
|
|
79
|
+
|
|
80
|
+
Run: `cd admin-frontend && bun test src/AdminShell.test.tsx`
|
|
81
|
+
Expected: PASS
|
|
82
|
+
|
|
83
|
+
- [ ] **Step 5: Compile and lint**
|
|
84
|
+
|
|
85
|
+
Run: `cd admin-frontend && bun run compile && bun run lint`
|
|
86
|
+
Expected: clean
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### Task 3: Manual UI verification
|
|
91
|
+
|
|
92
|
+
**Files:** none (runtime check in example-frontend admin)
|
|
93
|
+
|
|
94
|
+
- [ ] **Step 1:** With backend + `frontend:web` running, open `/admin` as `admin@example.com`
|
|
95
|
+
- [ ] **Step 2:** Narrow viewport below 768 → hamburger header, no fixed sidebar
|
|
96
|
+
- [ ] **Step 3:** Open drawer, navigate, confirm close; widen to ≥768 → sidebar returns
|
|
97
|
+
- [ ] **Step 4:** Capture screenshot evidence under `.devdata/artifacts/`
|
package/dist/docs/versioned/next/superpowers/specs/2026-07-28-admin-mobile-navigation-design.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Admin Mobile Navigation Design
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Make the admin shell usable on narrow devices by replacing its fixed 280px sidebar with a hamburger-triggered navigation drawer below 768px.
|
|
6
|
+
|
|
7
|
+
## Responsive behavior
|
|
8
|
+
|
|
9
|
+
- At widths of 768px and above, preserve the existing sidebar and content layout.
|
|
10
|
+
- Below 768px, hide the fixed sidebar and show a compact header with an accessible hamburger button and the Admin label.
|
|
11
|
+
- Opening the hamburger displays the same grouped navigation in a left slide-over drawer above the page.
|
|
12
|
+
- A backdrop covers the remaining page and dismisses the drawer when pressed.
|
|
13
|
+
- The drawer also closes from its close button and after any navigation item is selected.
|
|
14
|
+
|
|
15
|
+
## Component structure
|
|
16
|
+
|
|
17
|
+
Extract the sidebar body into a shared internal component so desktop and mobile render the same links, groups, configuration entry, and footer. `AdminShell` owns responsive width detection and drawer open state. The existing main content, top bar, breadcrumbs, and header actions remain unchanged apart from the added mobile header.
|
|
18
|
+
|
|
19
|
+
## Accessibility
|
|
20
|
+
|
|
21
|
+
- The hamburger button has an "Open navigation menu" label.
|
|
22
|
+
- The drawer close button has a "Close navigation menu" label.
|
|
23
|
+
- Backdrop dismissal is available in addition to the explicit close button.
|
|
24
|
+
- Existing navigation labels and hints remain intact.
|
|
25
|
+
|
|
26
|
+
## Testing
|
|
27
|
+
|
|
28
|
+
Add focused `AdminShell` tests covering:
|
|
29
|
+
|
|
30
|
+
- Desktop sidebar rendering at 768px and above.
|
|
31
|
+
- Mobile header rendering below 768px.
|
|
32
|
+
- Opening and closing the drawer.
|
|
33
|
+
- Closing after selecting a navigation item.
|
|
34
|
+
- Hamburger and close-button accessibility labels.
|
|
35
|
+
|
|
36
|
+
Run the admin frontend test suite, compile, lint, and responsive UI verification.
|
|
@@ -14,25 +14,25 @@ See: [`docs/implementationPlans/agentic-sdlc-plugin.md`](../implementationPlans/
|
|
|
14
14
|
|
|
15
15
|
## Phase 1: Decide and sanitize
|
|
16
16
|
|
|
17
|
-
- [
|
|
17
|
+
- [ ] **Task 1.1**: Audit the plugin for repo- and org-specific content
|
|
18
18
|
- Description: Grep all five skills for Flourish-specific and monorepo-specific content: `rg -n -i "phi|hipaa|flourish|dcb3|example-frontend|admin-spa|bun run|docs/implementationPlans"` across `plugins/`. Produce a table: file, line, category (org-specific / repo-path / hardcoded value / tool assumption), and the proposed replacement. Include the `.cursor-plugin/*.json` files (owner email, marketplace description). Do not change anything yet.
|
|
19
19
|
- Files: none (findings in the PR body)
|
|
20
20
|
- Depends on: none
|
|
21
21
|
- Acceptance: every hit is categorized with a proposed replacement; the table covers all five skills plus both JSON files.
|
|
22
22
|
|
|
23
|
-
- [
|
|
23
|
+
- [ ] **Task 1.2**: Generalize sensitive-data handling
|
|
24
24
|
- Description: Per IP question AP2, replace PHI/HIPAA-specific language with generalized sensitive-data language across `terreno-1-blend`, `terreno-4-pour`, and `terreno-5-dialin`. The rule must survive the rewrite: PR text, review replies, and attached evidence media must not contain sensitive data (PHI, PII, credentials, customer data). Keep it concrete — a vague "be careful with data" is not a usable instruction for an agent.
|
|
25
25
|
- Files: `plugins/terreno-planning/skills/terreno-1-blend/SKILL.md`, `terreno-4-pour/SKILL.md`, `terreno-5-dialin/SKILL.md`
|
|
26
26
|
- Depends on: Task 1.1
|
|
27
27
|
- Acceptance: `rg -n -i "phi|hipaa" plugins/` returns nothing; each rewritten rule names the specific artifacts it applies to (PR body, comments, screenshots, recordings) and the data categories.
|
|
28
28
|
|
|
29
|
-
- [
|
|
29
|
+
- [ ] **Task 1.3**: Fix the hardcoded branch suffix
|
|
30
30
|
- Description: `terreno-4-pour` documents the branch convention as `cursor/<descriptive-name>-dcb3`. The suffix is per-agent-run, not a fixed literal, so this will produce wrong branch names. Replace it with a description of the convention (a `cursor/` prefix plus the run-specific suffix the agent was given) and instruct the agent to use the suffix from its own run instructions rather than any literal in the skill.
|
|
31
31
|
- Files: `plugins/terreno-planning/skills/terreno-4-pour/SKILL.md`
|
|
32
32
|
- Depends on: Task 1.1
|
|
33
33
|
- Acceptance: no literal run suffix appears anywhere in `plugins/`; the convention is described such that an agent in any run produces a correctly-suffixed branch.
|
|
34
34
|
|
|
35
|
-
- [
|
|
35
|
+
- [ ] **Task 1.4**: Clean up plugin metadata
|
|
36
36
|
- Description: Update `.cursor-plugin/marketplace.json` and `plugins/terreno-planning/.cursor-plugin/plugin.json`: adjust the marketplace description per the AP1 decision (it currently says "Team marketplace"), review whether a personal email is the right contact for a public plugin versus a role address, and add a field or README note declaring which Terreno versions the plugin targets (IP question AP7). Add `plugins/terreno-planning/LICENSE` matching the root license.
|
|
37
37
|
- Files: `.cursor-plugin/marketplace.json`, `plugins/terreno-planning/.cursor-plugin/plugin.json`, `plugins/terreno-planning/LICENSE` (new)
|
|
38
38
|
- Depends on: Task 1.1
|
|
@@ -99,7 +99,7 @@ See: [`docs/implementationPlans/agentic-sdlc-plugin.md`](../implementationPlans/
|
|
|
99
99
|
- Acceptance: under 150 lines; every coffee name is glossed; the installation steps were performed from a fresh state; the quickstart matches a run you completed.
|
|
100
100
|
|
|
101
101
|
- [ ] **Task 4.2**: Write `docs/explanation/agentic-sdlc.md`
|
|
102
|
-
- Description: The conceptual case, no commands. Cover: the process layer versus the MCP tool layer, and why both are needed; why planning is a separate stage with a question-first gate (agents confidently plan the wrong thing); why review happens in a **fresh context** rather than by asking the implementing agent to check its own work; why verification is a separate stage from implementation; why the frontend gate demands evidence rather than a claim; and why flaky CI must be classified rather than guessed at. Then an honest limits section: what the pipeline does not do, where a human must decide, and
|
|
102
|
+
- Description: The conceptual case, no commands. Cover: the process layer versus the MCP tool layer, and why both are needed; why planning is a separate stage with a question-first gate (agents confidently plan the wrong thing); why review happens in a **fresh context** rather than by asking the implementing agent to check its own work; why verification is a separate stage from implementation; why the frontend gate demands evidence rather than a claim; and why flaky CI must be classified rather than guessed at. Then an honest limits section: what the pipeline does not do, where a human must decide, and the 15-minute review timeout. Reuse the mermaid diagram from the IP.
|
|
103
103
|
- Files: `docs/explanation/agentic-sdlc.md` (new), `docs/explanation/README.md`
|
|
104
104
|
- Depends on: Task 4.1
|
|
105
105
|
- Acceptance: no commands; all six design rationales explained; a limits section naming at least three things the pipeline will not do; linked from the explanation index.
|