Commit graph

17 commits

Author SHA1 Message Date
Till JS
f94c047daa chore: silence pre-existing svelte-check a11y warnings
Pre-push hook runs svelte-check with --fail-on-warnings; nine
long-standing warnings in unrelated files (forms / website-blocks)
were blocking otherwise-clean pushes.

Each <label> here is a visual label whose control follows on the next
line — accessible to a screen reader through proximity but not through
a `for=`/`id` association. The state_referenced_locally cases capture
a prop on first render by design (re-running the hook on prop change
would be a different feature). The <nav role=tablist> is the existing
tab-strip semantic.

All seven sites get scoped svelte-ignore comments rather than functional
rewrites — the goal is to unblock CI, not redesign these components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:34:36 +02:00
Till JS
ace1b706e6 feat(forms): M8 website-block — formEmbed bindet Mana-Formulare ein
Neuer Block-Type `formEmbed` im Website-Builder
(docs/plans/forms-module.md M8):

- @mana/website-blocks/src/formEmbed/:
  - schema.ts: FormEmbedSchema mit token (32-char base64url) +
    titleOverride + optional resolved-Block (formTitle, fields,
    branching, settings.{submitButtonLabel, successMessage}).
    FormFieldEmbedSchema duplicated leichtgewichtig statt cross-
    package import — website-blocks bleibt self-contained.
  - FormEmbed.svelte: edit/preview rendert Placeholder-Card mit
    Token-Snippet und resolved-Status; public rendert die kompletten
    11 Field-Types inkl. Live-Branching-aware-Render. Submitter-
    Block (Name+Email optional). Submit POSTet an
    /api/v1/forms/public/:token/submit. Lazy-Fallback fetcht
    /api/v1/unlisted/public/:token wenn die publish-resolver-blob
    fehlt. Bot-Honeypot bleibt M8-Polish.
  - FormEmbedInspector.svelte: Token-Input mit base64url-Validierung
    bei blur, optional titleOverride, resolved-Card mit
    Field-Count + Logik-Regel-Count.
- BLOCK_SPECS + BLOCK_SCHEMAS + BLOCK_DEFAULTS um formEmbed
  erweitert. schemas.test.ts erwartet jetzt 12 Block-Types.
- apps/mana/apps/web/src/lib/modules/website/forms-embeds.ts:
  resolveFormEmbed scant formTable nach unlistedToken (linear scan
  ist günstig bei <100 forms pro user, kein Index nötig), dekrypted,
  validiert published-status, gibt resolved-Block zurück.
- publish.ts.resolveEmbedsInTree erweitert um formEmbed-Branch — ruft
  resolveFormEmbed parallel zu resolveEmbed (moduleEmbed) im selben
  Walk.

Trade-offs:
- Token statt formId: bei Token-Rotation (M4b) muss der User den Block
  neu konfigurieren. Der formEmbed-Block-Resolver erkennt das + setzt
  resolved.error; public-Renderer fällt auf lazy-fetch zurück.
- Plaintext stored: das resolved-Blob landet als plaintext im
  public-snapshot, gleiches Trust-Modell wie moduleEmbed (öffentliche
  Website per Definition).

Tests: website-blocks 50/50 grün (12 schema-block-types + per-type
defaults validation). svelte-check 0 errors. forms 26/26 unverändert.

Use-Case: Vereins-Sommerfest. User legt /forms/anmeldung an,
publisht, setzt unlisted, kopiert Token. Im Website-Builder fügt er
einen formEmbed-Block auf der Event-Seite ein, paste Token → bei
Publish wird der Form-Schema inlined → Besucher submitten direkt
auf der Vereins-Website.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 02:38:28 +02:00
Till JS
1cb137c4ff feat(visibility): pull augur onto the embed + privacy-overview rails
Augur landed (faa16fa89) with the visibility Picker + setVisibility
already in place — but no embed-resolver and no entry in the
/settings privacy registry. So flipping an omen to 'public' did
nothing visible, and the kill-switch couldn't see augur records
either. Closes both gaps.

