Goal editor UI: pin per-goal Daily Driver feature areas (featureAreas override)#2813
Merged
Conversation
…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
force-pushed
the
next/issue-2679
branch
from
July 19, 2026 22:00
7effd99 to
f1bfbf9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the missing UI for the per-goal
featureAreasoverride 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.GoalEditForm, populated fromFEATURE_AREAS/FEATURE_AREA_IDS(icon + label per area), styled as a toggle-button group mirroring the tag editor.useGoalDetail.startEdit()fromgoal.featureAreas; persists through the existingupdateGoalpath.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:
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.getGoalFeatureAreasfilters unknown ids at read time.saveEditomitsfeatureAreaswhen 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
client—GoalDetailPanel.test.jsx: round-trips the override; empty selection falls back to the category default; initializes fromgoal.featureAreas; omitsfeatureAreaswhen untouched; preserves forward-unknown ids when changed; shows the category-default hint for an unknown-only override. (16 pass)server—identityValidation.test.js: create rejects unknown ids (strict), update accepts forward-unknown ids (lenient) and rejects non-string entries. (53 pass)clientbuild + lint clean.Closes #2679