diff --git a/scripts/screenshots/seed.ts b/scripts/screenshots/seed.ts new file mode 100644 index 0000000..ea172bd --- /dev/null +++ b/scripts/screenshots/seed.ts @@ -0,0 +1,287 @@ +import type { Page } from 'playwright'; +import { generateBretzelPng, generateSaltShakerPng } from './bretzel-pixel'; +import { BASE_URL, ADMIN, COLLABORATOR } from './constants'; + +type HttpMethod = 'post' | 'patch' | 'get' | 'delete'; + +async function api(page: Page, method: HttpMethod, path: string, data?: unknown) { + const res = await page.request[method]( + `${BASE_URL}${path}`, + data !== undefined ? { data } : undefined + ); + if (!res.ok()) { + throw new Error(`${method.toUpperCase()} ${path} failed: ${res.status()} ${await res.text()}`); + } + const text = await res.text(); + return text ? JSON.parse(text) : {}; +} + +async function uploadAttachment( + page: Page, + noteId: string, + filename: string, + pngBuffer: Buffer, + featured: boolean +): Promise { + const res = await page.request.post(`${BASE_URL}/api/notes/${noteId}/attachments`, { + multipart: { + file: { + name: filename, + mimeType: 'image/png', + buffer: pngBuffer + } + } + }); + if (!res.ok()) { + throw new Error(`Upload ${filename} failed: ${res.status()} ${await res.text()}`); + } + if (featured) { + const attachment = await res.json(); + await api(page, 'patch', `/api/notes/${noteId}/attachments?attachmentId=${attachment.id}`, { + featured: true + }); + } +} + +interface NoteData { + key: string; + title: string; + content: string; + color: string; + pinned?: boolean; + checklistMode?: boolean; +} + +function getNotesData(): NoteData[] { + // Created in reverse display order so newest (first created) appears last, + // and the grid reads naturally with pinned "Bretzel Ingredients" at top-left + return [ + { + key: 'bakingcode', + title: 'Baking code for the...', + content: `A developer's checklist where code meets dough. + +- [x] Set up CI/CD pipeline +- [x] Write the CRDT sync logic +- [ ] Bake the docker image +- [ ] Deploy to the homelab +- [ ] Celebrate with a bretzel + +#dev #bretzel`, + color: 'clay', + checklistMode: true + }, + { + key: 'flute', + title: 'Ancient Flute Harmonica Discovered', + content: `Archaeologists found a 40,000-year-old flute carved from vulture bone. + +The oldest known musical instrument. +Imagine the first song ever played. + +#reading`, + color: 'chalk' + }, + { + key: 'weekend', + title: 'Weekend Plans', + content: `Saturday: +- Farmer's market (get sourdough starter) +- Fix the Dokploy SSL cert renewal +- Bake a batch of bretzels + +Sunday: +- Retro gaming marathon +- Update the self-hosting stack +- Plan next trip on 42 + +#personal #weekend`, + color: 'mint' + }, + { + key: 'manifesto', + title: 'The Best Software Is the Software You Own', + content: `Self-host everything. Trust no cloud. + +The best software is the software you own. +Your data. Your server. Your rules. + +If it can't run on your hardware, it's not really yours. + +#personal #bretzel`, + color: 'coral' + }, + { + key: 'retro', + title: 'Back to Retro Gaming', + content: `Games to replay this summer: + +Chrono Trigger (SNES) — best RPG ever made +The Legend of Zelda: Link's Awakening (GB) +Castlevania: Symphony of the Night (PS1) +Advance Wars (GBA) +Final Fantasy Tactics (PS1) + +The pixel art era was peak game design. + +#reading #personal`, + color: 'peach' + }, + { + key: 'selfhosting', + title: 'Self-Hosting Stack', + content: `Current homelab setup: + +**Crumbs** — notes (obviously) +**Vaultwarden** — passwords +**Nextcloud** — files & calendar + +Just getting started. More to come. + +#homelab #devops`, + color: 'fog' + }, + { + key: 'bretzel', + title: 'Bretzel Ingredients', + content: `The perfect Alsatian bretzel recipe: + +- [x] 500g bread flour +- [x] 300ml warm water +- [x] 10g salt +- [ ] 7g dry yeast +- [ ] 30g butter (softened) +- [ ] Baking soda for the bath +- [ ] Coarse salt for topping + +#baking #bretzel`, + color: 'sand', + pinned: true, + checklistMode: true + } + ]; +} + +function getVersionHistoryContents(): string[] { + return [ + // Version 2: Add Dokploy + Outline + `Current homelab setup: + +**Crumbs** — notes (obviously) +**Dokploy** — deployment platform +**Outline** — team wiki +**Vaultwarden** — passwords +**Nextcloud** — files & calendar + +Growing the stack. + +#homelab #devops`, + + // Version 3: Add Navidrome + Plausible + `Current homelab setup: + +**Crumbs** — notes (obviously) +**Dokploy** — deployment platform +**Outline** — team wiki +**Navidrome** — music streaming +**Vaultwarden** — passwords +**Nextcloud** — files & calendar +**Plausible** — analytics + +The stack is getting serious. + +#homelab #devops`, + + // Version 4: Add 42, final version + `Current homelab setup: + +**Crumbs** — notes (obviously) +**42** — holiday budget tracking +**Dokploy** — deployment platform +**Outline** — team wiki +**Navidrome** — music streaming +**Vaultwarden** — passwords +**Nextcloud** — files & calendar +**Plausible** — analytics + +Everything below daily reverse proxy. + +#homelab #devops` + ]; +} + +export async function seed(page: Page): Promise { + // 1. Setup admin (sets session cookie) + await api(page, 'post', '/api/auth/setup', { + email: ADMIN.email, + displayName: ADMIN.displayName, + password: ADMIN.password + }); + console.log(' admin created'); + + // 2. Create collaborator user + const alice = await api(page, 'post', '/api/admin/users', { + email: COLLABORATOR.email, + displayName: COLLABORATOR.displayName, + password: COLLABORATOR.password, + role: 'user' + }); + const aliceId: number = alice.id; + console.log(' collaborator created (id:', aliceId, ')'); + + // 3. Create notes (order matters — newest appear first in grid) + const noteData = getNotesData(); + const noteIds: Record = {}; + for (const note of noteData) { + const created = await api(page, 'post', '/api/notes', { + title: note.title, + content: note.content, + color: note.color, + pinned: note.pinned ?? false, + checklistMode: note.checklistMode ?? false + }); + noteIds[note.key] = created.id; + console.log(' note created:', note.title); + } + + // 4. Share "Weekend Plans" with Alice + await api(page, 'post', `/api/notes/${noteIds['weekend']}/collaborators`, { + userId: aliceId + }); + console.log(' shared "Weekend Plans" with Alice'); + + // 5. Create version history for "Self-Hosting Stack" + const versionContents = getVersionHistoryContents(); + for (let i = 0; i < versionContents.length; i++) { + await api(page, 'patch', `/api/notes/${noteIds['selfhosting']}`, { + content: versionContents[i] + }); + console.log(` version ${i + 2} created for "Self-Hosting Stack"`); + } + + // 6. Upload pixel-art images to "Bretzel Ingredients" + await uploadAttachment(page, noteIds['bretzel'], 'bretzel.png', generateBretzelPng(), true); + console.log(' uploaded bretzel.png (featured)'); + await uploadAttachment( + page, + noteIds['bretzel'], + 'salt-shaker.png', + generateSaltShakerPng(), + false + ); + console.log(' uploaded salt-shaker.png'); + + // 7. Create API keys + await api(page, 'post', '/api/settings/api-keys', { name: 'Claude Code' }); + await api(page, 'post', '/api/settings/api-keys', { name: 'N8N' }); + console.log(' API keys created'); +} + +export async function login(page: Page): Promise { + await page.goto(`${BASE_URL}/login`); + await page.getByTestId('email-input').fill(ADMIN.email); + await page.getByTestId('password-input').fill(ADMIN.password); + await page.getByTestId('login-btn').click(); + await page.waitForURL('**/'); + await page.waitForLoadState('networkidle'); +}