- New EmbedSource `augur.entries` + resolveAugurEntries. Whitelist:
  claim + "{kind} · {vibe} · {outcome}" line. Personal fields
  (feltMeaning, expectedOutcome, source name, outcomeNote, related
  dream/decision links, livingOracleSnapshot) all stay private.
  Optional `status` filter maps to AugurOutcome so the user can
  build "predictions I got right" widgets.
- Sort: resolved-first, then encounteredAt desc — fulfilled
  predictions outrank still-open ones (more interesting public
  signal).
- Inspector dropdown gains "Augur (Omen / Wahrsagungen)".
- exposed-records.ts gains the augur entry — augur records now
  show up in /settings → Privatsphäre and the kill-switch.

Note: augur's `unlistedToken` field (set by its store on
'unlisted' flips) is currently dead code — the mana-api unlisted
backend doesn't know about `augurEntries` and there's no shared
view component. Half-state predates this commit; full unlisted-
share wiring is a separate, larger task that would touch the
backend's ALLOWED_COLLECTIONS, the resolvers blob, and a new
SharedAugurEntryView. Leaving as-is until there's clear demand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:18:14 +02:00
Till JS
f71a9377c0 feat(visibility): embed resolvers for memoro/cards/presi (M6 follow-on)
Closes the M6 loop — flipping a memo, card-deck, or presi-deck to
'public' now actually surfaces it on the owner's website embed.
Previously M6 wired the Picker but the embed pipeline didn't know
about these sources, so the flip had no visible effect.

Three new sources in EmbedSourceSchema:
- memoro.memos — voice-memo teaser. Title + intro (140 chars) +
  audio duration. Transcript, source-audio paths, and per-utterance
  speaker data stay private — those are the user's words verbatim
  with much stronger privacy weight than a curated headline.
- cards.decks — flashcard-collection teaser. Name + "N Karten".
  Card fronts/backs, difficulty, review history all private — the
  deck is a unit; the cards belong to the play experience.
- presi.decks — "talks I've given" teaser. Title + "N Folien"
  (counted by joining the slides table). Slide content stays
  private — the public deck is a pointer, the slides belong to
  the talk experience.

Each resolver tolerates the M6 soft-migration window: visibility
falls back to legacy isPublic for rows that haven't been re-saved
since the M6 commit.

Inspector dropdown updated to expose all 15 sources.

Note: 3 unrelated svelte-check errors in
data/seeds/wiring.test.ts (spaceId on LocalWorkbenchScene) from a
parallel session. Not introduced here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:33:43 +02:00
Till JS
59b147f5ee feat(visibility): embed resolvers for habits/quiz/social-events + inspector refresh
Closes the loop on the M5-Rest visibility rollout — flipping a habit,
quiz, or social-event to 'public' now actually surfaces it on the
owner's website embed.

EmbedSourceSchema gains three new sources:
- habits.habits — build-in-public widget. Title + "🔥 N Tage Streak ·
  gesamt M ×". Per-log timestamps + notes stay private (sleep/intake
  patterns are not for public consumption).
- quiz.quizzes — shareable-quiz teaser. Title + "N Fragen · {category}".
  Questions, options, explanations, attempts/scores all stay private —
  the actual play-experience is reserved for a future unlisted-share
  flow.
- events.socialEvents — RSVP-event teaser. Title + formatted start
  date + location + cover image. Hard-gated on the unified `visibility`
  only; the legacy `isPublished` flag is intentionally bypassed so the
  new Picker is the single source of truth (M6 will drop isPublished).

ModuleEmbedInspectorFallback now lists all 12 sources — was only
exposing 2 of the 9 already-wired ones (latent debt unblocking the
new sources from being addable in the editor).

