43f4345 ←c42b5e4Claude CodeApr 14, 2026 macos

plan created

A /docs/undefined
1# Plan: macOS avatar + wallpaper upload to sublimated space
2
3## Context
4
5The sublimated macOS app currently shows a letter initial as the user's avatar. We want to extract the macOS user profile picture and desktop wallpaper, upload them as regular CAS-backed files into the user's space at `.sbl/avatar.jpg` and `.sbl/wallpaper.jpg`, and serve them via vanity URLs `sublimated.com/{username}/avatar.jpg` and `sublimated.com/{username}/wallpaper.jpg`.
6
7## Part 1: Swift — ProfileAssetUploader
8
9**New file:** `sublimated.com/apps/mac/Sublimated/ProfileAssetUploader.swift`
10
11Modeled after popssh's `ProfileAssetUploader` but uses the sublimated SDK (`SpaceHandle.createFile` / `SpaceHandle.updateContent`).
12
13### Logic:
14- `uploadAll(spaceSlug:token:)` — concurrently uploads avatar + wallpaper, logs errors, does not throw
15- `uploadAvatar(space:)` — reads macOS user avatar from cache paths (JPEG > TIFF > dsimport), converts to JPEG 512px, uploads
16- `uploadWallpaper(space:)` — reads `NSWorkspace.shared.desktopImageURL(for:)`, converts to JPEG 1920px, uploads
17- upload flow per file: list items in space to check if `.sbl/avatar.jpg` exists. if exists → `updateContent()`. if not → `createFile()` with `folderPath: ".sbl"`, `name: "avatar.jpg"`, `contentType: "image/jpeg"`
18- NSImage JPEG helpers — copy from popssh (private extension on NSImage with `jpegData(maxSize:)`, `jpegData(maxWidth:)`, `resizedJpeg(to:)`)
19
20### Trigger:
21- In `MainView.swift` `.task` block, after `validateSession()` succeeds — fire-and-forget `Task { ... }`
22- Create `SublimatedClient` from `AppConfig.apiBaseURL` + `StaticTokenProvider` with `credentials.token`
23- Get `SpaceHandle` via `client.space(credentials.spaceSlug)`
24
25### Files:
26- `sublimated.com/apps/mac/Sublimated/ProfileAssetUploader.swift` (new) — xcodegen auto-discovers it in `mac/Sublimated/`
27- `sublimated.com/apps/mac/Sublimated/MainView.swift` (edit line ~47-51 .task block)
28- run `xcodegen generate` from `sublimated.com/apps/`
29
30## Part 2: Edge rewrite — serve avatar.jpg / wallpaper.jpg
31
32**File:** `sublimated.com/src/sublimated.com/src/pages/[...path].astro`
33
34Insert early return after `parsePageContext()` (line 54), before `fetchPageData`.
35
36### Logic:
37```
38if pathSegments.length === 1 && (pathSegments[0] === 'avatar.jpg' || pathSegments[0] === 'wallpaper.jpg'):
39 1. createDb(runtime.env.DB_READ)
40 2. getSpaceBySlug(db, slug) — resolve space
41 3. if no space → fall through to normal 404
42 4. build ltree: `${slug}._sbl.${nameToLtreeLabel(pathSegments[0])}`
43 → avatar.jpg becomes `{slug}._sbl.avatar-jpg`
44 → wallpaper.jpg becomes `{slug}._sbl.wallpaper-jpg`
45 5. getItemByPath(db, space.id, ltreePath) — find item
46 6. if no item → return 404 Response
47 7. fetch from CAS: runtime.env.CAS.get(item.contentHash)
48 8. return new Response(blob, {
49 headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'public, max-age=3600' }
50 })
51```
52
53### Imports to add:
54- `getSpaceBySlug` from `../../../../shared/services/query/spaces`
55- `getItemByPath` from `../../../../shared/services/query/items`
56- `nameToLtreeLabel` from `../../../../shared/names`
57
58### Reused functions:
59- `getSpaceBySlug(db, slug)``src/shared/services/query/spaces.ts:6`
60- `getItemByPath(db, spaceId, ltreePath)``src/shared/services/query/items.ts:7`
61- `nameToLtreeLabel(name)``src/shared/names.ts:23`
62- `createDb()``src/sublimated.com/src/lib/db.ts`
63
64## Verification
65
66- build macOS app in Xcode, sign in, check console for `[ProfileAsset]` upload logs
67- visit `sublimated.com/{username}/avatar.jpg` in browser — should return JPEG
68- visit `sublimated.com/{username}/wallpaper.jpg` — should return JPEG wallpaper
69- visit for nonexistent user or user without upload — should 404
70- verify normal page routes still work (no regression from early return)