Skip to content

Goal editor UI: pin per-goal Daily Driver feature areas (featureAreas override)#2813

Merged
atomantic merged 5 commits into
mainfrom
next/issue-2679
Jul 19, 2026
Merged

Goal editor UI: pin per-goal Daily Driver feature areas (featureAreas override)#2813
atomantic merged 5 commits into
mainfrom
next/issue-2679

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Adds the missing UI for the per-goal featureAreas override introduced in #2666. A goal can now pin exactly which PortOS feature areas the Daily Driver deep-links to, instead of always using the category default.

  • New "Daily Driver Feature Areas" multi-select in GoalEditForm, populated from FEATURE_AREAS/FEATURE_AREA_IDS (icon + label per area), styled as a toggle-button group mirroring the tag editor.
  • Initialized in useGoalDetail.startEdit() from goal.featureAreas; persists through the existing updateGoal path.
  • When no locally-known area is selected, the editor shows the greyed category default the Daily Driver falls back to (gated on known-selection, so an override of only forward-unknown ids still displays the real fallback).
  • Accessible group: role="group" + aria-labelledby, each toggle a <button aria-pressed> with a visible text label.

Federation forward-compat

Goals sync between peers as whole records, so an older install can hold a goal carrying a feature-area id a newer peer introduced. This PR keeps that safe:

  • Update schema is lenient (z.array(z.string())) so editing such a goal never 400s; create schema stays strict (z.enum(FEATURE_AREA_IDS)) so a new goal authored here still rejects typos. getGoalFeatureAreas filters unknown ids at read time.
  • saveEdit omits featureAreas when the selector was untouched (avoids a stale whole-record snapshot clobbering a concurrent peer edit via LWW) and preserves forward-unknown ids when it was changed (never truncates and LWW-erases a newer peer's config).

Test plan

  • clientGoalDetailPanel.test.jsx: round-trips the override; empty selection falls back to the category default; initializes from goal.featureAreas; omits featureAreas when untouched; preserves forward-unknown ids when changed; shows the category-default hint for an unknown-only override. (16 pass)
  • serveridentityValidation.test.js: create rejects unknown ids (strict), update accepts forward-unknown ids (lenient) and rejects non-string entries. (53 pass)
  • client build + lint clean.
  • Reviewed with claude, ollama, and codex (series); all findings resolved.

Closes #2679

…ct to goal editor

Issue #2666 added a per-goal featureAreas override that pins which PortOS
feature areas the Daily Driver deep-links to, but there was no UI to set it.

Add a multi-select control to GoalEditForm populated from FEATURE_AREAS /
FEATURE_AREA_IDS (label + icon per area), mirroring the tag editor. It's
initialized in useGoalDetail.startEdit() from goal.featureAreas and persists
through the existing update path. When no override is selected the control
shows the category default (greyed) so the user sees which areas the Daily
Driver falls back to. An empty selection saves featureAreas: [], which
getGoalFeatureAreas treats as no-override → category default.
…reAreas ids

On a version-skewed federated install, a goal synced from a newer peer can
carry a featureAreas id this install's FEATURE_AREA_IDS enum doesn't know yet.
Because startEdit now always copies featureAreas into the form and saveEdit
always sent it, updateGoalInputSchema's strict enum would reject Save with a
400 — even when the user only changed an unrelated field. Before this feature
the editor omitted featureAreas entirely and the service preserved the stored
value.

Fix: only include featureAreas in the update payload when the user actually
changed the override; when unchanged, omit it so the service preserves the
stored value (forward-unknown ids included). On the changed path, strip to
locally-known ids so the update always validates.
…tead of stripping them

The prior fix stripped ids this install doesn't know before sending an edit.
That is destructive across federation: toggling any visible area truncated the
stored array, and because updateGoal advances updatedAt, the LWW merge in
dataSync.js would propagate the truncated record back and permanently erase a
newer peer's feature-area config (P1).

Root cause was the strict enum on the edit path fighting records that sync
already stores. Fix end-to-end instead:
- Server (identityValidation.js): featureAreasSchema accepts any non-empty
  string id with a generous cap, not z.enum(FEATURE_AREA_IDS). Sync stores
  whole records bypassing route validation anyway; getGoalFeatureAreas already
  filters unknown ids at read time, so unknown ids are preserved in storage and
  ignored when resolving areas.
- Client (useGoalDetail): send the full featureAreas array, unknown ids
  included — they ride along untouched through the known-id-only multi-select.
- Client (GoalEditForm): gate the greyed category-default hint on whether any
  LOCALLY-KNOWN area is selected, not raw array length, so an override of only
  forward-unknown ids still shows the fallback the Daily Driver actually uses (P2).
…t untouched override on save

Two fixes from a third review pass:

- Keep create validation strict, relax only update. A brand-new goal is
  authored on this install, so an unknown feature-area id is a typo/bug worth
  rejecting; only the update path needs to tolerate forward-unknown ids synced
  from a newer peer. Split featureAreasSchema into a strict create variant
  (z.enum) and a lenient update variant (z.string), instead of weakening both.

- Don't re-send an untouched featureAreas snapshot. The editor sends a startEdit
  snapshot of every field; re-sending featureAreas the user never touched would
  let that stale value win the whole-record LWW merge and clobber a concurrent
  featureAreas edit from a federated peer. saveEdit now omits featureAreas unless
  the selector actually changed (restoring the pre-#2679 immunity for the
  untouched case), and preserves forward-unknown ids when it did change.

Adds identityValidation tests pinning create-strict vs update-lenient.
@atomantic
atomantic merged commit 282b63b into main Jul 19, 2026
2 checks passed
@atomantic
atomantic deleted the next/issue-2679 branch July 19, 2026 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Goal editor UI: pin per-goal Daily Driver feature areas (featureAreas override)

1 participant