Note: 7 unrelated svelte-check errors exist in
data/scope/dedup-workbench-scenes.test.ts from a parallel session
(spaceId not on LocalWorkbenchScene). Not introduced here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:58:43 +02:00
Till JS
87b567eec9 i18n: fix IT/FR/ES parity gaps in dashboard + memoro
- dashboard: +5 Einträge pro Sprache für die beiden neuen Widgets
  activity_feed + articles_unread.
- memoro: +1 Eintrag pro Sprache für memo.load_more.

Damit sind dashboard (111) und memoro auf gleichem Stand wie DE/EN.
Verbleibende Drift (app_slider-Legacy-Keys in memoro IT/FR/ES,
common/auth-Legacy in calendar/times) ist strukturell und bleibt
einem Folge-Cleanup vorbehalten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:19:59 +02:00
Till JS
218cf45005 feat(wardrobe): M5.c — outfits adopt the unified visibility system
Eighth consumer of @mana/shared-privacy. Wardrobe outfits now carry a
VisibilityLevel flipped via <VisibilityPicker compact> in the outfit
detail page; the wardrobe.outfits embed powers the style-portfolio
use-case on the owner's website.

Scope: outfits only, not individual garments. Outfits are the composite
unit users curate for public presentation (an outfit is an intentional
composition; a single garment rarely is). Garments inherit their outfit
visibility implicitly — a public outfit reveals the look, the garment
pieces behind it stay private at the record level.

Changes:
- wardrobe/types: visibility + unlistedToken + visibilityChangedAt +
  visibilityChangedBy on LocalWardrobeOutfit; Outfit (UI) requires
  visibility; toOutfit converter forwards with 'space' fallback
- wardrobe/stores/outfits: createOutfit stamps
  defaultVisibilityFor(activeSpace.type); new setVisibility(id, level)
  mints/clears the unlisted token on the transition boundary and emits
  cross-module VisibilityChanged
- wardrobe/views/DetailOutfitView: <VisibilityPicker compact> in the
  metadata header row, left of the favourite/edit icons — keeps the
  action rail tight while making exposure state glanceable

website embed:
- website-blocks/moduleEmbed/schema: 'wardrobe.outfits' added to
  EmbedSourceSchema
- website/embeds: resolveWardrobeOutfits gates hard on
  canEmbedOnWebsite, filters archived + deleted, optional isFavorite /
  tagIds filters, favourites-first then newest. Inlines title +
  occasion/season meta + the lastTryOn.imageUrl (the AI-generated
  wearing shot). Description, garment details, and internal tag labels
  stay out of the public snapshot

Verified:
- pnpm check (web): 7450 files, 0 errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:08:32 +02:00
Till JS
0e0d48acec feat(recipes): M5.b — recipes adopt the unified visibility system
Seventh consumer of @mana/shared-privacy. Recipes now carry a
VisibilityLevel; the recipes.recipes embed powers "my cookbook" /
"tested recipes" sections on the owner's website.

Changes:
- recipes/types: visibility + unlistedToken + visibilityChangedAt +
  visibilityChangedBy on LocalRecipe; Recipe (UI) requires visibility
- recipes/queries: toRecipe forwards visibility with 'space' fallback
- recipes/stores/recipes: createRecipe stamps
  defaultVisibilityFor(activeSpace.type); duplicateRecipe resets to
  the space default (copies don't inherit public status — same rule
  as picture boards); new setVisibility(id, level) emits
  cross-module VisibilityChanged
- recipes/ListView: <VisibilityPicker> as the first row of the
  detail-panel when a card is expanded. Recipes has no dedicated
  detail route so inline-expand is the canonical surface

website embed:
- website-blocks/moduleEmbed/schema: 'recipes.recipes' added to
  EmbedSourceSchema
