Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f88b13c
feat(ws): ✨ add rooms to the WebSocket registry and the content live …
claude Oct 8, 2026
6ca303e
feat(content): ✨ add live editing broadcast and lifecycle seams
claude Oct 8, 2026
20f993f
feat(content): ✨ add live editing socket: presence and collaborative …
claude Oct 8, 2026
ee238fb
feat(content): ✨ add field locks, shared drafts and autosave
claude Oct 8, 2026
380110d
feat(content): ✨ clean up stale field locks, drafts and documents daily
claude Oct 8, 2026
a2a7aaa
test(content): ✅ cover field locks and draft merges against Postgres
claude Oct 8, 2026
4c9de65
feat(content): ✨ add richText field with SSR renderer
claude Oct 8, 2026
51f8877
feat(db): 🗃️ add live editing tables and store blog content as rich t…
claude Oct 8, 2026
6d80f17
docs(content): 📝 document live editing and autosave
claude Oct 8, 2026
34f53c5
feat(content): ✨ add collaborative rich text editor and presence UI
claude Oct 8, 2026
50031c2
feat(blog): ✨ co-edit articles with live presence
claude Oct 8, 2026
6fc4712
fix(content): 🐛 stop the collaborative editor from reopening its docu…
claude Oct 8, 2026
4fcce26
docs(content): 📝 add live editing screenshots, presence UI and blog g…
claude Oct 8, 2026
e09e916
docs(plan): 📝 mark live editing implemented and list known limitations
claude Oct 8, 2026
ebd9978
docs(content): 🎨 format the live editing and rich text pages
claude Oct 8, 2026
bbb701d
chore: 🔧 refresh lockfile after rebasing on canary
aXenDeveloper Oct 10, 2026
db87323
test(content): 🔥 drop Postgres integration tests whose helper canary …
aXenDeveloper Oct 10, 2026
6042b31
fix(content): 🐛 adapt rich text and live editing to canary's AI actio…
aXenDeveloper Oct 10, 2026
39bde6e
fix(db): 🗃️ regenerate the live editing migration on top of canary
aXenDeveloper Oct 10, 2026
ecbb214
fix(ai): 🐛 read rich text documents in translation freshness
aXenDeveloper Oct 10, 2026
cb90058
feat(live-editing): ✨ enhance presence UI with outlines and animation…
aXenDeveloper Oct 10, 2026
dde84ba
feat(docs): ✏️ add rich text and services documentation
aXenDeveloper Oct 10, 2026
b16034c
feat(file-display): ✨ add AutoFormFileDisplay context and tests for f…
aXenDeveloper Oct 10, 2026
f1f71de
refactor(presence): ✨ streamline presence model and related components
aXenDeveloper Oct 10, 2026
c84bbaa
fix(live-store): 🐛 update `setWhere` in `acquireContentFieldLock` for…
aXenDeveloper Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 5 additions & 1 deletion .claude/skills/vitnode-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Keep the final verification in “Check the result” instead of repeating verif
#### Code and reference schema

- Give every code block a language and, for file edits, a supported file title.
- Keep title short.
- Show a complete minimal file when creating one; show a labeled excerpt when changing an existing file. State where an excerpt belongs and link to required setup.
- Exclude unrelated styling, route metadata, schemas, and error handling from focused excerpts. Preserve code required to reproduce the task safely and correctly.
- Explain a new concept once, next to its first use. Do not narrate obvious code line by line.
Expand Down Expand Up @@ -191,7 +192,10 @@ Capture screenshots when documenting AdminCP, forms, settings, editors, or other
6. Capture the relevant panel or region with enough navigation context to orient the reader. Add a second screenshot only if a distinct state needs explanation.
7. Inspect each saved image. Retake it if labels are unreadable, content is clipped, or the page shows loading/error states unrelated to the guide.
8. Avoid exposing personal data, tokens, cookies, private URLs, or real user records. Prefer preparing clean demo data before capture.
9. Save assets in the repository's existing documentation image location. Use descriptive kebab-case names and stable repository paths. Use PNG or WebP according to existing support; keep text sharp and file sizes reasonable.
9. Save assets in the repository's existing documentation image location. Use descriptive kebab-case names and stable repository paths. Save every new or re-captured screenshot as WebP; keep text sharp and file sizes reasonable.
- Capture at a 2x device scale factor. If the tool only writes PNG, capture to PNG, then convert it to WebP and delete the intermediate PNG. Never commit PNG screenshots.
- Convert with high quality to keep UI text sharp, for example Pillow lossy `quality=90` with `method=6`. Use lossless WebP when small text blurs. Inspect the WebP after conversion.
- When replacing an existing PNG screenshot, change the MDX import from `./name.png` to `./name.webp` and delete the old PNG. Leave no reference to the removed file.
10. Place the image beside the step it illustrates. Add useful alt text and a short caption when the state needs explanation. Keep the action and result in text as well.

Never generate, draw, or fabricate an application screenshot. Do not substitute a mockup or claim a screenshot was captured when it was not. If the app, credentials, or capture tools are unavailable, finish the text and report the missing capture and its precise prerequisite. Do not commit broken image references or placeholders as completed screenshots.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ npm i x

# Testing

