Admin: write blog posts and manage images

A Posts page lists every post with Write a post: title, topic, kind,
date, summary, a cover picked from the vault or uploaded, the pages it is
about, the Markdown body with a rendered preview, and Save draft or
Publish. An Images page uploads several images into an _attachments
folder, shows which notes use each one, copies its embed line, and
deletes unused ones. The content service gains the attachment list,
upload and delete endpoints behind the editor and admin tokens.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-12 03:40:59 +00:00
parent 151bed230a
commit a9fb5b3e85
12 changed files with 694 additions and 12 deletions
+34
View File
@@ -77,6 +77,40 @@ describe("sync reports", () => {
});
});
describe("attachments", () => {
const png = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.alloc(32)]);
const upload = (name: string, bytes: Buffer, token = T.admin) =>
fetch(`${base}/api/attachments?folder=Posts&name=${encodeURIComponent(name)}`, {
method: "POST",
headers: { authorization: `Bearer ${token}`, "content-type": "application/octet-stream" },
body: new Uint8Array(bytes),
});
it("adds an image under a safe, unused name", async () => {
const a = (await (await upload("My Photo!.PNG", png)).json()) as { name: string };
const b = (await (await upload("my photo.png", png)).json()) as { name: string };
expect(a.name).toBe("my-photo.png");
expect(b.name).toBe("my-photo-2.png");
expect((await get("/attachments/my-photo-2.png", T.reader)).status).toBe(200);
});
it("turns away other files and other roles", async () => {
expect((await upload("x.svg", Buffer.from("<svg/>"))).status).toBe(400);
expect((await upload("x.png", Buffer.from("not a png"))).status).toBe(400);
expect((await upload("x.png", png, T.agent)).status).toBe(403);
expect((await get("/api/attachments", T.reader)).status).toBe(403);
});
it("lists images with the notes that use them, and deletes only unused ones", async () => {
await post_("/api/changes", T.admin, { op: "create", data: post("With a photo"), body: "![[my-photo.png]]" });
const list = (await (await get("/api/attachments", T.admin)).json()) as { name: string; folder: string; usedBy: { title: string }[] }[];
expect(list.find((a) => a.name === "my-photo.png")).toMatchObject({ folder: "Posts", usedBy: [{ title: "With a photo" }] });
expect((await post_("/api/attachments/delete", T.admin, { name: "my-photo.png" })).status).toBe(409);
expect((await post_("/api/attachments/delete", T.admin, { name: "my-photo-2.png" })).status).toBe(200);
expect((await get("/attachments/my-photo-2.png", T.reader)).status).toBe(404);
});
});
describe("MCP server", () => {
it("lets an agent search, post a status and draft a post, but not publish it", async () => {
const client = new Client({ name: "test", version: "1.0.0" });