- website/embeds: resolveRecipes gates hard on canEmbedOnWebsite,
  optional isFavorite + tagIds filters, favourites-first then newest,
  inlines { title, subtitle ('30 Min · 4 Port.'), imageUrl }.
  Ingredients + steps + internal tag labels stay out of the snapshot —
  the embed is a teaser; full recipes are a later M8 unlisted-page
  feature.

Verified:
- pnpm check (web): 7450 files, 0 errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:04:14 +02:00
Till JS
2af2a4d5c0 feat(places): M5.a — places adopt the unified visibility system
Sixth consumer of @mana/shared-privacy. Places now carry a VisibilityLevel
flipped via <VisibilityPicker> in the Places DetailView; the new
places.places embed powers "my favourite cafes" / "rehearsal rooms" /
"gyms I train at" sections on the owner's website.

Changes:
- places/types: visibility + unlistedToken + visibilityChangedAt +
  visibilityChangedBy on LocalPlace; Place (UI type) requires visibility
- places/queries: toPlace forwards visibility with 'space' fallback for
  legacy rows
- places/stores/places: createPlace stamps
  defaultVisibilityFor(activeSpace.type); new setVisibility(id, level)
  mints/clears the unlisted token on the transition boundary and emits
  cross-module VisibilityChanged
- places/views/DetailView: <VisibilityPicker> as the first field-row,
  above Kategorie

website embed:
- website-blocks/moduleEmbed/schema: 'places.places' added to
  EmbedSourceSchema; filter docstring describes the places-specific
  reuse of existing kind/isFavorite/tagIds filter fields
- website/embeds: resolvePlaces gates hard on canEmbedOnWebsite,
  applies optional kind (→ PlaceCategory) / isFavorite / tagIds
  filters, sorts favourites-first then alphabetical.

Privacy: Whitelist (title + address only). Latitude/longitude are
explicitly NOT inlined — 10m precision of a home or workplace can
identify someone, and silently publishing coords on a visibility flip
would be the classic leak the design was built to prevent (plan §2).

Verified:
- pnpm check (web): 7450 files, 0 errors

Next: M5.b — Events (socialEvents), Recipes, Wardrobe-Outfits, Habits,
Quiz, Invoices-Clients. Same pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:59:15 +02:00
Till JS
95e85bdffd feat(goals): M4.c — goals adopt the unified visibility system
Fifth consumer of @mana/shared-privacy, completing the M4 trio
(Calendar + Todo + Goals). Goals live under $lib/companion/goals/
(legacy path, pre-rename to 'ai') instead of the standard /modules/
tree, so the adoption lands in its own commit.

Enables the "public progress page" use case — a fitness / learning /
build-in-public goal with its current-period progress inlined on the
owner's website, rendered as "4 / 5 · Woche".

Changes:
- companion/goals/types: visibility + unlistedToken +
  visibilityChangedAt + visibilityChangedBy on LocalGoal (LocalGoal
  doubles as the UI type here, no separate plaintext variant)
- companion/goals/store: createFromTemplate and create both stamp
  defaultVisibilityFor(activeSpace.type) at insert; new
  setVisibility(id, level) mints/clears the unlisted token on the
  transition boundary and emits cross-module VisibilityChanged
- modules/goals/ListView: <VisibilityPicker compact> on each active
  goal card, sitting between the title and the pause button (goals
  have no dedicated detail view — list-inline is the natural spot)

website embed:
- website-blocks/moduleEmbed/schema: 'goals.goals' added to
  EmbedSourceSchema; filter docstring describes the active-vs-
  completed split that power users can use to section their progress
  page
- website/embeds: resolveGoals gates hard on canEmbedOnWebsite,
  filters by optional status ('active' | 'completed' | 'paused' |
  'abandoned'), sorts active-first then by target descending so
  milestone goals land on top. Inlined EmbedItem is whitelist-only —
  title + compact progress line like "4 / 5 · Woche". Description,
  metric configuration (event types, filter fields), and internal
  tracking state stay out of the snapshot; the goal's implementation
  detail leaks what the user is measuring, not just the milestone

