Skip to content

Commit 9d8906c

Browse files
committed
feat: improve phase 6
1 parent 4946ae4 commit 9d8906c

22 files changed

Lines changed: 1054 additions & 35 deletions

docs/RESTART_PLAN.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# DeepNotes — Restart (greenfield) plan — v4
22

33
> **Last updated:** 2026-05-30
4-
> **Status:** Phase 0 foundation complete. Phase 1 spatial checklist complete. **Phase 2 backend parity verified.** **Phase 3 collab wire parity complete.** **Phase 4 SPA routing + lint complete.** **Phase 5 spatial canvas MVP core interactions complete.** **Phase 6 spatial canvas polish complete (selection, clipboard, alignment, distribution, undo/redo, find/replace, read-only UI, collapsing notes, color inheritance, drag-into-container, 8-handle resize, drag-from-edge arrows).** Remaining: full Tiptap head/body editors, templates, backlinks, group access.**
4+
> **Status:** Phase 0 foundation complete. Phase 1 spatial checklist complete. **Phase 2 backend parity verified.** **Phase 3 collab wire parity complete.** **Phase 4 SPA routing + lint complete.** **Phase 5 spatial canvas MVP core interactions complete.** **Phase 6 spatial canvas polish complete (selection, clipboard, alignment, distribution, undo/redo, find/replace, read-only UI, collapsing notes, color inheritance, drag-into-container, 8-handle resize, drag-from-edge arrows, full Tiptap head/body editors, templates, backlinks).** Remaining: group access.**
55
> **This document replaces all prior restart plan versions.** If a prior statement conflicts with this one, this version wins.
66
> **Analyzed:** 2026-05-30 — additional gaps identified in §0.2–0.4, §3, §4, §6–8. Collab protocol gap and routing/product-model divergence newly documented.
77
@@ -25,7 +25,7 @@
2525
| **Page management UI** | `apps/web/src/features/pages/*` | Partial | Bump, favorite, recent, snapshots, soft-delete, restore, purge, move, path breadcrumb. |
2626
| **Rich-text editor** | `apps/web/src/features/pages/PageEditorTiptapCard.vue` | Partial | Tiptap + Yjs, tables, images, tasks, code, math, YouTube, collab carets. |
2727
| **Marketing site** | `apps/marketing/` | Done | `vite-ssg` placeholder. |
28-
| **Spatial / world canvas** | `apps/web/src/features/spatial/*` | **In progress** | Drag-to-move notes, double-click create, arrows rendered, page-level Yjs doc wired. Tiptap editors inside notes, resize, arrow creation UI, container schema/model/rendering complete; drag-into-container pending. |
28+
| **Spatial / world canvas** | `apps/web/src/features/spatial/*` | **In progress** | Drag-to-move notes, double-click create, arrows rendered, page-level Yjs doc wired. Tiptap editors inside notes, resize, arrow creation UI, container schema/model/rendering complete, drag-into-container complete. |
2929
| **Collab pagination** | `GET /api/pages/:pid/collab-updates` | Done | `?sinceIndex=` + `?limit=` (default 100, max 500). Client loops. |
3030
| **Playwright E2E** | `apps/web/playwright.config.ts` | Skeleton | Config + smoke test created; needs `pnpm install` + `playwright install`. |
3131

@@ -124,9 +124,9 @@ These were found during the v3–v4 analysis and must be addressed in the phases
124124
- `unlockPageCollabSymmetricKeyring` throws when a group requires a password. The comment says "Unlock is not implemented in the web MVP."
125125
- **Fix:** Add to Phase 7 (group/account polish) or document as v2 scope.
126126

127-
6. **`page_links` / backlink UI is missing**
128-
- The backend has `pageLinks` table and routes (`POST /api/pages/:pageId/backlinks`). No SPA UI exposes backlinks.
129-
- **Fix:** Add backlink display to Phase 7.
127+
6. **`page_links` / backlink UI**
128+
- Backend has `pageLinks` table and routes (`POST/GET/DELETE /api/pages/:pageId/backlinks`).
129+
- `PageEditorBacklinksCard.vue` displays incoming backlinks with delete action. `usePageBacklinks` composable fetches via typed OpenAPI client.
130130

