rdev/cookbooks/trees/aeries-daeya.yaml
jordan 32d50a6952
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: make infra provisioning idempotent + aeries-daeya public discovery feed
- Make postgres and redis provisioning idempotent: return success when already
  provisioned with credentials stored, allowing cookbook trees to safely include
  explicit add-db/add-redis steps alongside auto-provisioned project creation
- Update tests to reflect new idempotent behavior
- Consolidate docs CI into single multi-stage Docker build (remove separate
  build-docs step; Dockerfile.nginx now builds Slate then serves with nginx)
- Delete redundant skeleton docs/Dockerfile (replaced by multi-stage nginx image)
- Add watch verb to woodpecker-deployer RBAC (required by kubectl rollout status)
- Aeries Daeya cookbook: add public discovery feed (/) + character profiles (/c/:handle),
  characters.published/handle/tagline fields, dark pink design system, /studio/* routes,
  verify-public-discovery + verify-otp-endpoint smoke test steps
- Fix Input.tsx: remove non-existent --border-hover CSS variable hover effect
2026-02-28 17:32:21 -07:00

223 lines
22 KiB
YAML

name: aeries-daeya
description: "Aeries Daeya: Privacy-first avatar social platform. Create a digital identity, style it with AI from photo references, share experiences without exposing your real face. Public discovery feed + mutation explorer for iterating on looks and backgrounds."
version: 1
vars:
project_name: ""
service_name: "daeya-api"
worker_name: "media-worker"
app_name: "studio-ui"
steps:
# --- Infrastructure ---
create-project:
action: api
method: POST
endpoint: /project
body:
name: "{{ .vars.project_name }}"
description: "Aeries Daeya: Privacy-first avatar social platform"
outputs:
- project_id: .data.name
- domain: .data.domain
add-db:
description: CockroachDB for characters, looks, albums, posts, mutation history
depends_on: [create-project]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
body: { type: postgres, name: "daeya-db" }
add-redis:
description: Redis for SSE pub/sub and AI generation job queue
depends_on: [create-project]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
body: { type: redis, name: "event-bus" }
add-components:
description: Add daeya-api + media-worker + studio-ui as a single atomic commit
depends_on: [add-db, add-redis]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components/batch"
body:
components:
- type: service
name: "{{ .vars.service_name }}"
- type: worker
name: "{{ .vars.worker_name }}"
- type: app-react
name: "{{ .vars.app_name }}"
wait-infra:
description: Wait for CI to build and deploy all components
depends_on: [add-components]
action: wait_pipeline
project_id: "{{ .outputs.create-project.project_id }}"
max_attempts: 120
poll_interval: 10
# --- Feature 1: Avatar & Look Data Model ---
implement-avatar-model:
description: "Characters, looks, albums, posts, mutation history — full data layer + CRUD API + public discovery endpoints"
depends_on: [wait-infra]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds"
body:
prompt: "/implement-feature avatar-model --requirements 'IMPORTANT: Do NOT modify or reimplement the existing skeleton auth system. The skeleton already ships working OTP email auth at /auth/otp/send and /auth/otp/verify using NOTIFY_URL and NOTIFY_API_KEY env vars. Only add new domain-specific endpoints below. DB migrations in daeya-api: (1) CREATE TABLE characters (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, handle TEXT UNIQUE NOT NULL, name TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT false, skin_tone INT NOT NULL DEFAULT 5, face_shape TEXT NOT NULL DEFAULT ''oval'', hair_color TEXT NOT NULL DEFAULT ''brown'', hair_style TEXT NOT NULL DEFAULT ''medium'', eye_color TEXT NOT NULL DEFAULT ''brown'', body_type TEXT NOT NULL DEFAULT ''average'', description TEXT NOT NULL DEFAULT '''', tagline TEXT NOT NULL DEFAULT '''', avatar_url TEXT, status TEXT NOT NULL DEFAULT ''pending'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (2) CREATE TABLE looks (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, name TEXT NOT NULL, outfit_description TEXT NOT NULL DEFAULT '''', outfit_url TEXT, source_photo_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], status TEXT NOT NULL DEFAULT ''pending'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (3) CREATE TABLE albums (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, name TEXT NOT NULL, cover_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (4) CREATE TABLE posts (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, character_id UUID REFERENCES characters(id), look_id UUID REFERENCES looks(id), album_id UUID REFERENCES albums(id), image_url TEXT, video_url TEXT, caption TEXT NOT NULL DEFAULT '''', location_name TEXT, location_lat DECIMAL, location_lng DECIMAL, prompt_text TEXT, show_prompt BOOLEAN NOT NULL DEFAULT false, frame_style TEXT NOT NULL DEFAULT ''none'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (5) CREATE TABLE mutation_history (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, parameters_json JSONB NOT NULL, result_url TEXT, committed BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()). Domain models: Character, Look, Album, Post, MutationRecord with same fields. Services with interfaces in ports: CharacterService (Create, GetByID, GetByHandle, ListByUser, ListPublished, Update, SetPublished, UpdateAvatarURL), LookService (Create, GetByID, ListByCharacter, UpdateOutfitURL), AlbumService (Create, GetByID, ListByUser), PostService (Create, GetByID, ListByAlbum, ListByUser), MutationService (Create, GetByCharacter, Commit). Endpoints under /api/daeya-api: PUBLIC (no auth): GET /characters/public?limit&offset (returns published=true characters ordered by created_at desc, includes avatar_url, name, handle, tagline, look count), GET /c/:handle (returns single published character by handle including looks list). AUTHENTICATED (require JWT auth via skeleton auth.Middleware): POST /characters (body: {name, handle, description, tagline?, skin_tone?, face_shape?, hair_color?, hair_style?, eye_color?, body_type?} → 202 + character), GET /characters (returns user characters), GET /characters/{id}, PATCH /characters/{id} (update fields including published bool), POST /characters/{id}/looks (body: {name, outfit_description?, source_photo_urls?[]} → 202 + look), GET /characters/{id}/looks, POST /characters/{id}/mutations (body: {skin_tone?, background?, lighting?, style_filter?} → 202 + mutation), POST /characters/{id}/mutations/{mutation_id}/commit (commits mutation as new character baseline), POST /albums (body: {name}), GET /albums, GET /albums/{id}/posts, POST /posts (body: {character_id, look_id?, album_id?, caption?, location_name?, location_lat?, location_lng?, prompt_text?, show_prompt?, frame_style?} → post), GET /posts/{id}. After creating a character enqueue job {type:generate_character, character_id}. After creating a look enqueue job {type:generate_look, look_id}. After creating a mutation enqueue job {type:generate_mutation, mutation_id}. Publish SSE events via SSE hub: character_updated {character_id, status, avatar_url} to channel:daeya, look_updated {look_id, status, outfit_url} to channel:daeya, mutation_ready {mutation_id, character_id, result_url} to channel:daeya.'"
auto_commit: true
auto_push: true
git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs:
- build_id: .data.task_id
wait-avatar-model:
depends_on: [implement-avatar-model]
action: wait_build
build_id: "{{ .outputs.implement-avatar-model.build_id }}"
max_attempts: 120
poll_interval: 5
# --- Feature 2: AI Generation Pipeline ---
implement-generation:
description: "Character portrait gen → look styling from photos → mutation explorer → action expressions"
depends_on: [wait-avatar-model]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds"
body:
prompt: "/implement-feature ai-generation --requirements 'Implement generation pipeline in media-worker using skeleton ai-client package (LAOZHANG_API_KEY for image generation, GEMINI_API_KEY for Gemini vision photo analysis). Worker consumes jobs from queue by type: JOB generate_character {character_id}: Load character row. Build a portrait prompt from character fields: face_shape, hair_color, hair_style, eye_color, skin_tone (map 1-10 to descriptive terms: 1=very fair, 3=light, 5=medium, 7=tan, 9=deep, 10=very dark), body_type, description. Prompt template: \"Portrait photo of a [gender-neutral] person. [description]. [skin_tone_desc] skin, [face_shape] face, [hair_color] [hair_style] hair, [eye_color] eyes. [body_type] build. Front-facing, neutral expression, soft studio lighting, clean white background. High quality, photorealistic.\" Generate one image via LAOZHANG_API_KEY. Save URL to character.avatar_url, set status=ready. Publish SSE character_updated {character_id, status:ready, avatar_url}. JOB generate_look {look_id}: Load look + character rows. If source_photo_urls non-empty: use Gemini vision (GEMINI_API_KEY) to analyze first photo and extract style descriptors (garment type, color palette, fabric, silhouette, styling details). Build outfit prompt using descriptors + character appearance. If no source photos: use outfit_description directly. Prompt template: \"The same person from the reference portrait is now wearing [outfit_description]. Full body view. [character appearance details]. Natural lighting. High quality, photorealistic.\" Use character.avatar_url as style reference if available. Generate one image. Save to look.outfit_url, set status=ready. Publish SSE look_updated {look_id, status:ready, outfit_url}. JOB generate_mutation {mutation_id}: Load mutation_history + character rows. Parameters from parameters_json may include: skin_tone (1-10), background (city_day|city_night|nature_park|studio_white|studio_dark|beach|mountain), lighting (natural|golden_hour|dramatic|neon|soft_box), style_filter (realistic|stylized|anime|painterly). Build variation prompt applying parameter overrides. Background mapping: city_day=busy city street daytime, city_night=city street at night with neon lights, nature_park=lush green park, studio_white=plain white studio, studio_dark=dark minimalist studio, beach=sandy beach golden hour, mountain=mountain vista clear sky. Style filter suffix: anime=anime art style illustration, painterly=oil painting impressionist style (others=photorealistic). Generate image. Save to mutation_history.result_url. Publish SSE mutation_ready {mutation_id, character_id, result_url}. JOB generate_action {character_id, look_id, action_text, post_id}: Load character + look. Build action prompt: the character performing the action described (spin while showing outfit=full body 3/4 turn pose; say Happy Birthday=person holding birthday cake smiling waving; wave=person waving at camera smiling). Generate image. Save image_url to posts table for post_id. Publish SSE action_ready {post_id, image_url}.'"
auto_commit: true
auto_push: true
git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs:
- build_id: .data.task_id
wait-generation:
depends_on: [implement-generation]
action: wait_build
build_id: "{{ .outputs.implement-generation.build_id }}"
max_attempts: 120
poll_interval: 5
wait-deploy-2:
description: Deploy generation pipeline
depends_on: [wait-generation]
action: wait_pipeline
project_id: "{{ .outputs.create-project.project_id }}"
max_attempts: 120
poll_interval: 10
# --- Feature 3: Studio UI ---
implement-studio-ui:
description: "Public discovery feed, character profile, creation wizard, mutation explorer, look styling"
depends_on: [wait-deploy-2]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds"
body:
prompt: |-
/implement-feature studio-ui --requirements 'Build the React studio UI using skeleton packages (@project/ui, @project/auth, @project/layout, @project/realtime, @project/api-client). DESIGN SYSTEM: Use these exact CSS custom properties or Tailwind custom tokens throughout — bg-base:#0a0a0f, bg-card:#12121a, bg-panel:#1a1a2e, border-default:#2a2a3e, text-primary:#e0e0e0, text-muted:#888888, accent:#ff6b9d, accent-dark:#c44569, success:#4ade80, error:#ef4444. Apply: cards use bg-card + 16px border-radius + 1px solid border-default + hover:translateY(-4px) + hover:box-shadow 0 8px 30px rgba(0,0,0,0.3). Primary buttons use gradient background linear-gradient(135deg,#ff6b9d,#c44569) + 12px border-radius + hover:translateY(-2px) + hover:box-shadow 0 8px 20px rgba(255,107,157,0.3). Secondary buttons use border 1px solid border-default + text-muted + hover:border-accent + hover:text-accent. Inputs/textareas use bg-base + border 1px solid border-default + 12px border-radius + focus:border-accent. Pills (selected state): border accent + bg rgba(255,107,157,0.1) + text white. Status badges: pending=bg rgba(42,42,62,1) text #888, generating=bg rgba(255,107,157,0.2) text #ff6b9d, ready=bg rgba(74,222,128,0.2) text #4ade80. All transitions 200ms ease. Body font: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif. ROUTE STRUCTURE — two surfaces, same domain: PUBLIC ROUTES (no auth required): / = Discovery feed, /c/:handle = Public character profile. PROTECTED ROUTES (require auth via ProtectedRoute from @project/auth): /login = OTP login, /studio = Your characters dashboard, /studio/characters/new = Creation wizard, /studio/characters/:id = Character detail + mutation explorer, /studio/albums = Album management. DISCOVERY FEED (/): Full-width page, bg-base. Top nav: logo left, "Create Your Character" button right (links to /login if not authed, else /studio/characters/new). Hero: centered headline "Meet characters created by real people" + subtext "Browse, follow, and collaborate without sharing your real face." Below hero: responsive grid (auto-fill minmax 260px) of CharacterCard components fetched from GET /api/daeya-api/characters/public. CharacterCard: bg-card, 16px radius, overflow hidden, hover elevation. Top: avatar_url as square image (aspect-ratio:1, object-fit:cover). Bottom: padding 16px, name in white 600 weight, handle in text-muted 14px (@handle), tagline in text-muted 13px truncated, row of up to 4 look-count and status badges. Click card → /c/:handle. Empty discovery state: "No characters published yet. Be the first." Infinite scroll or Load More button calling GET /characters/public?offset=N. PUBLIC CHARACTER PROFILE (/c/:handle): Fetch from GET /api/daeya-api/c/:handle. Large hero: avatar_url as circular 160px centered, name heading, handle in text-muted, tagline. Trait pills row (skin tone swatch + hair color + hair style + eye color). "Looks" section: masonry or even grid of look outfit_url images. Clicking image opens fullscreen modal with prev/next. "Collaborate" button (deferred, shows "Coming soon" toast). OTP LOGIN (/login): bg-base centered card. Email input → "Send Code" button (primary). Then code input → "Verify" button. On success store token and redirect to /studio. Uses POST /api/daeya-api/auth/otp/send and POST /api/daeya-api/auth/otp/verify. STUDIO DASHBOARD (/studio): DashboardShell from @project/layout. Header: "Your Characters" title + "New Character" primary button. Grid of CharacterCard (same card component as discovery but with edit controls: Publish toggle + Edit link). Published characters show a small accent dot. Empty state: centered + "Create your first character" button. CREATION WIZARD (/studio/characters/new): 4-step wizard, progress bar in accent color. STEP 1 DESCRIBE: Textarea placeholder "Describe your character — their vibe, energy, how they carry themselves." Handle field: @handle (lowercase, alphanumeric, used in public URL). Skin tone picker: 10 circular swatches (#FDDBB4 lightest to #3D1A00 darkest), selected=2px ring in accent. STEP 2 SHAPE: Hair color pills (Blonde,Brown,Black,Red,Auburn,White,Silver). Hair style pills (Short,Medium,Long,Curly,Wavy,Straight,Bob,Bun). Eye color pills (Brown,Blue,Green,Hazel,Gray,Amber). Body type pills (Slim,Athletic,Average,Curvy,Plus). Face shape pills (Oval,Round,Square,Heart,Diamond). Name field. Tagline field. All pills: default=secondary style, selected=accent pill style. STEP 3 SOUL (optional): Interest pills multi-select up to 3 (Travel,Fashion,Music,Food,Art,Fitness,Books,Tech,Nature). Live summary card updates as user selects — shows all chosen traits as accent pills. STEP 4 GENERATE: Summary card. "Generate Character" primary button. On submit POST /api/daeya-api/characters. Shows full-screen generating overlay: avatar generating animation (pulsing circle in accent color) + cycling messages "Bringing your character to life..." / "Crafting their appearance..." / "Almost there...". On SSE character_updated with status=ready redirect to /studio/characters/:id. CHARACTER DETAIL (/studio/characters/:id): Two-column layout. Left (1/3): circular avatar 200px (pending=pulsing bg-panel circle). Name + @handle + tagline. Trait pills. PUBLISH TOGGLE: labeled "Public" — toggle switch in accent when on. On toggle call PATCH /api/daeya-api/characters/:id {published: true/false}. Published characters get a small "Live on discovery" badge. "Add Look" button opens look panel. Looks list: each row shows look name, 48px outfit_url thumbnail, status badge. Clicking look opens fullscreen modal. Right (2/3): MUTATION EXPLORER panel (bg-panel, 16px radius, p-24). Header "Explore Variations" in white. Parameters: Skin Tone — row of 10 swatches, click to select (highlight in accent). Background — pill row: City Day, City Night, Nature, Studio, Beach, Mountain. Lighting — pill row: Natural, Golden Hour, Dramatic, Neon. Style — pill row: Realistic, Stylized, Anime, Painterly. "Generate Preview" primary button → POST /api/daeya-api/characters/:id/mutations. Preview area 1:1 aspect ratio: shows result_url when ready, else shows pulsing bg-panel placeholder + "Generating..." text. "Commit as New Baseline" button (accent, enabled only when preview ready) → POST commit endpoint → refresh avatar. "Discard" secondary button resets params. ADD LOOK PANEL (slide-in from right, 400px, bg-panel): Look name field. Upload area: dashed border border-default, "Drop photos here or click to upload" in text-muted, max 3. Thumbnail previews of uploaded photos. "Or describe the outfit" textarea below. "Generate Look" primary button. ALBUMS (/studio/albums): Grid of AlbumCard. "New Album" button → inline modal. REALTIME: useEventChannel from @project/realtime subscribed to channel:daeya. On character_updated: refresh avatar + status in dashboard and detail. On look_updated: refresh look thumbnail. On mutation_ready: show in mutation explorer preview. On action_ready: show in post.'
auto_commit: true
auto_push: true
git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git"
outputs:
- build_id: .data.task_id
wait-studio-ui:
depends_on: [implement-studio-ui]
action: wait_build
build_id: "{{ .outputs.implement-studio-ui.build_id }}"
max_attempts: 120
poll_interval: 5
wait-deploy-final:
description: Deploy final build
depends_on: [wait-studio-ui]
action: wait_pipeline
project_id: "{{ .outputs.create-project.project_id }}"
max_attempts: 120
poll_interval: 10
# --- Verification ---
verify-health:
description: Verify daeya-api is healthy
depends_on: [wait-deploy-final]
action: shell
command: |
HEALTH=$(curl -sf "https://{{ .outputs.create-project.domain }}/api/daeya-api/health" | jq -r '.data.status // empty')
if [ "$HEALTH" = "healthy" ]; then
echo "daeya-api healthy"
exit 0
else
echo "daeya-api not healthy: $HEALTH"
exit 1
fi
verify-site:
description: Verify studio-ui frontend loads
depends_on: [wait-deploy-final]
action: wait_site
domain: "{{ .outputs.create-project.domain }}"
project_id: "{{ .outputs.create-project.project_id }}"
max_attempts: 30
poll_interval: 5
verify-public-discovery:
description: Verify public discovery feed responds without auth
depends_on: [verify-health]
on_error: continue
action: shell
command: |
DOMAIN="{{ .outputs.create-project.domain }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"https://$DOMAIN/api/daeya-api/characters/public")
echo "GET /characters/public (no auth) returned: $STATUS"
if [ "$STATUS" = "200" ]; then echo "Public discovery endpoint confirmed"; exit 0; fi
echo "Unexpected status — public endpoint may not exist"
exit 1
verify-auth-endpoint:
description: Verify authenticated character endpoint requires auth (401 confirms it)
depends_on: [verify-health]
on_error: continue
action: shell
command: |
DOMAIN="{{ .outputs.create-project.domain }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"https://$DOMAIN/api/daeya-api/characters" \
-H "Content-Type: application/json")
echo "GET /characters without auth returned: $STATUS"
if [ "$STATUS" = "401" ]; then echo "Auth guard confirmed"; exit 0; fi
echo "Unexpected status — endpoint may not exist"
exit 1
verify-otp-endpoint:
description: Verify OTP email endpoint exists and notify is configured (200/202/429 confirms it)
depends_on: [verify-health]
on_error: continue
action: shell
command: |
DOMAIN="{{ .outputs.create-project.domain }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
"https://$DOMAIN/api/daeya-api/auth/otp/send" \
-H "Content-Type: application/json" \
-d '{"email":"smoke-test@example.com"}')
echo "POST /auth/otp/send returned: $STATUS"
if [ "$STATUS" = "200" ] || [ "$STATUS" = "202" ] || [ "$STATUS" = "429" ]; then
echo "OTP endpoint + notify confirmed working"
exit 0
fi
echo "Unexpected status — OTP endpoint may be missing or notify credentials not injected"
exit 1
teardown:
- description: Delete project and all provisioned resources
action: api
method: DELETE
endpoint: "/project/{{ .outputs.create-project.project_id }}"