Verified:
- pnpm check (web): 7450 files, 0 errors
- pnpm test goals + website: 29/29
- pnpm run validate:all green

M4 is done. Next: M5 — Places + Events + Recipes + Habits + Quiz +
Wardrobe + Invoices-Clients. Same pattern, one module at a time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 02:41:27 +02:00
Till JS
015a2c18ee feat(todo): M4.b — tasks adopt the unified visibility system
Fourth consumer of @mana/shared-privacy. Tasks now carry a
VisibilityLevel flipped via <VisibilityPicker> in the Todo DetailView;
a new todo.tasks embed source powers the "public roadmap" use-case
(mark a handful of tasks public, drop the embed on the Website).

Changes:
- todo/types: visibility + unlistedToken + visibilityChangedAt +
  visibilityChangedBy on LocalTask; Task (UI type) requires visibility
- todo/queries: toTask forwards visibility with 'space' fallback for
  legacy rows (pre-M4.b records have no field set; Dexie hook stamped
  'space' since spaces-foundation v28)
- todo/stores/tasks: createTask stamps
  defaultVisibilityFor(activeSpace.type); new setVisibility(id, level)
  mints/clears the unlisted token on the transition boundary and
  emits cross-module VisibilityChanged
- todo/views/DetailView: <VisibilityPicker> dropped in as the first
  prop-row above Priorität so the user sees exposure state at a glance
  whenever they open a task

website embed:
- website-blocks/moduleEmbed/schema: 'todo.tasks' added to
  EmbedSourceSchema; filter docstring explains the todo-specific shape
  (status + tagIds for the typical "shipped items with #public" filter)
- website/embeds: resolveTodoTasks gates hard on canEmbedOnWebsite,
  maps the optional status filter ('completed' → isCompleted=true),
  joins the N:N taskTags table for the optional tagIds filter, sorts
  newest-first with id as stable tiebreaker. Inlined EmbedItem is
  whitelist-only — title + status label ('Erledigt' / 'In Arbeit').
  Description, subtasks, LLM-labels, due-dates, and project
  memberships stay out of the public snapshot (per plan §2 redaction
  policy)

Verified:
- pnpm check (web): 7450 files, 0 errors
- pnpm test todo + website: 38/38

Next: M4.c — Goals. Lives under $lib/companion/goals/ (not in the
standard /modules/ tree), so the adoption path is slightly different
and gets its own commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 02:37:32 +02:00
Till JS
ac44d51363 feat(calendar): M4.a — events adopt the unified visibility system
Third consumer of @mana/shared-privacy. Calendar events now carry a
VisibilityLevel the owner flips from the EventDetailModal via
<VisibilityPicker>; a new calendar.events embed source lets the user
drop a moduleEmbed block on their website that pulls their public
events in.

This unblocks concrete use-cases the Website-Builder audit surfaced:
band tour dates, public workshops, public rehearsals on a team-space
website, meeting-with-the-host pages.

Changes:
- calendar/types: visibility + unlistedToken + visibilityChangedAt +
  visibilityChangedBy on LocalEvent; CalendarEvent (UI type) requires
  visibility. timeBlockToCalendarEvent forwards the field; cross-module
  TimeBlocks (tasks, habits, time entries) without an owning
  LocalEvent fall back to 'space' so they stay off the public embed
- calendar/stores/events: createEvent stamps
  defaultVisibilityFor(activeSpace.type); createDraftEvent seeds a
  'private' draft until the user explicitly opts in; new
  setVisibility(id, level) mints/clears the unlisted token on the
  transition boundary and emits cross-module VisibilityChanged
- calendar/components/EventDetailModal: <VisibilityPicker compact>
  sits in the modal-actions row left of copy/edit/delete

website embed:
- website-blocks/moduleEmbed/schema: EmbedSourceSchema adds
  'calendar.events'; the filter shape gains optional `upcomingDays`
  (1-365) and `tagIds` (up to 16). Old filters (isFavorite/status/kind)
  remain — each source uses only its own subset