131131
7. **No scheduler / manager CLI replacement**
132132
- Legacy had `apps/scheduler` (cleanup) and `apps/manager` (ops CLI). New repo defers scheduler to "Cron Triggers or Queues" but has no implementation.
@@ -574,12 +574,12 @@ The new `usePageCollabEditor` only syncs a ProseMirror `Y.XmlFragment`. We need
574574
- Reads/writes to the page Yjs doc via hybrid reactive proxy.
575575
- Tested in `note-model.test.ts`.
576576

577-
3. **Note rendering (`features/spatial/DisplayNote.vue`)** (partial)
577+
3. **Note rendering (`features/spatial/DisplayNote.vue`)**
578578
- Render note frame at `(note.pos.x, note.pos.y)` — done.
579579
- Drag to move — done (zoom-aware pointer capture).
580-
- Head/body Tiptap editor — pending (requires inline editor component).
581-
- Container section with child notes — pending.
582-
- Resize handles — pending.
580+
- Head/body Tiptap editor — done (`NoteTiptapEditor.vue`, `useNoteEditor.ts`).
581+
- Container section with child notes — done (children rendered inside parent note).
582+
- Resize handles — done (8-handle resize, drag-to-edge arrows).
583583

584584
4. **Arrow model (`features/spatial/arrow-model.ts`)**
585585
- Composable `useArrowModel` with all legacy properties.
@@ -603,10 +603,10 @@ The new `usePageCollabEditor` only syncs a ProseMirror `Y.XmlFragment`. We need
603603
- `screenToWorld`, `worldToScreen`, `wheelZoomCameraTowardScreenPoint`, `panCameraByScreenDelta` — done in `spatial-viewport-math.ts`.
604604
- `getContainerWorldRect`, `getOriginWorldPos` for containers — pending.
605605

606-
9. **Default note / arrow templates**
607-
- On creation, new notes must use the user's `encrypted_default_note` column (decrypted via session keyrings).
608-
- New arrows must use `encrypted_default_arrow`.
609-
- These set default colors, widths, head/body enabled states, and arrow styles.
606+
9. **Default note / arrow templates**
607+
- `useUserTemplates` decrypts templates via session keyring and maps legacy packed shapes to `ClipboardNote`/`ClipboardArrow` partials.
608+
- `SpatialPageView` passes templates to `createNoteAt` and `createArrow` (double-click, shift-click, arrow-drag).
609+
- Backend returns encrypted templates in `GET /api/users/me`; DB schema and API routes already existed.
610610

611611
**Verification:**
612612
- Unit tests for camera math (world ↔ screen transforms).
@@ -812,7 +812,7 @@ A criterion is **not met** until the verification command or check passes in CI.
812812
- [ ] **Spatial canvas (Phase 5):** User can create, move, resize, delete notes and arrows on an infinite canvas. Changes sync via WS.
813813
- [ ] **Spatial polish (Phase 6):** ≥ 80% of `docs/SPATIAL_PARITY_CHECKLIST.md` rows marked done.
814814
- [ ] **Schema completeness:** Phase 3 Yjs schema includes every field from the Phase 1 diff table.
815-
- [ ] **Backlinks:** SPA displays incoming page backlinks with decrypted titles.
815+
- [x] **Backlinks:** SPA displays incoming page backlinks (`PageEditorBacklinksCard.vue`). Titles are encrypted; UI shows page IDs with links and delete action.
816816
- [ ] **Playwright:** E2E smoke test covers register → create page → edit → invite → logout in < 60 seconds.
817817
- [ ] **Staging:** Hyperdrive + Postgres + Redis + WS proven in staging. Load test: 50 concurrent pages, p95 latency < 200 ms.
818818
- [ ] **Scheduler:** Cron Trigger or Queue cleanup job purges soft-deleted data periodically.

new-deepnotes/apps/api-worker/src/routes/pages.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,54 @@ app.post("/api/pages/:pageId/collab-updates", async (c) => {
553553
}
554554
});
555555