- Write and run vitest unit tests for a new feature - a capability the codebase did not have - and for every bug fix. Skip only if vitest isn't configured.
- Never write unit tests in the same session. Spawn a new session, and write the test after the feature is working. Tests are for regression, not for development.
- UI component tests (`*.test.tsx`) should test DOM output, click events, and dialogs. Never test CSS classes or form UI (`AutoForm`, form fields).
- Do not test an existing feature applied somewhere new. Building the widgets system earns tests; adding widget zones or widgets to another page or plugin does not, because the mechanism is already covered and only the wiring is new.
- Do not test database models or schema definitions (Drizzle tables, column types, indexes, foreign keys).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ feature. That boundary pays rent surprisingly quickly.
- [Build your first plugin](https://vitnode.com/docs/guides/first-plugin)
- [Plugin routes](https://vitnode.com/docs/dev/routing)
- [Admin Control Panel](https://vitnode.com/docs/dev/plugins/admin)
- [Content delivery and SEO](https://vitnode.com/docs/dev/content-engine/content-delivery-and-seo)
- [Content delivery and SEO](https://vitnode.com/docs/content-engine/public-pages)
- [Write documentation](https://vitnode.com/docs/dev/documentation)

## Project scope
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
CREATE TABLE "core_content_documents" (
"id" serial PRIMARY KEY,
"contentTypeId" varchar(100) NOT NULL,
"itemId" integer NOT NULL,
"field" varchar(100) NOT NULL,
"language" varchar(35) DEFAULT '' NOT NULL,
"state" bytea NOT NULL,
"baseVersion" integer,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"updatedBy" integer
);
--> statement-breakpoint
ALTER TABLE "core_content_documents" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "core_content_drafts" (
"id" serial PRIMARY KEY,
"contentTypeId" varchar(100) NOT NULL,
"itemId" integer NOT NULL,
"language" varchar(32) DEFAULT '' NOT NULL,
"values" jsonb DEFAULT '{}' NOT NULL,
"baseVersion" integer DEFAULT 0 NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"updatedBy" integer
);
--> statement-breakpoint
ALTER TABLE "core_content_drafts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE TABLE "core_content_field_locks" (
"id" serial PRIMARY KEY,
"contentTypeId" varchar(100) NOT NULL,
"itemId" integer NOT NULL,
"field" varchar(100) NOT NULL,
"language" varchar(32) DEFAULT '' NOT NULL,
"userId" integer NOT NULL,
"acquiredAt" timestamp DEFAULT now() NOT NULL,
"expiresAt" timestamp NOT NULL
);
--> statement-breakpoint
ALTER TABLE "core_content_field_locks" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "blog_posts_translations" ALTER COLUMN "content" SET DATA TYPE jsonb USING (
CASE
WHEN "content" IS NULL THEN NULL
WHEN btrim(regexp_replace("content", '<[^>]*>', ' ', 'g')) = '' THEN '{"type":"doc","content":[]}'::jsonb
ELSE jsonb_build_object(
'type', 'doc',
'content', jsonb_build_array(jsonb_build_object(
'type', 'paragraph',
'content', jsonb_build_array(jsonb_build_object(
'type', 'text',
'text', btrim(regexp_replace(regexp_replace("content", '<[^>]*>', ' ', 'g'), '\s+', ' ', 'g'))
))
Comment on lines +45 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve formatting during the rich-text migration

For every existing blog article containing editor-generated HTML, this conversion removes all tags and stores the remaining words in one paragraph. Headings, lists, links, tables, media, and inline formatting are therefore irreversibly lost as soon as the migration runs; convert the HTML structure to equivalent ProseMirror JSON instead of stripping it.

Useful? React with 👍 / 👎.

))
)
END
);--> statement-breakpoint
CREATE UNIQUE INDEX "core_content_documents_doc_unique" ON "core_content_documents" ("contentTypeId","itemId","field","language");--> statement-breakpoint
CREATE INDEX "core_content_documents_updated_at_idx" ON "core_content_documents" ("updatedAt");--> statement-breakpoint
CREATE INDEX "core_content_documents_updated_by_idx" ON "core_content_documents" ("updatedBy");--> statement-breakpoint
CREATE UNIQUE INDEX "core_content_drafts_key_unique" ON "core_content_drafts" ("contentTypeId","itemId","language");--> statement-breakpoint
CREATE INDEX "core_content_drafts_updated_at_idx" ON "core_content_drafts" ("updatedAt");--> statement-breakpoint
CREATE INDEX "core_content_drafts_updated_by_idx" ON "core_content_drafts" ("updatedBy");--> statement-breakpoint
CREATE UNIQUE INDEX "core_content_field_locks_key_unique" ON "core_content_field_locks" ("contentTypeId","itemId","field","language");--> statement-breakpoint
CREATE INDEX "core_content_field_locks_expires_at_idx" ON "core_content_field_locks" ("expiresAt");--> statement-breakpoint
CREATE INDEX "core_content_field_locks_user_id_idx" ON "core_content_field_locks" ("userId");--> statement-breakpoint
ALTER TABLE "core_content_documents" ADD CONSTRAINT "core_content_documents_updatedBy_core_users_id_fkey" FOREIGN KEY ("updatedBy") REFERENCES "core_users"("id") ON DELETE SET NULL ON UPDATE CASCADE;--> statement-breakpoint
ALTER TABLE "core_content_drafts" ADD CONSTRAINT "core_content_drafts_updatedBy_core_users_id_fkey" FOREIGN KEY ("updatedBy") REFERENCES "core_users"("id") ON DELETE SET NULL ON UPDATE CASCADE;--> statement-breakpoint
ALTER TABLE "core_content_field_locks" ADD CONSTRAINT "core_content_field_locks_userId_core_users_id_fkey" FOREIGN KEY ("userId") REFERENCES "core_users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Loading
Loading