- website/embeds: resolveCalendarEvents gates hard on
  canEmbedOnWebsite(event.visibility ?? 'private'), joins each event
  to its LocalTimeBlock for the real start/end, applies the optional
  upcomingDays window and tag-id AND-filter, sorts upcoming-first with
  id as stable tiebreaker

Redaction is whitelist-per-design (plan §2): the inlined snapshot
carries only title, formatted date range, and location — NOT
description, reminders, tag labels, or the guest list. Fields that
typically hold private context stay out of the public blob regardless
of the visibility toggle.

Verified:
- pnpm check (web): 7450 files, 0 errors
- pnpm test calendar + website: 26/26
- pnpm run validate:all green

Next: M4.b — Todo, M4.c — Goals. Same pattern; split out because
goals lives under $lib/companion/goals/ with its own structure and
Todo has a complex view-column/filter surface that warrants its own PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 02:32:25 +02:00
Till JS
f20ace0358 test(website): broad automated coverage across the builder surface
83 new tests across 5 files — pure-logic, fast, run on every
push. Caught one real bug + motivated one small refactor.

Coverage:

- apps/mana/.../website/constants.test.ts (8): isValidSlug + RESERVED_SLUGS
  + isValidPath. Caught the 1-char-slug bug (regex allowed length 1;
  UI + plan say min 2). Fixed the regex in both the webapp and the
  mirrored server list.
- apps/mana/.../website/publish.test.ts extended (8 total): adds
  self-parent cycle, 3-level nesting, all-orphans, empty-input cases
  on top of the original determinism + orphan-drop tests.
- apps/mana/.../website/templates.test.ts (7): parameterised over each
  of the 4 bundled templates — clone produces fresh UUIDs, page +
  block counts match, navConfig populated. Plus unknown-template and
  duplicate-slug rejection. Container-nesting is punted to the smoke
  test (none of the bundled templates use columns yet).
- packages/website-blocks/src/schemas.test.ts (38): every block
  (11) + sanity-checks (defaults satisfy own schema, enum + length
  bounds, required fields). Pure Zod — no Svelte runtime needed.
- packages/website-blocks/src/themes/themes.test.ts (12): preset
  parity, resolveTheme overrides, themeCssVars output format +
  heading-font fallback.
- apps/api/src/modules/website/reserved-slugs.test.ts (10): mirror of
  the client tests for the server SSOT, plus new hostname validation
  cases (.mana.how reservation, length, malformed edges).

Refactor:

- apps/api/src/modules/website/reserved-slugs.ts now owns
  isValidHostname + RESERVED_HOSTNAMES. domains.ts imports them.
  Pure functions live next to the other pure validators; easier to
  test + share.