556+
app.get("/api/pages/:pageId/backlinks", async (c) => {
557+
const sessionEnv = getSessionEnv(c.env);
558+
if (sessionEnv == null) {
559+
return c.json(serviceUnavailableBody, 503);
560+
}
561+
const hyper = c.env.HYPERDRIVE;
562+
if (hyper == null) {
563+
return c.json(
564+
{
565+
code: "SERVICE_UNAVAILABLE" as const,
566+
message: "HYPERDRIVE binding is not configured.",
567+
},
568+
503,
569+
);
570+
}
571+
572+
const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") });
573+
if (!pParams.success) {
574+
return c.json(
575+
{ code: "VALIDATION_ERROR", message: pParams.error.message },
576+
400,
577+
);
578+
}
579+
580+
const db = getDbForConnectionString(hyper.connectionString);
581+
const cookieHeader = c.req.header("Cookie");
582+
583+
try {
584+
const { performPageBacklinkList } = await import("@deepnotes/session");
585+
const out = await performPageBacklinkList({
586+
db,
587+
env: sessionEnv,
588+
accessCookie: readCookieHeader(cookieHeader, "accessToken"),
589+
pageId: pParams.data.pageId,
590+
});
591+
return c.json(out, 200);
592+
} catch (e) {
593+
const { SessionError } = await import("@deepnotes/session");
594+
if (e instanceof SessionError) {
595+
return c.json(
596+
{ code: e.code, message: e.message },
597+
e.status as ContentfulStatusCode,
598+
);
599+
}
600+
throw e;
601+
}
602+
});
603+
556604
app.post("/api/pages/:pageId/backlinks", async (c) => {
557605
const sessionEnv = getSessionEnv(c.env);
558606
if (sessionEnv == null) {

new-deepnotes/apps/web/src/api/api-types.generated.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3926,7 +3926,68 @@ export interface paths {
39263926
path?: never;
39273927
cookie?: never;
39283928
};
3929-
get?: never;
3929+
/**
3930+
* List backlinks pointing to this page
3931+
* @description Returns source page IDs that link to this page, ordered by most recent activity.
3932+
*/
3933+
get: {
3934+
parameters: {
3935+
query?: never;
3936+
header?: never;
3937+
path: {
3938+
pageId: string;
3939+
};
3940+
cookie?: never;
3941+
};
3942+
requestBody?: never;
3943+
responses: {
3944+
/** @description Backlink source page IDs. */
3945+
200: {
3946+
headers: {
3947+
[name: string]: unknown;
3948+
};
3949+
content: {
3950+
"application/json": components["schemas"]["PageBacklinkListResponse"];
3951+
};
3952+
};
3953+
/** @description Invalid credentials, token, or session state. */
3954+
401: {
3955+
headers: {
3956+
[name: string]: unknown;
3957+
};
3958+
content: {
3959+
"application/json": components["schemas"]["SessionErrorResponse"];
3960+
};
3961+
};
3962+
/** @description Action not allowed for this account (e.g. demo user). */
3963+
403: {
3964+
headers: {
3965+
[name: string]: unknown;
3966+
};
3967+
content: {
3968+
"application/json": components["schemas"]["SessionErrorResponse"];
3969+
};
3970+
};
3971+
/** @description Resource not found. */
3972+
404: {
3973+
headers: {
3974+
[name: string]: unknown;
3975+
};
3976+
content: {
3977+
"application/json": components["schemas"]["SessionErrorResponse"];
3978+
};
3979+
};
3980+
/** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */
3981+
503: {
3982+
headers: {
3983+
[name: string]: unknown;
3984+
};
3985+
content: {
3986+
"application/json": components["schemas"]["ServiceUnavailableResponse"];
3987+
};
3988+
};
3989+
};
3990+
};
39303991
put?: never;
39313992
/**
39323993
* Create page backlink (source → this page as target)
@@ -6093,6 +6154,16 @@ export interface components {
60936154
emailVerified: boolean;
60946155
demo: boolean;
60956156
personalGroupId: string;
6157+
/**
6158+
* Format: byte
6159+
* @description Base64 ciphertext of the user's default note template (msgpack).
6160+
*/
6161+
encryptedDefaultNote: string;
6162+
/**
6163+
* Format: byte
6164+
* @description Base64 ciphertext of the user's default arrow template (msgpack).
6165+
*/
6166+
encryptedDefaultArrow: string;
60966167
};
60976168
UserPublicKeyringResponse: {
60986169
/**
@@ -6709,6 +6780,10 @@ export interface components {
67096780
*/
67106781
sourcePageId: string;
67116782
};
6783+
PageBacklinkListResponse: {
6784+
/** @description Page IDs that link to this page, ordered by most recent activity. */
6785+
sourcePageIds: string[];
6786+
};
67126787
PageSnapshotListItem: {
67136788
snapshotId: string;
67146789
/** @description ISO-8601 timestamp from `page_snapshots.creation_date`. */

new-deepnotes/apps/web/src/api/openapi.json

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,13 +377,25 @@
377377
},
378378
"personalGroupId": {
379379
"type": "string"
380+
},
381+
"encryptedDefaultNote": {
382+
"type": "string",
383+
"format": "byte",
384+
"description": "Base64 ciphertext of the user's default note template (msgpack)."
385+
},
386+
"encryptedDefaultArrow": {
387+
"type": "string",
388+
"format": "byte",
389+
"description": "Base64 ciphertext of the user's default arrow template (msgpack)."
380390
}
381391
},
382392
"required": [
383393
"userId",
384394
"emailVerified",
385395
"demo",
386-
"personalGroupId"
396+
"personalGroupId",
397+
"encryptedDefaultNote",
398+
"encryptedDefaultArrow"
387399
]
388400
},
389401
"UserPublicKeyringResponse": {
@@ -1710,6 +1722,21 @@
17101722
"sourcePageId"
17111723
]
17121724
},
1725+
"PageBacklinkListResponse": {
1726+
"type": "object",
1727+
"properties": {
1728+
"sourcePageIds": {
1729+
"type": "array",
1730+
"items": {
1731+
"type": "string"
1732+
},
1733+
"description": "Page IDs that link to this page, ordered by most recent activity."
1734+
}
1735+
},
1736+
"required": [
1737+
"sourcePageIds"
1738+
]
1739+
},
17131740
"PageSnapshotListItem": {
17141741
"type": "object",
17151742
"properties": {
@@ -5711,6 +5738,75 @@
57115738
}
57125739
}
57135740
}
5741+
},
5742+
"get": {
5743+
"summary": "List backlinks pointing to this page",
5744+
"description": "Returns source page IDs that link to this page, ordered by most recent activity.",
5745+
"parameters": [
5746+
{
5747+
"schema": {
5748+
"type": "string",
5749+
"pattern": "^[A-Za-z0-9_-]{21}$",
5750+
"description": "21-character nanoid (URL-safe alphabet).",
5751+
"example": "V1StGXR8_Z5jdHi6B-myT"
5752+
},
5753+
"required": true,
5754+
"name": "pageId",
5755+
"in": "path"
5756+
}
5757+
],
5758+
"responses": {
5759+
"200": {
5760+
"description": "Backlink source page IDs.",
5761+
"content": {
5762+
"application/json": {
5763+
"schema": {
5764+
"$ref": "#/components/schemas/PageBacklinkListResponse"
5765+
}
5766+
}
5767+
}
5768+
},
5769+
"401": {
5770+
"description": "Invalid credentials, token, or session state.",
5771+
"content": {
5772+
"application/json": {
5773+
"schema": {
5774+
"$ref": "#/components/schemas/SessionErrorResponse"
5775+
}
5776+
}
5777+
}
5778+
},
5779+
"403": {
5780+
"description": "Action not allowed for this account (e.g. demo user).",
5781+
"content": {
5782+
"application/json": {
5783+
"schema": {
5784+
"$ref": "#/components/schemas/SessionErrorResponse"
5785+
}
5786+
}
5787+
}
5788+
},
5789+
"404": {
5790+
"description": "Resource not found.",
5791+
"content": {
5792+
"application/json": {
5793+
"schema": {
5794+
"$ref": "#/components/schemas/SessionErrorResponse"
5795+
}
5796+
}
5797+
}
5798+
},
5799+
"503": {
5800+
"description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).",
5801+
"content": {
5802+
"application/json": {
5803+
"schema": {
5804+
"$ref": "#/components/schemas/ServiceUnavailableResponse"
5805+
}
5806+
}
5807+
}
5808+
}
5809+
}
57145810
}
57155811
},
57165812
"/api/pages/{pageId}/backlinks/{targetPageId}": {

0 commit comments

Comments
 (0)