- 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>
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>
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>
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>
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>
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>
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>
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>