All 83 new tests green. Web-app svelte-check + apps/api type-check
both clean. Existing publish.test.ts / website-blocks tests still
pass (the monorepo-wide count is now well above 83 — these are
the new ones from this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 21:07:40 +02:00
Till JS
4fc9d6c59c feat(wardrobe): module foundation — garments + outfits space-scoped data layer (M1)
M1 of docs/plans/wardrobe-module.md — pure data layer + backend plumbing,
zero UI (that's M2). A user can now hold a digital wardrobe per space:
brand merch, club Trikots, family Kleiderschrank, team Kostüme, practice
Dresscode, and personal closet all live as separate pools under the same
Dexie tables, space-scoped like tags/scenes/agents after Phase 2c.

Data model — two tables, no join:

- wardrobeGarments (Dexie v41): single clothing items / accessories.
  Indexed on `category` + `createdAt` + `isArchived`. Encrypted:
  name/brand/color/size/material/tags/notes. Plaintext: category,
  mediaIds, counters, timestamps — all indexed or structural.
  `mediaIds[0]` is the primary photo used for try-on; additional
  ids are alternate views (back, detail) for M7.

- wardrobeOutfits (Dexie v41): named compositions referencing
  garment ids. Encrypted: name/description/tags. Plaintext:
  garmentIds (FK array), occasion (closed enum — useful for
  undecrypted filtering), season, booleans, lastTryOn snapshot.

- picture.images gains `wardrobeOutfitId?: string | null` as a
  plaintext back-reference. Try-on results land in the Picture
  gallery like any other generation; the outfit detail view
  queries them via this id rather than maintaining a third table.

Space scope:

- `wardrobe` added to all five explicit allowlists in shared-types/
  spaces.ts (personal is wildcard, no edit needed). Each space type
  gets a one-line comment explaining the real-world use case.
- App registry: `wardrobe` entry in shared-branding/mana-apps.ts
  with a rose→fuchsia gradient icon (T-shirt on hanger silhouette),
  color #e11d48, tier 'beta', status 'beta'.
- Module registry: wardrobeModuleConfig imported + appended to
  MODULE_CONFIGS so SYNC_APP_MAP picks it up automatically.

Backend:

- MAX_REFERENCE_IMAGES bumped 4 → 8 in picture/generate-with-
  reference (plus the client-side default in ReferenceImagePicker).
  Justified with a comment: face + body + top + bottom + shoes +
  outerwear + 2 accessories = 8. Cost doesn't scale with ref count
  (OpenAI bills per output), so the bump is a pure capability
  expansion with no credit-side risk.
- New POST /api/v1/wardrobe/garments/upload wraps uploadImageToMedia
  with app='wardrobe'. Registered under /api/v1/wardrobe in index.ts.
  Pattern 1:1 with the profile/me-images/upload endpoint; tier-gating
  falls out of wardrobe NOT being in RESOURCE_MODULES (tier='guest'
  works — consistent with picture's plain CRUD).

Stores emit domain events (WardrobeGarmentAdded, WardrobeOutfitCreated,
WardrobeOutfitTryOn, etc.) so later mana-ai missions can observe
activity without polling.

No UI in this commit. M2 (Garments-Grundlayer) wires the route + grid
+ upload-zone; M3 the Outfit composer; M4 the Try-On integration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:27:37 +02:00
Till JS
57be0f61b1 feat(website): M4 — forms + moduleEmbed
Adds two new block types and the server-side infrastructure for
untrusted input + cross-module data embedding.

Forms:
- packages/website-blocks/src/form: declarative fields (text, email,
  tel, url, textarea, number) with required / maxLength / placeholder
  per field. Honeypot hidden input in the renderer; public-mode POST
  to a same-origin SvelteKit proxy that forwards to mana-api.
- apps/api: website.submissions table (schema.ts + 0001_submissions.sql)
  + POST /public/submit/:siteSlug/:blockId. Loads the current published
  snapshot, finds the form block, validates payload against its
  declared fields (trim, type check, length cap), rejects honeypot
  submissions silently, rate-limits per IP (10 / 5 min) in-memory.
  Unknown keys are dropped — clients can only submit declared fields.
- Owner-facing: GET/DELETE /sites/:id/submissions + SubmissionsView
  component + /(app)/website/[siteId]/submissions route. Shows
  incoming submissions with status pill + payload preview + delete.
- apps/mana/.../routes/s/[siteSlug]/__submit/[blockId]/+server.ts:
  same-origin proxy so form posts don't trigger CORS and IP / user-
  agent headers are forwarded via SvelteKit's trusted getClientAddress.

M4 first-pass does NOT wire target-module delivery (contacts / notify).
Submissions stay in the inbox until owner-side tool handlers land
(M4.x). `target` enum is intentionally `['inbox']` only for now.

moduleEmbed:
- packages/website-blocks/src/moduleEmbed: source dropdown
  (picture.board | library.entries), max-items, layout (grid | list),
  optional filter object. The `resolved` field on props is populated at
  publish time by the editor-side resolver — public renderer reads it
  directly, no Dexie / API round-trip needed.
- apps/mana/.../website/embeds.ts: per-source resolvers. picture.board
  enforces `isPublic=true`; library.entries respects filter.isFavorite
  / kind / status so owners can expose a subset (e.g. "my favorites").
- buildSnapshot() walks the tree after assembly and fills in
  block.props.resolved for every moduleEmbed. Publish slower, public
  visits fast. No cross-service call at render time.

Validation:
- pnpm run validate:all: 6/6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api type-check: green

Apply Postgres with:
  psql "$DATABASE_URL" -f apps/api/drizzle/website/0001_submissions.sql

Plan: docs/plans/website-builder.md (M4 shipped)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:36:52 +02:00
Till JS
7a4f8894e1 feat(website): M3 — 5 more blocks, containers, upload, themes
Expands the builder from 3 M1 blocks to 8. Containers (columns) and
media blocks (image, gallery) are the structural additions; cta and faq
round out the content coverage.

packages/website-blocks:
- image, cta, faq, columns (container), gallery — each with Zod schema,
  renderer (mode-aware for edit/preview/public), and fallback inspector.
- Block type extended with optional `children` + `renderChild` snippet
  so containers render their children through the same chrome the
  outer renderer provides (click-to-select, public-path tagging).
- themes/: 3 presets (classic light, modern dark, warm) with
  `resolveTheme` + `themeCssVars` helpers. Public layout now emits
  CSS vars via `style=` on the root; block components read
  `var(--wb-primary)` / `var(--wb-bg)` / `var(--wb-fg)` / etc.
- Registry updated; new exports + `./themes` subpath export.

apps/mana/apps/web/src/lib/modules/website:
- upload.ts: multipart POST to mana-media with `app=website` scope,
  returns { mediaId, url }. 25 MB cap, non-image rejection client-side.
- components/ImageInspector + GalleryInspector: app-side overrides
  wired to upload. Registered via `CUSTOM_INSPECTORS` in BlockInspector
  so block.type → app-side inspector, fallback to registry otherwise.
- components/SiteSettingsDialog: theme preset picker + color overrides
  for primary/bg/fg + footer text. Mounted from a ⚙ button in the
  editor's left pane.
- components/BlockRenderer: rebuilt around a byParent map + recursive
  `renderBlock` snippet so container blocks can render their children
  through the same click-to-select wrapper as top-level blocks.
- routes/s/[siteSlug]: rename `[[...path]]` → `[...path]` (SvelteKit
  treats rest segments as optional automatically — double-bracket form
  errored at sync time). +page.svelte renders snapshot trees
  recursively so published pages match the editor.

apps/api: unchanged.

Validation:
- pnpm run validate:all: all 6 gates green
- pnpm run check (web): 0 errors, 0 warnings
- apps/api type-check: green
- website-blocks tsc: green

Plan: docs/plans/website-builder.md (M3 block shipped)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:27:49 +02:00
Till JS
54a12ffd5c feat(webapp): wire isParallelSafe in Companion chat + Mission runner
Enables the M1 parallel-reads optimisation on the webapp side. Both
consumers of runPlannerLoop pass an isParallelSafe predicate derived
from the tool catalog:

  isParallelSafe: (name) =>
    AI_TOOL_CATALOG_BY_NAME.get(name)?.defaultPolicy === 'auto'

Auto-policy tools (list_tasks, get_habits, nutrition_summary, …) run
via Promise.all in batches of 10 when the LLM fans them out in one
round. Propose-policy tools — which surface to the user as Proposal
cards — stay sequential so intent ordering in the inbox is preserved
and pre-execute guardrails can reason about prior-step state.

Tests: 31 existing companion + mission tests pass unchanged; the
parallel path is exercised via the new loop.test.ts cases shipped
with the M1 commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:11:24 +02:00