mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 10:03:29 +00:00
f222131dfe488caa77b10a653fae2bece6fc3a2a
168 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f222131dfe |
fix(ui): always show faint hover-only sidebar + buttons and card stars (#141)
* fix(ui): always show faint hover-only controls The sidebar + buttons and item-card star buttons were fully hidden until hover, which wasn't discoverable. They're already muted enough that showing them at reduced opacity is fine, and they still pop to full opacity on hover. - Sidebar .section-add-btn: opacity 0 -> 0.5 - Sidebar .nav-quick-add: visibility:hidden -> opacity 0.5 - ItemCard .star-btn: opacity 0 -> 0.4 (unstarred outline ☆ now visible) Refs IDEA-605 * fix(ui): bump unstarred star opacity 0.4 -> 0.65 for mobile readability |
||
|
|
9e7daa779f |
feat: tie done-detection to the board group-by field (TASK-604) (#140)
* feat: tie done-detection to the board group-by field
Closes TASK-604. Make "is this item done?" follow the collection's
settings.board_group_by rather than the hardcoded `status` key. If a
collection's board is grouped by `resolution`, then resolution's
terminal options drive dashboard counts, progress bars, changelog,
and starred-items filtering. Collections without an explicit
board_group_by (every collection today) continue to behave exactly
as before because the fallback resolves to `"status"`.
Why this shape
- No ambiguity: one field per collection wins. No reconciling
"status says in-progress, resolution says fixed."
- One JSON path to swap: every $.status query becomes
$.<done_field>. No dynamic OR across schema-discovered fields.
- Matches the mental model: the field you organize the board by is
the field that represents the item's current state. The old
mismatch (board grouped by X, "done" count from status) is a
latent bug this resolves.
- Non-breaking: board_group_by defaults to nil → DoneFieldKey
returns "status" → behavior identical to pre-TASK-604.
Model layer (internal/models/terminal.go)
- DoneFieldKey(schema, settings) resolves the done-field key with a
fallback chain: valid select on schema → that field, else "status".
- TerminalValuesForDoneField(schema, settings) returns (fieldKey,
values) honoring the done field, falling back to
DefaultTerminalStatuses when the resolved field has no
terminal_options.
- TerminalPlaceholdersForDoneField(schema, settings) is the SQL
convenience returning (fieldKey, placeholders, args).
- IsTerminalItem(fields, schema, settings) is the canonical
Go-side membership check.
- Legacy API (TerminalStatusesFromSchema, IsTerminalStatus,
TerminalStatusPlaceholders) kept as back-compat wrappers that
delegate with empty settings — resolve to "status" for callers
that don't have settings in scope yet.
SQL callers migrated to the new helpers
- internal/store/collections.go ListCollections active-count query
- internal/store/items.go GetItemProgress + GetAllItemProgress:
- New collectionDoneFilter type + childrenDoneFiltersFor{Parent,
Collection} + doneFiltersForWorkspace helpers load each
candidate collection's (schema, settings) and resolve per-
collection done keys + terminals.
- buildChildrenDoneExpr(filters, alias) compiles filters into a
single SQL boolean expression using per-collection OR clauses:
((alias.collection_id=? AND LOWER(...)
IN (?,?)) OR (alias.collection_id=? AND LOWER(...)
IN (?,?)) ...)
- Each child item is evaluated against its own collection's
done rules, so mixed-collection child progress is correct
without a global union hack.
- internal/store/agent_roles.go GetRoleBreakdown + Go-side filter
- internal/store/item_stars.go starred-items filtering now uses a
collectionDoneContext map (schema + settings) and IsTerminalItem.
Go-side callers migrated
- internal/server/handlers_dashboard.go: buildSchemaMap →
buildDoneContextMap (carries settings), isItemTerminal →
isItemDone (evaluates against the done field). 7 call sites
updated.
- internal/server/handlers_items.go: plan-progress recompute and
per-item /progress endpoint now use the done-context approach.
Left status-specific (per task scope)
- Link-payload $.status extracts in items.go getItemLink /
GetItemLinks / GetParentForItem — these populate
link.SourceStatus / link.TargetStatus, which are status-specific
by design.
- cmd/pad reconcile paths — no schema in scope, default-list
fallback is the right call.
- search.go facet "status breakdown" — a different UX concept
(bucket search results by status values) than done-detection.
Web UI reactivity
- FieldEditor: new activeDoneField prop. Each modal derives it from
boardGroupBy with the same fallback rule as the Go DoneFieldKey.
- Fields tab: the "Done?" column header on each select field renders
an "Active" green pill when that field is the board group-by, or a
muted "Saved" pill + inline hint otherwise ("Switch the board
group-by to <key> to make them drive done-detection"). Reactive to
boardGroupBy changes in the Display tab.
- DisplaySettingsEditor: "Board group by" label gets a helper line
explaining the new responsibility.
Tests
- internal/models/terminal_test.go: 13 unit tests covering fallback
resolution, placeholder args, membership (case-insensitive), and
back-compat shim semantics.
- internal/store/done_field_test.go: 3 integration tests:
1. Bugs collection grouped by resolution → items with terminal
resolution values count as done; items with status=fixed but
resolution=open do NOT count as done (proves status is no
longer consulted when it isn't the done field).
2. Collection without board_group_by still uses status terminals.
3. Mixed-collection children: each child evaluated against its
own done rules.
All pass alongside the full existing suite.
* fix: restrict done field to select (reject multi_select)
Two linked Codex P1 findings on PR #140, both rooted in the same
gap: multi_select fields store their values as JSON arrays, but both
the Go-side membership check (IsTerminalItem) and the SQL done
expression (buildChildrenDoneExpr) assume a scalar string. Naively
accepting multi_select as a done field would silently miss items
whose terminal value is one of several in the array — dashboards
and progress would report wrong counts.
Rather than implement array-containment semantics across both
paths (which would require deciding "any terminal value → done" vs
"all terminal values → done", SQL-dialect-aware JSON-contains, and
new tests for both shapes), close the gap with a constraint: only
select fields qualify as a done field. If array semantics become
a requirement later, that's a focused follow-up that can update
both paths together with a clear definition.
Changes
- DoneFieldKey and TerminalValuesForDoneField: loop bodies now
match only `select`, not `select || multi_select`. A
board_group_by pointing at a multi_select field falls back to
'status' — matching the rule for non-existent or non-select
fields.
- IsTerminalItem: docstring made the scalar contract explicit;
non-string values (which would be the multi_select array shape)
already returned false, which is now the deliberate behavior.
- buildChildrenDoneExpr: added a doc note that the scalar
JSON_EXTRACT path is correct because the upstream resolution
only hands us select fields.
- Web UI: EditCollectionModal + CreateCollectionModal derive
activeDoneField matching the backend rule (select only), and
FieldEditor.isActiveDoneField gates on field.type === 'select'.
A multi_select field never lights up the green "Active" pill now,
even if a user somehow pointed board_group_by at one.
Tests
- Replaced TestDoneFieldKey_AcceptsMultiSelect with
TestDoneFieldKey_RejectsMultiSelect. Asserts that a multi_select
board_group_by falls back to 'status' instead of being honored.
- Existing 12 unit tests + 3 integration tests all still pass.
* fix: include soft-deleted collections in done-filter loaders
Two related Codex P2s on PR #140. The done-filter loaders were
limiting their SELECT to collections with deleted_at IS NULL, but
the outer callers (GetItemProgress, GetAllItemProgress,
GetRoleBreakdown) count items regardless of their collection's
deleted_at. Net effect: after a collection was soft-deleted, its
items lost their per-collection clause in buildChildrenDoneExpr and
were always evaluated as non-terminal — undercounting done in plan
progress and inflating active counts in the role breakdown.
Fix
Drop the `c.deleted_at IS NULL` guard from all three filter
loaders:
- childrenDoneFiltersForParent
- childrenDoneFiltersForCollection
- doneFiltersForWorkspace
Soft-deleted collections still have valid schema + settings rows in
the DB, so the done rules remain applicable until a hard delete
cascades. This also matches what the outer queries count: if they
include items from a soft-deleted collection, the filter loaders
must too.
Regression test
TestGetItemProgress_HonorsSoftDeletedChildCollections:
1. Create a parent + two children in a child collection where one
child is done and one is open — assert done=1.
2. DeleteCollection on the child collection (soft-delete).
3. Re-run GetItemProgress — assert done is still 1, not 0.
Fails before the filter-loader fix, passes after.
* fix: avoid N+1 in plans progress + preserve done fallback on bad schemas
Two Codex P2s on PR #140.
P2: Avoid N+1 list-collection queries in plans progress
handlePlansProgress's restricted path was calling s.store.
ListCollections solely to build a ctxMap, but ListCollections runs a
separate active-item COUNT query per collection (collections.go),
burning O(number of collections) round-trips on every call. In
larger workspaces this materially inflates latency and can cause
timeouts. Add a lightweight Store.ListCollectionsMinimal that
returns only the ID / Schema / Settings needed for done-context
construction and skips the count queries entirely. Handler switches
to it.
P2: Preserve done fallback for unparseable collection schemas
scanCollectionDoneFilters was `continue`-ing past collections whose
schema failed to parse. Because buildChildrenDoneExpr composes a
per-collection OR clause and only applies the default-list fallback
when NO filters are constructed overall, a single malformed
collection could leave its items without a matching clause —
silently marking them as perpetually active in progress / role /
starred queries. Emit a fallback filter (status + DefaultTerminal-
Statuses) for that collection instead of skipping it, matching
pre-TASK-604 behavior for its items while still honoring the
configured rules for every other collection.
* fix: sanitize done-field keys + cover granted-item collections
Two more Codex findings on PR #140.
P1: Sanitize done-field keys before embedding SQL JSON paths
buildChildrenDoneExpr passes the resolved done-field key straight
into JSONExtractText, whose dialect implementations interpolate it
as a string literal inside `json_extract(..., '$.<key>')` /
`-->>'<key>'`. Schema / settings rows are persisted without backend-
side key validation, so a crafted board_group_by (e.g. a key with
quotes, semicolons, or SQL metacharacters) could break the
resulting query or inject. Since TASK-604 made done-field
resolution dynamic, this needs a chokepoint.
Fix: DoneFieldKey now refuses to resolve to any candidate that
doesn't match ^[a-zA-Z][a-zA-Z0-9_]*$ and falls back to the literal
"status" (which is always safe). The pattern matches the convention
already in use for search-field filtering in internal/server/
handlers_search.go.
Added TestDoneFieldKey_RejectsUnsafeKeys covering injection-shaped
strings, dots, dashes, leading digits, empty strings, and spaces.
P2: Include granted-item collections in dashboard done context
The dashboard was filtering `collections` by visibility BEFORE
building ctxMap, but allItems can still include items from
collections outside the visibility set via item-level grants
(dashItemIDs). Those items missed their own done-rules and
fell back to the status-default, misclassifying them for guests
with item-level grants in collections that use a non-status done
field.
Fix: build ctxMap from ListCollectionsMinimal(workspaceID) first —
always covering every collection in the workspace — then apply
visibility filtering to `collections` for the summary section only.
isItemDone now sees the real done rules for every item the
dashboard iterates, regardless of how visibility surfaced it.
* fix(web): mirror backend safe-key check in activeDoneField derivation
Codex P2 on PR #140. The previous commit added a safe-key regex on
the backend (DoneFieldKey rejects keys outside ^[a-zA-Z][a-zA-Z0-9_]*$
and falls back to "status"), but the Web activeDoneField derivation
in both modals only checked type === 'select'. For legacy / API-
created schemas carrying keys like `resolution-v2` or `foo.bar`, the
Fields tab would display an "Active" green pill on that field even
though the server silently ignores it and falls back to status. Users
could configure terminal options on the wrong field and never see
them take effect.
Fix: export isSafeDoneFieldKey from field-editor-types.ts (a tiny
helper wrapping the same regex the backend uses) and gate both
modals' activeDoneField derivations on it. Unsafe keys fall back to
'status' in the UI, matching the backend's behavior exactly —
Active/Saved pills are now truthful.
|
||
|
|
bafb3c2be5 |
feat(web): create-time Display/Quick Actions + live prompt preview (TASK-599) (#139)
* feat(web): create-time Display/Quick Actions + live prompt preview
Closes TASK-599 in PLAN-593 — the last task.
Closes the parity gap between Create and Edit modals by bringing the
Display and Quick Actions editors to the Create flow (under an
"Advanced" reveal so the default create path stays short), and adds a
live substitution preview to the Quick Actions prompt editor in both
modals.
New shared code
- web/src/lib/utils/quick-action-preview.ts: single source of truth
for the template-variable list, kept in lockstep with the runtime
substitution in QuickActionsMenu. Exports parsePrompt() that
tokenizes a prompt into text / known-var / unknown-var segments,
plus contextFromItem() (real items for Edit) and
placeholderContext() (synthetic for Create or empty collections).
- DisplaySettingsEditor.svelte: extracts the 5 display selects
(default view, layout, board/list group-by, list sort-by) into a
reusable pure-presentation block with bindable props.
- QuickActionsEditor.svelte: extracts the full Quick Actions sub-UI
(both Item and Collection sections) with add/remove/reorder logic
internal to the component. Each action card now renders a live
preview panel below the prompt input showing the resolved output
with subtle blue highlights on known variables and red + wavy
underline on unknown ones. An explicit warning line appears below
the preview when typos are detected.
EditCollectionModal
- Replaces the inline Display tab markup with DisplaySettingsEditor.
- Replaces the inline Quick Actions tab markup with QuickActionsEditor.
- Fetches the first item in the collection on open
(api.items.listByCollection limit=1) to build a realistic preview
context; falls back to placeholder values if the collection is
empty or the fetch fails.
- Net result: ~390 lines removed (deduped into the components), local
state for action list and group-by derivation remains here since it
drives the schema save.
CreateCollectionModal
- New collapsible "Advanced" section below the fields area, collapsed
by default. Contains DisplaySettingsEditor + QuickActionsEditor.
- New state for default_view / layout / board_group_by / list_group_by
/ list_sort_by / quick_actions, wired into handleCreate's settings
serialization.
- Template selection now pre-fills the Advanced state from the
template's settings (board_group_by, default_view, quick_actions
etc.), so template-provided settings are preserved even for users
who never open the Advanced section.
- Derived selectFieldKeys / sortableFieldKeys from the (not-yet-saved)
fields so the group-by pickers reflect what the user is building.
- A small $effect auto-corrects boardGroupBy / listGroupBy when the
user removes the select field they pointed at (Advanced only —
doesn't mutate state behind the user's back while collapsed).
- Preview context uses placeholderContext() since no items exist yet;
the {collection} token updates live as the user types a name.
Out of scope
- Cross-field done-detection (separate, tracked in TASK-604).
- Any new field types / schema additions.
* fix(web): scope-aware previews and honest empty-resolution rendering
Two Codex findings on PR #139, both about preview accuracy:
P2: Use scope-aware context for collection action previews
Collection-scope actions run with `item` unset in QuickActionsMenu,
so item-only variables ({ref}, {title}, {status}, {priority},
{content}, {fields}, {plan}, {phase}) resolve to empty strings at
runtime. The preview was parsing collection-scope prompts with the
same item-populated context used for item-scope actions, so the
preview could show rich substitutions the user would never actually
get when clicking the action.
Fix: add toCollectionScope() in quick-action-preview.ts that clears
item-only variables and keeps only {collection}. QuickActionsEditor
now derives itemScopeContext (verbatim) and collectionScopeContext
(reshaped), and the two sections parse against the right one.
P2: Render empty resolved variables as empty in preview
The preview template `{seg.resolved || `{${seg.name}}`}` treated
legit empty substitutions as falsy and fell through to the raw
token, so a known variable that legitimately resolves to `""` at
runtime (e.g. {plan} with no plan, or any item variable in a
collection-scope action) was displayed as if the token would be
copied literally. That's the opposite of what runtime actually
does.
Fix: when seg.resolved === '', render an italic muted "(empty)"
pill with a tooltip explaining the variable resolves to an empty
string. Non-empty resolutions render unchanged. This surfaces the
emptiness to the user without lying about what gets copied.
Both fixes pair with the scope-aware context change — collection-
scope previews now correctly show all item variables as "(empty)"
instead of rich values, matching runtime output exactly.
* fix(web): drop template quick_actions from spread so user can clear them
Codex P2 (PR #139): the Create modal merged `selectedSettings` into
the final settings object and only wrote `quick_actions` when
`savedActions.length > 0`. After picking a template with pre-shipped
quick actions, a user who deleted every quick-action row would still
end up saving the template's original quick_actions because they were
re-introduced by `...selectedSettings`. "Remove all quick actions"
was effectively impossible for templates that defined them.
Fix: destructure `quick_actions` out of `selectedSettings` before the
spread, leaving only the non-action template fields (default_view,
board_group_by, etc.) to be merged. `quickActions` state is already
the single source of truth for quick actions — it's populated from
the template on pick and then edited by the user — so the spread no
longer needs to contribute them. This makes `savedActions` ← the
in-editor list authoritative, including when it's empty.
|
||
|
|
6d5fa969e2 |
feat(web): visual redesign pass across collection modals (#138)
Closes TASK-598 in PLAN-593. Applies the design doc recorded on the task before implementation. The work: 1. Field card alignment (resolves the T2 regression) Key row moved out of the header flex into a full-width block below. Header is now baseline-aligned at a consistent height: drag handle, label input, type-select, remove button all sit on one row regardless of whether a key row is present. Label-and- key feel related without being cramped. 2. Emoji picker parity Both modals' General tabs now render EmojiPickerButton (size md) next to the name input, matching the Quick Actions pattern. The inline .icon-btn + <EmojiPicker> dance, its showEmojiPicker state, and all supporting CSS are deleted. 3. Danger zone Archive moved out of the footer into a dedicated "Danger zone" section at the bottom of the General tab. Red-tinted background, red section header, red destructive button that fills on hover. Confirmation flow lives inside the section (not crammed into the footer). Footer is now Cancel + Save Changes only. 4. Empty states Fields empty state gets icon + title + description (not a bare string). Quick Actions empty states explain what item / collection actions are for, inside a dashed-bordered suggestion block. 5. Template picker Blank card shares the same structure as other templates — a muted circular + icon wrapper instead of a dashed outline. All cards have a consistent min-height so Blank doesn't look stubby. Added :focus-visible outline for keyboard users. 6. Typography rhythm Section labels normalized to the app convention: 0.75em, 600, uppercase, 0.05em tracking, --text-muted. Applied to .fields-label, .form-label, .actions-section-title. 7. Responsive Both modals: 16px overlay padding, scrollable content, full- width under 640px. Edit modal tab bar scrolls horizontally with a right-edge fade mask under 640px; settings grid collapses to one column; footer buttons fill width. 8. Motion Modal fade + subtle scale-in (160ms ease-out). Respects prefers-reduced-motion. Existing tab/chevron transitions kept. Out of scope (per plan): new features (T6), backend changes, schema shape changes. |
||
|
|
26d124f19d |
feat(web): allow terminal toggle on any select/multi_select field (TASK-597) (#137)
* feat(web): allow terminal-option toggle on any select/multi_select field
Closes TASK-597 in PLAN-593 (scope A — UI + persistence).
FieldEditor previously gated the "Done?" column and per-option
terminal toggle on field.key === 'status', matching the pre-T4
behavior exactly. This commit lifts that gate so any select or
multi_select field with at least one option exposes the toggle, and
updates all three save paths (CreateCollectionModal, Edit addedFields,
Edit existingFields) to persist terminal_options for any
select-typed field instead of only status.
Changes
- FieldEditor.svelte: showsTerminalColumn now derives from
isSelectType && options.length > 0. Removed the inner
{#if field.key === 'status'} around both the option-done-toggle
button and the option-terminal class:directive. Extended the
column-header title to explain what terminal means (dashboard
filtering, progress bars, changelog) instead of the prior
status-specific wording.
- CreateCollectionModal / EditCollectionModal save paths: replace
the key === 'status' gate with a select/multi_select type check.
Stale terminal values are still filtered to the saved options set
so renames/removals don't leave orphan terminal pointers.
Scope note: the backend's done-detection (dashboards, progress
bars, search filters, changelog) remains status-centric. Marking
terminal options on a non-status field persists the schema but
doesn't yet affect aggregation — that architectural change is
tracked separately in TASK-604 (follow-up). This scope-A ship
satisfies PLAN-593's "surface, don't hide" principle by making the
UI match the data model without coupling it to the bigger backend
refactor.
Manual smoke test
- Create a "resolution" select field; mark fixed/wontfix/duplicate
as terminal; save. Reopen collection — terminal markings round
trip correctly.
- Existing status field behavior unchanged: terminal toggles still
render, apply, and persist.
* fix(web): align terminal tooltip with status-only backend semantics
Codex P2 (PR #137): the prior tooltip claimed terminal options on
any select field drive dashboard filtering / progress bars /
changelog, but the backend still only reads terminal_options from
the status field (internal/models/terminal.go,
TerminalStatusesFromSchema). That copy misled users into making
configurations that silently do nothing.
Rewrite the tooltip to be honest about current semantics: only the
status field drives aggregation today; markings on other fields are
persisted on the schema for API consumers and for the future
cross-field done-detection work tracked in TASK-604.
|
||
|
|
3463c83bf7 |
feat(web): contextual browser-tab titles (IDEA-592) (#136)
* feat(web): add page title store and wire root layout (TASK-602)
Foundation for contextual browser-tab titles (IDEA-592 / PLAN-601).
Introduces a centralized rune store at web/src/lib/stores/title.svelte.ts
that composes titles as `{item|section} · {workspace} · Pad` with the most
specific label first (browsers truncate from the right). The root layout's
<svelte:head> renders `<title>{titleStore.title}</title>` reactively.
The store exposes `setPageTitle({ workspace?, section?, item? })` with
per-key merge semantics: omitted keys preserve, `null` clears, strings set.
This lets a layout set the workspace once while leaf pages contribute only
their own section or item ref without clobbering context.
With no route wired yet (TASK-603), all pages continue to render `Pad` —
identical behavior to before, now served via the store.
OG meta tags are unchanged on purpose; this only affects the browser tab.
* feat(web): wire contextual titles for big-four routes (TASK-603)
Completes contextual browser-tab titles for IDEA-592 / PLAN-601.
Each workspace-area route now calls `titleStore.setPageTitle(...)` from
a `$effect` to contribute its slice of context:
- `[username]/[workspace]/+layout.svelte` — sets `workspace` from
`workspaceStore.current?.name`; clears on destroy so leaving the
workspace area resets the tab to bare `Pad`.
- `[username]/[workspace]/+page.svelte` (workspace home) — clears
section/item so only the layout-owned workspace name shows.
- `[username]/[workspace]/[collection]/+page.svelte` — section from
the loaded collection's display name.
- `[username]/[workspace]/[collection]/[slug]/+page.svelte` — item
from `formatItemRef(item)` (e.g. `IDEA-592`); section cleared so
the format reads `{REF} · {Workspace} · Pad` (the ref prefix
already encodes the collection).
- `[username]/[workspace]/activity/+page.svelte` — static section
`Activity`; removes the old ad-hoc `<svelte:head><title>` block
that conflicted with the store-driven root <title>.
Results:
- `/` → `Pad`
- `/{user}/{ws}` → `{Workspace} · Pad`
- `/{user}/{ws}/{collection}` → `{Collection} · {Workspace} · Pad`
- `/{user}/{ws}/{collection}/{ref}` → `{REF} · {Workspace} · Pad`
- `/{user}/{ws}/activity` → `Activity · {Workspace} · Pad`
Niche routes (settings, roles, console, billing) are unchanged and
continue to fall back to `Pad` — they can migrate to the store
incrementally.
* fix(web): clear stale title parts on route change in workspace layout
Addresses Codex P1 on PR #136: `setPageTitle` preserves omitted keys by
design, so navigating from a wired route (item detail, collection list,
activity) to an unwired route (settings, roles, dashboard, library,
playbooks, conventions) left the previous section/item in the tab title.
The workspace layout's title effect now reads `page.url.pathname` so it
re-runs on every SPA navigation and clears `section`/`item` alongside
the `workspace` set. Leaf pages that want to contribute their own parts
continue to do so in their own `$effect`s, which run after this one per
Svelte 5's parent-before-child effect ordering. Unwired routes inherit
the cleared state and correctly fall back to `{Workspace} · Pad`.
* fix(web): re-run activity title effect on pathname change
Addresses Codex P2 on PR #136. The activity page's title `$effect` set
`section: 'Activity'` with no reactive dependencies, so it only fired on
first mount. Because SvelteKit reuses the page component when navigating
between `/{user}/{ws1}/activity` and `/{user}/{ws2}/activity`, and the
workspace layout now clears `section` on every pathname change, the tab
title dropped to `{Workspace} · Pad` after cross-workspace navigation
until a full remount.
Reading `page.url.pathname` at the top of the effect gives it a dep that
changes on every SPA navigation, so the activity section is re-asserted
after the layout's clear.
The other wired leaf pages (workspace home, collection list, item detail)
are not affected: the home page sets only nulls (matches the layout's
clear), and the collection/item effects already depend on reactive state
(`collection?.name`, `formatItemRef(item)`) that gets refreshed on
navigation.
* fix(web): split workspace-name sync from section/item clear in layout
Addresses Codex P1 on PR #136 (third round). The previous combined
effect in the workspace layout depended on both `page.url.pathname` and
`workspaceStore.current?.name`, so every async resolution of the
workspace name would clear `section`/`item` in addition to updating
`workspace`. If a leaf page (e.g. activity) had already set its section
before the workspace resolved, the layout's rerun would wipe it.
Splitting the single effect into two:
1. Workspace-name sync — depends only on `workspaceStore.current`. Only
touches the `workspace` slot. Safe to fire asynchronously after the
leaf has set its context.
2. Route-change clear — depends only on `page.url.pathname`. Fires
exactly once per SPA navigation, clearing `section`/`item`. Leaf
`$effect`s run after (parent-before-child ordering) and re-assert
their parts.
Unwired routes still correctly fall back to `{Workspace} · Pad`, and
the activity page retains `Activity · {Workspace} · Pad` after the
workspace-name resolution.
|
||
|
|
5f7b7af50f |
feat(web): surface required/default/suffix/relation field controls (TASK-596) (#135)
* feat(web): surface required/default/suffix/relation field controls
Closes TASK-596 in PLAN-593.
Expose field capabilities that already round-tripped through
EditableField but had no UI. Controls live in a collapsible Advanced
section on each field card.
FieldEditor
- New exported CollectionOption type for the relation picker input.
- Advanced section (collapsed by default, auto-expanded when any of
required/default/suffix/collection is already set) containing:
* Required checkbox (all types)
* Default value — type-appropriate input:
text/url -> text input
number -> number input (+ Suffix row below it)
date -> date picker
checkbox -> "Checked by default" toggle
select -> dropdown restricted to the field's options
multi_select / relation -> deliberately skipped
* Relates to dropdown (relation type only), populated from the
workspace collections list passed in via props. Shows a helpful
empty-state when no other collections exist.
- Computed fields render a muted "computed" badge and the advanced
inputs are disabled (changing defaults / suffix / required on a
computed field is nonsensical). Label / type / remove remain
editable to preserve current behavior.
- Typed input handlers coerce field.default into the right shape
(string / number / boolean) so the polymorphic value stays clean.
CreateCollectionModal + EditCollectionModal
- Both fetch api.collections.list(ws) lazily on open and pass the
result down to every FieldEditor as `collections`.
- Both new-field save paths now emit required / computed / suffix /
collection / default onto the serialized FieldDef. Existing-field
save path in EditCollectionModal already handled these; this brings
the new-field path to parity and adds equivalent handling in the
Create modal.
Behavior notes
- Values round-trip: set in Advanced -> save -> reopen -> still there.
- Emit-when-set keeps payloads compact and compatible with existing
schemas that don't carry these fields.
- Known visual quirk: the taller card may exacerbate the type-select
alignment already tracked on TASK-598; deferred to the visual pass.
* fix(web): gate advanced field properties by current field type
Codex P2 (PR #135): the save paths emitted `suffix`, `collection`,
and `default` for every new field regardless of f.type, so a user
could set a number default/suffix, switch the field to `relation` or
`multi_select`, and still persist the hidden value — producing schema
defaults that don't match the final type and are then auto-applied
to new items by ValidateFields.
Fix: gate type-specific advanced-value emission by the current type
at save time. This keeps the user's in-memory state intact (no
surprise clears on type toggle) but prevents stale values from
leaking into the saved schema.
- suffix: only when type === 'number'
- collection (relation target): only when type === 'relation'
- default: only when typeSupportsDefault(type) returns true
Apply the gating in all three save paths:
- CreateCollectionModal.handleCreate (new fields)
- EditCollectionModal.handleSave addedFields (new fields)
- EditCollectionModal.handleSave updatedExisting (existing fields) —
same pre-existing risk if the user changes an existing field's
type and hits save
Extract the default-support check into typeSupportsDefault() in
field-editor-types.ts so FieldEditor (which gates the rendered
default input) and the save paths share one predicate. Adjust
FieldEditor's local `supportsDefault` derived to call through it.
* fix(web): coerce and normalize default values at save time
Two related Codex findings on PR #135:
P1: Coerce default values to active field type before save
Type-switch drift — user sets a boolean default on a checkbox, then
switches the type to `text`, the stale boolean was previously
serialized as the text default. ValidateFields later auto-applies
it to new items without re-validating the value type.
P2: Trim select defaults to match normalized option values
Option text is trimmed on save ("open " -> "open"), but the select
default handler stored raw option text, producing schemas with
`options:["open"]` + `default:"open "` — defaults that aren't in
the allowed set and get auto-injected as invalid values.
Fix: add coerceDefault(raw, type, options?) to field-editor-types.ts.
Returns undefined when the raw value can't be represented in the
target type (caller drops it). Handles:
- text/url -> must be a non-empty string
- number -> number, or parseable non-empty numeric string
- date -> non-empty string (server validates format)
- checkbox -> must be boolean
- select -> trimmed string that exists in normalized options
Wire through all three save paths:
- CreateCollectionModal.handleCreate (new fields)
- EditCollectionModal.handleSave addedFields (new fields)
- EditCollectionModal.handleSave updatedExisting (existing fields,
where the same type-switch risk applies)
The select-options branch passes the already-normalized `def.options`
into coerceDefault so whitespace drift is caught in the same step as
type coercion.
* fix(web): tighten date coercion, preserve opaque defaults, stable keys
Three Codex findings on PR #135:
P1: Validate date defaults before persisting them
coerceDefault was accepting any non-empty string for the date type,
so switching a field from text/select to date could serialize stale
garbage like "soon" as the date default even though the date input
renders blank. Tighten the date branch to require ISO 8601 format
(YYYY-MM-DD, optionally followed by a T-prefixed datetime tail).
Server still performs stricter parsing; this guard blocks obvious
invalid strings from leaking through.
P1: Preserve unsupported field defaults during edit saves
The existing-fields save path dropped `default` whenever
typeSupportsDefault(f.type) returned false. Opening and saving a
collection that contained a multi_select or relation default (e.g.
from an API import) would silently strip those defaults as a side
effect of unrelated edits — schema-mutating regression.
Fix: in the existing-fields branch, if the active type isn't UI-
editable for defaults, pass field.default through verbatim instead
of dropping it. Types that *are* UI-editable still run through
coerceDefault. New-field paths are unchanged because new fields
never carry a pre-existing opaque default.
P2: Use stable unique keys for select default options
The default-value dropdown for select fields keyed its <option>s by
text, but duplicate option labels aren't prevented anywhere in the
editor or save path. A collection with duplicate options would hit
Svelte's keyed-each duplicate-key behavior and break the control.
Switch to keying by index for display stability.
* fix(web): clear stale relation options before async reload
Codex P2 (PR #135): loadCollectionOptions() awaited the fetch before
replacing collectionOptions, so a reopened modal — especially after
a workspace switch — briefly showed the previous workspace's
relation targets. A fast user could pick one and persist a slug that
doesn't exist in the current workspace.
Fix: clear collectionOptions = [] synchronously at the start of
loadCollectionOptions(), before awaiting the request. If the fetch
fails the picker falls back to its empty-state hint. Applied in both
CreateCollectionModal and EditCollectionModal.
* fix(web): token-guard collection fetch + checkbox default clear
Two Codex findings on PR #135:
P2: Ignore stale collection-list responses before setting options
The previous fix cleared collectionOptions at fetch start but still
unconditionally applied whichever response resolved last. Rapid
reopens or slow networks could let an older response land after a
newer one and overwrite it, letting a user persist a relation slug
from the wrong workspace.
Fix: add a monotonic collectionsRequestToken in both modals. Bump it
on each fetch, capture the current value, and drop the response if
the token has moved on when it resolves. Applied in both success
and error paths.
P2: Allow clearing checkbox defaults instead of forcing false
The checkbox default was tri-state at the schema level (no default
/ default false / default true) but the UI only toggled between
true and false. Unchecking stored `false`, and there was no way to
get back to `undefined` — so ValidateFields would auto-inject
`false` into new items even when the user meant "no default".
Fix: add an explicit "Clear" affordance next to the checkbox that
shows only when field.default is set. Clears to undefined, leaving
schema with no default for that field. Preserves the intentional
`false` case (user wants new items to default to unchecked).
* fix(web): calendar-validate date defaults instead of regex shape only
Codex P2 (PR #135): the date branch of coerceDefault accepted any
string matching the YYYY-MM-DD shape, so impossible dates like
"2026-99-99" or "2026-01-32" could be persisted when users switched
a field from text/select to date. ValidateFields later auto-applies
these as defaults on new items without re-checking, propagating
invalid dates silently.
Replace the shape-only regex with real calendar validation:
- Plain date branch (YYYY-MM-DD): parse month/day, then round-trip
through Date.UTC and verify the resulting year/month/day match
the input. Rejects out-of-range components (month > 12) and
overflow cases (day 32 rolling to next month).
- RFC3339 datetime branch: keep the shape check (stricter than a
loose `T.+` suffix — rejects "2026-01-01Tnot-a-time"), then confirm
Date.parse yields a finite timestamp.
Both branches return undefined on rejection so the caller drops the
default rather than persisting garbage.
* fix(web): strict datetime coercion + drop select defaults w/ empty opts
Two follow-up Codex findings on PR #135:
P1: Reject non-RFC3339 datetime defaults in coercion
Previous fix did shape + Date.parse, but `new Date(...)` silently
rolls calendar-invalid dates (e.g. "2026-02-31T10:00:00Z" becomes
March 3) so impossible timestamps still passed. Switch the datetime
branch to the same component-parse + round-trip technique as the
YYYY-MM-DD branch:
- Extract Y/M/D + h/m[/s] from the regex capture groups
- Range-check each component (month 1–12, day 1–31, h ≤ 23, m/s ≤ 59)
- Construct a UTC Date from Y/M/D and verify the resulting
components match the input to catch day overflow
Date.parse is no longer trusted alone. Out-of-range days,
impossible calendar dates, and non-RFC3339 strings are all dropped.
P2: Drop select defaults when normalized options are empty
The save paths passed `def.options` into coerceDefault, but
`def.options` is omitted when the normalized list is empty, so a
select field with no options would skip the membership check and
keep a stale string default. ValidateFields would then auto-apply
a default that doesn't exist in any allowed set.
Fix: in all three save paths, pass the already-normalized opts
array (including []) to coerceDefault when the type is select.
Non-select types continue to pass undefined since they don't
consult the options parameter.
- CreateCollectionModal: use the local `opts` variable
- EditCollectionModal addedFields: use the local `opts` variable
- EditCollectionModal updatedExisting: extract a
`normalizedOpts` local (options were previously inlined) and
reuse it for both def.options and the coerceDefault call
* fix(web): drop stale default on type switch to multi_select/relation
Two Codex findings on PR #135:
P1: Drop stale default when existing field switches to relation/multi_select
The existing-fields save path preserved f.default verbatim for every
UI-unsupported type. That's correct when the field was loaded with a
pre-existing opaque default (API/import). But it misfires when the
user sets a default while the field is text/number/select and then
switches the type to relation or multi_select — the default UI
hides, but the stale value persists and gets saved.
Fix: track the load-time type as `originalType` on EditableField and
only fall through to the verbatim-preserve branch when the active
type still matches the original. In-session type switches to a
UI-unsupported type now drop the default instead. New-field paths
don't need this because new fields never carry pre-existing
opaque defaults.
P2: Enforce strict RFC3339 datetime shape in default coercion
The previous datetime regex accepted optional timezone and
offsets without the colon, so "2026-01-01T10:00" and
"2026-01-01T10:00+0100" round-tripped as defaults even though the
backend's time.RFC3339 parser requires seconds + a colon in the
offset. That lets defaults survive here that the server rejects.
Fix: require seconds, require timezone, require colon in offset.
Matches strict RFC3339 / Go time.RFC3339.
* fix(web): validate RFC3339 timezone offsets in date coercion
Codex P2 (PR #135): the datetime regex enforced the `±hh:mm` shape
but never validated the numeric ranges of the offset, so values like
"2026-01-01T10:00:00+99:99" were treated as valid and serialized.
Go's time.RFC3339 (backend parser) rejects those, and defaults are
auto-applied to new items without re-validation, so an invalid
offset would silently propagate.
Add explicit offset bounds: hours 0–23, minutes 0–59 (matching Go's
time.RFC3339 acceptance of ±23:59). `Z` skips the check. Applied
after the regex match in the datetime branch.
* fix(web): raw string number default + defaults-equal type switch check
Two Codex findings on PR #135:
P2: Preserve raw number input until commit
The number-default input called Number(v) on every oninput and
wrote the coerced value back to field.default. Because the input
was controlled by `value={defaultAsString}`, partial typing states
like "1." collapsed to "1" on each keystroke (Number("1.") === 1),
making it impossible to type decimals. Negative signs had the same
problem.
Fix: keep the raw string in field.default while editing.
coerceDefault already handles string→number conversion at save
time and drops garbage strings, so no save-path change is needed.
P2: Track any type switch before preserving hidden defaults
The existing-fields unsupported-type fallback preserved f.default
whenever the active type matched originalType. That missed the
round-trip case: relation → text → relation with a new default
injected in the middle. Type matches at save but the default is
stale and un-editable through the UI.
Fix: snapshot originalDefault at load alongside originalType, and
only preserve the default when BOTH are unchanged. Otherwise drop.
Add defaultsEqual() helper to field-editor-types.ts for
polymorphic comparison (JSON-stringify-based — fine for schema
defaults, which are always JSON primitives/arrays).
* fix(web): truncate datetime defaults to YYYY-MM-DD for date input binding
Codex P2 (PR #135): <input type="date"> only accepts a YYYY-MM-DD
value. An RFC3339 datetime default like "2026-01-01T10:00:00Z" was
bound directly via defaultAsString and rendered blank, leading users
to believe the field had no default — while field.default remained
populated and was preserved on save through coerceDefault. Result:
hidden datetime defaults that silently survived unrelated edits.
Fix: derive a display-only dateDefaultDisplay string that truncates
anything after the YYYY-MM-DD prefix, and bind the date input to
that. field.default itself stays untouched until the user actually
picks a new date, at which point onDefaultDateInput writes the pure
YYYY-MM-DD value. This keeps API-loaded datetime defaults round-
tripping untouched (when the user doesn't edit them) while making
them visible for manual correction.
|
||
|
|
c9f16a04ea |
feat(web): key/label split + slugification in collection modals (TASK-595) (#134)
* feat(web): add key/label split + slugification to collection modals
Closes TASK-595 in PLAN-593.
Replace the Create modal's stripped-down row form with the shared
FieldEditor component from TASK-594 and introduce a proper key/label
split with auto-slugification, duplicate detection, and reserved-key
validation.
New UI
- FieldEditor shows a muted monospace Key input under the Label input
for new (unsaved) fields only. The key auto-syncs from the label
via slugify(). Once the user edits the key manually (keyTouched),
auto-sync stops and an inline hint explains that keys are immutable
after save.
- Inline error message + red outline on the key input when the key is
invalid or collides with another field. Create/Save buttons are
disabled (with a reason tooltip) while any field has a blocking
error.
Shared helpers in field-editor-types.ts
- slugifyKey(): lowercase, strip non-[a-z0-9_\s-], collapse whitespace
and hyphens to underscores, trim, max 40 chars.
- RESERVED_FIELD_KEYS: UI-side reserved list mirroring top-level Item
JSON fields (id, slug, ref, title, content, created_at, etc.) that
would shadow core item properties and cause confusion.
- validateFieldKey(): structural validation (non-empty, starts with a
letter, only lowercase+digits+underscore, not reserved, length).
- fieldFromDef(): hydrate an EditableField from a FieldDef preserving
the key verbatim — used when loading templates or existing fields
so slugify doesn't overwrite already-valid keys.
CreateCollectionModal
- Drops the { key, type, options: string } row form.
- Now renders fields via FieldEditor bound to EditableField[].
- Template selection marks loaded fields keyTouched=true so template
keys stay intact. Duplicate / reserved / empty keys disable Create
with a hover tooltip reason.
- Save uses field.key directly (no longer derives key from label at
submit time).
EditCollectionModal
- New fields gain the same key/label split + validation. Duplicate
detection runs against existing field keys + other new-field keys.
- hasNewFieldBlockingErrors gates the Save button with a tooltip.
- Existing-field behavior is unchanged: label editable, key frozen,
no key row rendered.
Behavior / regression notes
- User-visible outcome for a typed-and-submitted collection is now:
label "Target Date" -> key "target_date" (was "Target Date" in
pre-T2 behavior). This is the intended fix.
- Templates continue to ship with their existing keys verbatim.
- Alignment of the type dropdown against the taller new-field card
will be revisited in TASK-598 (visual redesign pass).
* fix(web): persist terminal_options on newly-created status fields
Codex P2 (PR #134): the Create modal and EditCollectionModal's new-
fields serialization paths copied key/label/type/options into FieldDef
but not terminal_options. Users could toggle terminal markings on a
status field in the FieldEditor, click Create (or Save), and have
those choices silently dropped — making terminal-dependent behavior
fall back to defaults and misclassify statuses.
Mirror the existing-fields branch that already handles this in
EditCollectionModal: when the field key is "status" and terminalOptions
is non-empty, filter to the options that survived in the saved set and
write them to def.terminal_options.
Key === "status" gating matches the current FieldEditor UI. T4
(TASK-597) will generalize terminal options to any select field and
the gate can be removed there.
|
||
|
|
ecea75dcd8 |
refactor(web): extract shared FieldEditor component (#133)
Pull the field-card UI out of EditCollectionModal into a reusable FieldEditor.svelte so existing and new fields render identically. Foundation for PLAN-593 (Collection Modal Redesign, TASK-594). - Add FieldEditor.svelte with its own scoped styles for the field card, reorder buttons, select/multi_select options editor, and status-field terminal toggle. - Add field-editor-types.ts with the shared EditableField interface, FIELD_TYPES list, and a blankField() factory. - EditCollectionModal adopts FieldEditor for both existing and new (unsaved) fields, replacing the stripped-down comma-separated row form. - Merge the two field lists into one visual container so new fields flow continuously with existing ones; drop the horizontal divider. - New fields gain reorder buttons within the new-fields list as a natural consequence of the unification. - Save logic, migration building, and status-terminal behavior are preserved exactly. Key-from-label derivation for new fields now reads from 'label' (was 'key'), maintaining identical user-visible behavior until TASK-595 introduces slugification. |
||
|
|
311067ade1 |
fix: fall through to legacy title lookup when ref-shaped key misses
A body like `[[ISO-9001]]` or `[[BUG1-5]]` matches REF_PATTERN but may legitimately be a pre-existing title-based wiki-link (the ref format is just PREFIX-NUMBER, which overlaps with plausible real titles). The previous commit returned the raw match immediately on a failed ref lookup, effectively dropping any ref-shaped legacy titles. Change the ref branch to fall through to the legacy title / collection matching paths when no item ref matches. Canonical `[[BUG-585]]` → BUG-586 still resolves correctly because the ref branch wins whenever the ref exists; only the miss case now continues searching. |
||
|
|
a4496849dc |
fix: prioritize REF lookup over full-body title match
Codex flagged a precedence inversion: the earlier full-body-first checks ran before REF_PATTERN, so a canonical ref link like `[[BUG-585]]` would silently retarget onto a user item whose title happened to match the ref literal (case-insensitive). Ref storage is now our canonical form via markdownToWikiLinks, so this path must be deterministic. Restructure the resolver so ref lookup is always checked first. The legacy full-body title / collection-qualified title checks only run on bodies that actually need them — i.e. when the body contains a pipe (which is the only condition that motivated the full-body check in the first place: recovering pre-existing "[[A|B]]" / "[[coll/A|B]]" titles). |
||
|
|
922cb26e1d |
fix: preserve legacy [[coll/Title]] links whose titles contain |
Follow-up to the previous commit: the full-body title lookup handled the plain-title case, but collection-qualified legacy links like `[[tasks/A|B]]` (where the item's real title is "A|B" in "tasks") were still split on the pipe before the collection/title resolution ran, so the lookup was attempted for title "A" and the link rendered as plain text. Add a full-body collection-qualified lookup alongside the full-body title lookup, both before the `key|display` split. |
||
|
|
de21b1199d |
fix: preserve legacy [[Title]] links whose titles contain |
Codex flagged a behavioral regression in `wikiLinksToMarkdown`: splitting the wiki body on the first unescaped `|` unconditionally meant a legacy link like `[[A|B]]` — where the item's actual title is literally `A|B` — would be parsed as `key=A, display=B`, fail to resolve, and render as plain text. Fix by attempting an exact full-body title match (with `|` intact) before falling through to the `key|display` split. This keeps the new ref-based forms (`[[REF]]`, `[[REF|Display]]`) working while recovering pre-existing content whose titles were never escape-encoded. |
||
|
|
d1a5ea3975 |
fix: robust wiki-link round-trip and navigable popover (BUG-586 follow-ups)
Follow-up to the BUG-586 fix that surfaced several edge cases under
real-world use. Covers three related improvements to the wiki-link
experience in the editor.
Reference-based wiki-link storage
Previously `[Title](/url)` round-tripped to `[[Title]]`. That form
broke for titles containing `[`, `]`, `/`, or `|`. Storage now uses
the item's opaque ref (e.g. `[[BUG-586]]`, or `[[BUG-586|Custom]]`
when the visible text differs from the item's current title).
`wikiLinksToMarkdown` accepts three forms in preference order:
ref-only, ref-with-display-override, and legacy title lookup.
Titles can now contain any characters and links survive renames.
Escape-aware parsing
Both `markdownToWikiLinks` and `wikiLinksToMarkdown` now recognize
`\.` escape sequences inside their capture groups. tiptap-markdown
emits `\[`, `\]`, `\\` in link text when the text contains literal
brackets, so the prior regexes (`[^\]]+`) terminated prematurely and
missed valid links. Helper functions escape/unescape the markdown
link-text layer and the wiki-link body layer separately so `]`, `|`,
and `\` can appear in display-override text.
Leave unresolved [[X]] untouched
The `[[…]]` regex is greedy and can match spans that were never
intended as wiki-links — notably `[[` sequences inside another
markdown link's text. On miss, the function now returns the original
match verbatim instead of emitting `[…](broken)`, which previously
hijacked surrounding content and accumulated corruption on each
save cycle. Broken items heal themselves on the next auto-save.
Picker: show ref + align URL with the route
The `[[` picker now lists the ref badge next to the title and keys
`{#each}` by `doc.id` so duplicate titles don't collide. `execLink`
now reads `page.params.username`/`page.params.workspace` from the
live route (previously `workspaceStore.current`, which could be
empty), so the inserted `href` matches the URL shape that the
round-trip expects.
Clickable link popover
The popover's URL label is now a real `<a href="…">`. Plain click →
`goto()` for internal paths, full navigation for external. Ctrl /
Cmd / middle-click pass through to the browser so "new tab" and
"copy link" work naturally. `onmousedown.stopPropagation` keeps the
outer popover's focus-trap from swallowing the click.
|
||
|
|
e328844a1b |
fix: resolve five open bugs (BUG-585, BUG-586, BUG-588, BUG-589, BUG-590)
BUG-585 — Code-block copy no longer includes ``` fences
Editor.svelte: ProseMirror plugin overrides copy/cut when the selection
is inside a code_block node and writes raw textBetween to the clipboard.
NodeView for non-mermaid code blocks now shows a hover "Copy" button that
uses the existing copyToClipboard() util (with execCommand fallback).
BUG-586 — Wiki-link picker matches on item ref
Editor.svelte: getFilteredLinks() now also matches formatItemRef(item),
so typing [[DOC-535]] finds items by their issue ID. Picker dropdown
shows the ref as a badge; {#each} key switched to doc.id so duplicate
titles across collections don't collide.
BUG-588 — Can unlink OAuth provider when password is configured
Adds a password_set column to track whether a user has a usable
password vs. the random placeholder hash given to OAuth users.
CreateUser sets it true, UpdateUser sets it true when a password is
provided, and ValidatePassword auto-upgrades it on any successful
email/password login (which transparently upgrades pre-existing users
who linked OAuth after signing up with a real password — the OAuth
placeholder hash cannot match user-supplied plaintext, so this is safe).
handleOAuthUnlink now permits removing the last provider when
user.HasPassword() is true.
BUG-589 — Pre-auth pages render standalone
+layout.svelte: isAuthPage now also matches /forgot-password and
/reset-password/* so those pages don't inherit the authenticated
sidebar/topbar layout.
BUG-590 — Search no longer crashes with null results
store.Search() returned a nil Results slice on no-match queries, which
Go marshals as JSON null; CommandPalette then crashed on results.length.
Backend now normalizes nil to []SearchResult{} before returning.
CommandPalette also coalesces resp.results ?? [] on the initial search
and loadMore paths as belt-and-suspenders hardening.
|
||
|
|
e530e5f1ab |
fix: force full navigation for OAuth link buttons (#131)
The "Link GitHub"/"Link Google" buttons on /console/settings and the OAuth sign-in buttons on /login were plain <a href="/auth/..."> tags. SvelteKit intercepted the clicks and did client-side navigation, so the request never hit the nginx router and pad-cloud never saw it. SvelteKit would then try to match /auth/github/link against the [workspace]/[collection] route, 404 on the API calls, and render "Collection not found". Add data-sveltekit-reload so the browser performs a real HTTP navigation and the nginx router can forward /auth/* to the pad-cloud sidecar. |
||
|
|
55a779d838 |
fix: load workspaces reactively after post-auth navigation (BUG-584) (#130)
* fix: load workspaces reactively after post-auth navigation (BUG-584)
Root layout only loaded workspaceStore inside onMount, guarded by
!isAuthPage. When a user first lands on /login, onMount skips the load
(isAuthPage is true). After login, goto('/console') does a client-side
navigation — the root layout's onMount does NOT re-run, so the
workspace store stays empty. When the user then opens a workspace,
<TopBar /> renders with a blank workspace list until a hard refresh.
The same failure mode affects register, join, reset-password, and any
other post-auth redirect path, since all of them mount the root layout
on an auth page first.
Fix: replace the onMount-local loadAll() call with a reactive $effect
that fires whenever the user is authenticated, the page is an app page
(not auth/share), and the store hasn't been loaded yet. A
workspacesLoaded latch prevents re-firing for users who legitimately
have zero workspaces.
Also fix a broken template-literal in the mobile header <a href>:
\${workspaceStore.current?.slug} was a JS template-literal in a plain
HTML attribute string, which Svelte renders as a literal '$'. Changed
to Svelte's {...} attribute interpolation.
* fix: drop authStore.authenticated gate from workspace loader effect
Addresses Codex P1 feedback on #130. The onMount auth-check block
intentionally swallows authStore.load() failures with a comment
explaining the server may not support auth. Gating the $effect on
authStore.authenticated therefore regressed the original !isAuthPage
behavior for deployments where /api/v1/auth/session is unavailable:
the effect never fired and the workspace list stayed empty.
Remove that one gate. The onMount flow still redirects unauthenticated
users to /login before authReady flips true, so by the time the effect
runs on an app page we're either authenticated or auth is
unsupported/errored — both cases should load workspaces, matching the
original behavior. Comment updated to document why.
* fix: skip workspace load during logged-out-to-/login redirect window
Addresses second Codex P1 on #130. The previous commit dropped the
authStore.authenticated gate from the workspace loader effect, which
fixed auth-unsupported deployments but introduced a new regression:
authReady is set to true inside the !auth / setup_required /
!auth.authenticated branches of onMount *before* goto('/login')
completes. For a logged-out user who first hits a protected route,
during that window the effect saw authReady=true and isAuthPage=false
(still on the protected URL), fired loadAll() (silently 401), and
latched workspacesLoaded=true — blocking the retry after login.
Gate on (authStore.authenticated || authLoadFailed) where
authLoadFailed is set only in the catch branch. This preserves both
the original backward-compat for auth-unsupported deployments and the
correct skip-during-redirect behavior, without coupling the effect
to the rest of the redirect machinery.
|
||
|
|
fb56ada1cd |
fix: align Starred page layout with sibling pages (BUG-579) (#129)
The Starred page used hardcoded layout values (max-width: 800px, asymmetric padding, vertically-stacked header, smaller h1) while every other workspace page uses the shared design tokens. Swap to the canonical pattern from activity/+page.svelte so the page feels consistent with Activity, Conventions, Settings, etc. - max-width: 800px -> var(--content-max-width) (960px) - padding: var(--space-6) var(--space-6) var(--space-12) -> var(--space-8) var(--space-6) - .page-header: flex row with justify-content: space-between - .header-top renamed to .page-header-left; redundant margin-bottom removed - h1 font-size: 1.5em -> 1.6em |
||
|
|
91920c9085 |
feat: add collection-level search with Cmd+F (#127)
* feat: add collection-level search with Cmd+F Intercept Cmd+F / Ctrl+F on collection pages to focus the search input instead of opening browser search. Replace client-side substring filtering with API-backed FTS search scoped to the collection, with 200ms debounce and instant client-side fallback while the API responds. - Cmd+F / Ctrl+F focuses the FilterBar search input - Escape clears search and blurs the input - Search uses /search?collection=<slug> for full-text matching - Client-side filter used as fallback during API debounce - searchResultIds cleared on all filter/view reset paths - FilterBar exposes searchInputEl via $bindable prop * fix: open filters panel on Cmd+F and guard stale search responses - Open filtersOpen panel before focusing search input so it exists in the DOM; use requestAnimationFrame to wait for mount - Snapshot query before async search and discard response if query changed while loading Addresses codex review on PR #127. * fix: route Cmd+F through layout keydown handler via UI store The collection page's svelte:window onkeydown couldn't reliably intercept Cmd+F because the layout already registers the window keydown handler. Move Cmd+F handling to the layout's handler and dispatch via a uiStore.collectionSearchRequested signal that the collection page watches with $effect. * fix: clear stale search IDs immediately on new query Set searchResultIds to null as soon as a new query arrives so the client-side fallback filter kicks in immediately while the API debounce is pending. Previously stale IDs from the prior query would persist during the 200ms gap. Archived filtering and limit concerns are already handled by the existing filteredItems pipeline which applies field/status filters on top of search results. Addresses codex review on PR #127. |
||
|
|
9616374a80 |
feat: upgrade CommandPalette with filters, grouping, and pagination (#126)
* feat: upgrade CommandPalette with filters, grouping, and pagination Rewrite the Cmd+K search modal as a full-featured search experience: - Filter chips: collection and status filters from facets, toggle on click - Grouped results: items grouped by collection with section headers - Better result cards: ref, title, priority dot, status badge, relative date - Pagination: "Load more" button appends next page of results - Recent searches: last 10 queries persisted in localStorage - Result count displayed below filter chips * fix: reset loading state on empty query and guard stale loadMore - Clear loading flag when query is emptied so spinner doesn't stick - Capture query/filter snapshot before loadMore API call and discard the response if they changed while loading Addresses codex review on PR #126. |
||
|
|
8351b8194f |
feat: add faceted counts to search results (#125)
* feat: add faceted counts to search results Add collection and status faceted counts to SearchResponse so the frontend can show breakdowns like "Tasks (24) · Ideas (4)". Facets reflect the full unpaginated result set via two GROUP BY queries using the same filters as the main search. - Add SearchFacets type with collections and statuses maps - Add searchFacets() method using appendSearchFilters for consistency - Add SearchFacets TypeScript type to frontend - Add TestSearchFacets covering counts and pagination independence * fix: include ref-hit items in facet counts Ref hits (e.g. searching "TASK-42") bypass FTS and wouldn't appear in FTS-based facet aggregation. Merge them into facets after the facet queries run so collection/status counts include all results. Addresses codex review on PR #125. * fix: remove ref-hit facet merge to avoid double-counting Unconditionally adding ref hits to facets overcounts when the item is also found by FTS (the common case). Since we can't cheaply detect overlap, leave facets as FTS-only. Ref searches typically return 1 exact match, so the off-by-one is acceptable. Addresses codex review on PR #125. |
||
|
|
999bd3cfca |
feat: add pagination and sorting to search API (#123)
* feat: add pagination and sorting to search API Extend the search endpoint with limit/offset pagination and sort options. The response now includes total count (from a separate count query) so frontends can paginate properly. - Add Limit, Offset, Sort, Order to SearchParams with Normalize() defaults - Return SearchResponse struct with total/limit/offset metadata - Count query runs alongside results query for accurate totals - Sort options: relevance (default), created_at, updated_at, title - Add --sort, --limit, --offset flags to CLI search command - Update frontend SearchFilters and SearchResponse types - Add TestSearchPagination and TestSearchSorting integration tests * fix: count ref hits in search totals and handle empty pages - Ensure total is never less than actual results when direct ref matches (e.g. "TASK-5") aren't captured by the FTS count query - Handle empty page in CLI output: show "No results on this page" instead of an invalid descending range like "Showing 11-10 of 5" Addresses codex review on PR #123. * fix: paginate ref hits correctly and add sort tie-breaker - Ref hits now occupy slots on page 0 only; FTS limit/offset adjusted so combined results respect the requested pagination contract - On subsequent pages, ref hits are excluded (already shown on page 0) - Add i.id as deterministic tie-breaker to all ORDER BY clauses to prevent duplicate/missing items across paginated pages Addresses codex review on PR #123. |
||
|
|
aef0e2326a |
feat: add collection and field filtering to search API (#122)
* feat: add collection and field filtering to search API Extend the /search endpoint to support scoping by collection slug and filtering by structured field values (status, priority, and generic field.* params). Works on both SQLite FTS5 and PostgreSQL tsvector. - Add Collection and FieldFilters to SearchParams (store layer) - Parse collection, status, priority, field.* query params (handler) - Add SearchFilters type and update api.search() signature (frontend) - Add --collection, --status, --priority flags to CLI search command - Add integration tests for collection, field, and combined filtering * fix: validate field filter keys to prevent SQL injection Reject field filter keys containing special characters before they reach JSONExtractText, which interpolates keys directly into SQL. Keys must match ^[a-zA-Z][a-zA-Z0-9_-]*$ — validation is applied in both the handler and the store layer as defense in depth. Addresses codex review on PR #122. |
||
|
|
f7d93d878d |
feat: add starred items to dashboard (#119)
* feat: add starred items to dashboard Add starred items section to the dashboard (PLAN-564, TASK-569): - Dashboard API: fetches non-terminal starred items for the current user, applies RBAC visibility filtering, returns as starred_items array - TypeScript types: add starred_items to DashboardResponse - Dashboard UI: renders starred items section with card grid, item refs, status pills, and "View all" link to /starred page * fix: cap starred items in dashboard response to 10 Match the same limit applied to active_items, preventing large payloads on the polling dashboard endpoint for users with many starred items. |
||
|
|
77d3fe2d72 |
feat: add Starred sidebar entry and starred items page (#118)
* feat: add Starred sidebar entry and starred items page
Add dedicated starred items view (PLAN-564, TASK-568):
- Sidebar: new "⭐ Starred" link below Activity, with active state
- Starred page: shows user's starred items grouped by collection
- "Show completed" toggle to include/exclude terminal status items
- Loading skeleton, empty state with usage instructions
- Excludes "starred" and "roles" from collection slug detection
* fix: reactively remove unstarred items, guard against stale responses
Two fixes for the starred page:
1. Items list is now derived from starredStore.isStarred, so unstarring
an item via the ItemCard toggle immediately removes it from the page
without a refetch.
2. Request sequencing via loadSeq counter prevents stale responses from
overwriting the UI when rapidly toggling "Show completed".
* fix: reserve collection slugs that collide with workspace UI routes
Prevent collections from being created or renamed to slugs that shadow
workspace-level routes (settings, activity, roles, starred, library,
new). If a reserved slug is generated, "-collection" is appended
(e.g. "starred" becomes "starred-collection").
Also fixes starred page: items list is now reactive to unstar actions,
and loadStarred uses request sequencing to prevent stale responses.
* fix: skip store filter on starred page until store is loaded
Trust the API response when starredStore hasn't loaded yet, since
/starred only returns starred items. Apply the reactive filter only
after the store is loaded, so unstar actions still remove items
immediately but initial render isn't broken by async timing.
|
||
|
|
5d23791c1d |
feat: add star toggle to web UI item views (#117)
* feat: add star toggle to web UI item views Add per-user item starring to the web UI (PLAN-564, TASK-567): - API client: star(), unstar(), starStatus(), starred() methods - Starred store: loads starred IDs on workspace init, optimistic toggle - ItemCard: star button (☆/★) in top-left, hidden until hover, always visible when starred, amber color - Item detail page: star button in meta-actions header row - Workspace layout: loads starred store on workspace change * fix: guard starred store against stale workspace responses Add a monotonic request counter so that if a user switches workspaces quickly, an older in-flight response won't overwrite the current workspace's starred state. Also guards toggle revert against workspace changes. * fix: merge in-flight toggles with load result in starred store Track toggles that occur while the initial load is in-flight via a pendingToggles map. When the load completes, merge local mutations on top of the server response so optimistic updates aren't overwritten. Reverts also update the pending map for consistency. * fix: preserve toggles on load error, serialize per-item toggles Two fixes for starred store edge cases: 1. Error path now applies pendingToggles instead of resetting to empty, so optimistic toggles survive a failed initial load. 2. Per-item toggle lock (toggleInFlight set) drops rapid duplicate clicks while a toggle API call is in flight, preventing out-of-order requests from producing inconsistent state. * fix: clear stale stars immediately on workspace load Reset starredIds at the start of load() before the async fetch, so a previous user/workspace's stars are never briefly visible during SPA navigation or re-authentication flows. |
||
|
|
b620b7e5cc |
feat: add edit button to collection detail page header (#113)
* feat: add edit button to collection detail page header Wire up the existing EditCollectionModal to the collection detail page with a pencil icon button in the header actions bar. Owner-only, responsive (icon-only on mobile), refreshes page data after saves. Closes IDEA-494 * fix: navigate to new slug after collection rename in edit modal When a collection is renamed, the backend regenerates the slug. The onupdated callback now receives the updated Collection object, so the collection page can detect a slug change and navigate to the new URL instead of 404ing on the old slug. Addresses PR review feedback from #113. * fix: refresh collectionStore after edit modal update Ensures the sidebar reflects updated collection name/slug/icon immediately after editing, matching the settings page behavior. Addresses P2 review feedback from #113. |
||
|
|
aeda627320 |
fix: sidebar collection drag-and-drop only moving by one slot (#111)
Fixed race condition where $effect reset sidebarCollections mid-update because isDraggingSidebar was cleared before API calls completed. - Capture reordered array locally; keep drag flag true during updates - Fire all sort_order updates in parallel with Promise.all - Wrap in try/finally so drag state always resets on failure - Guard reset with generation counter to prevent stale finalize from clobbering a newer drag operation Fixes BUG-562 |
||
|
|
75bed01701 |
feat: sticky breadcrumb header with copy item ID button (#112)
* feat: sticky breadcrumb header with copy item ID button Make the breadcrumb navigation bar sticky so it stays visible when scrolling on item detail pages. Add a copy-to-clipboard button next to the item ref (e.g. TASK-5) that shows a "Copied!" tooltip on click. Uses the existing copyToClipboard utility with non-SSL fallback. Closes IDEA-501 * fix: lower sticky breadcrumb z-index below mobile header The mobile layout has a sticky header at z-index 5. Lower the breadcrumb's z-index to 4 so it slides under the mobile header instead of overlapping navigation controls. * fix: offset sticky breadcrumb below mobile header instead of hiding it Instead of lowering z-index (which hides the breadcrumb behind the mobile header), offset it with top: 45px on mobile so it stacks neatly below the mobile nav. Keeps z-index: 10 so the sticky effect works properly on all screen sizes. |
||
|
|
1fb50d4f62 |
feat: hide plan settings in non-cloud mode, improve overrides UX (#110)
* feat: hide plan settings in non-cloud mode, improve plan overrides UX 1. Gate Plan column, Plan dropdown, and Plan Overrides behind cloud_mode so self-hosted instances don't see irrelevant billing UI. 2. Replace raw JSON textarea for plan overrides with a structured grid of labeled number inputs for each known limit (workspaces, items per workspace, members, API tokens, webhooks). Each field shows "default" as placeholder and accepts -1 for unlimited. Addresses IDEA-561 * fix: address codex review — preserve extra overrides, fix colspan, validate integers - Preserve non-modeled override keys (e.g. storage_bytes) by stashing them on select and merging them back on save - Fix colspan: 6 columns in cloud mode, 5 without (was off by one) - Use Number() + Number.isInteger() instead of parseInt to reject floats and scientific notation instead of silently truncating |
||
|
|
ba01d95111 |
feat: add web UI for TOTP 2FA setup in user settings (#109)
* feat: add web UI for TOTP 2FA setup in user settings Add a Two-Factor Authentication section to the console settings page so users can enable/disable TOTP 2FA from the browser. The backend API already existed (PR #77); this wires up the frontend. - Add 2FA section to console settings with enable/disable flows - Enable flow: QR code + manual secret + verification code input - Recovery codes displayed with copy/download after setup - Disable flow: password confirmation modal - Add totp.setup/verify/disable methods to API client - Add TOTP types (TOTPSetupResponse, TOTPVerifyResponse, etc.) - Add totp_enabled to User type and /auth/me response - Add qrcode npm dependency for rendering otpauth:// URIs Closes TASK-402 * fix: address codex review — separate QR rendering from setup, use clipboard util - Separate QR code rendering from TOTP setup API call so a QR failure doesn't abort setup when manual entry is still available - Use existing copyToClipboard utility with legacy fallback instead of raw navigator.clipboard.writeText |
||
|
|
3d284641d7 |
feat: add audit log UI for admin console (#108)
* feat: add audit log UI for admin console Replace placeholder with full audit log page: filterable by action type and date range, paginated table with relative timestamps, color-coded action badges, parsed metadata details, and IP addresses. * fix: show Unknown for missing actors, guard against stale filter responses Show "Unknown" instead of "System" for entries without actor info (e.g. failed logins). Add a request counter so rapid filter changes discard stale responses instead of overwriting newer results. |
||
|
|
56adba4b58 |
feat: add invitation management panel for admin console (#107)
* feat: add invitation management panel for admin console Platform-wide view of all pending invitations with search, resend, and revoke. Resend creates a fresh invitation code and sends the email. New admin endpoints: GET/POST/DELETE for /admin/invitations. * fix: check email opt-out on resend, abort on stale delete, reload list Respect unsubscribe preferences before resending invitation emails. Abort resend if the old invitation was already accepted/revoked concurrently. Reload the full invitations list after resend since the row ID changes. |
||
|
|
86451174ad |
feat: add user detail panel with workspace memberships (#106)
* feat: add user detail panel with workspace memberships
New GET /api/v1/admin/users/{id}/workspaces endpoint returning workspace
name, slug, role, and join date. Frontend loads memberships when a user
row is expanded and displays them as a linked list with role badges.
* fix: scope workspace fetch error/loading to active selection
Gate both the catch and finally blocks with a selectedId check so stale
requests from previously selected users don't wipe workspace data or
clear the loading indicator for the current selection.
|
||
|
|
b3af1acd07 |
feat: add last active tracking for users (#105)
* feat: add last active tracking for users Track when users were last active via a throttled update (once per 5 minutes) in the auth middleware. Adds last_active_at column, displays relative time in admin user list with full timestamp on hover. * fix: bound last-active goroutine with 3s context timeout Use a short-lived context for the background TouchUserActivity write so it gets cancelled under DB pressure, preventing goroutine/connection buildup from unbounded background work. |
||
|
|
d968b551b7 |
feat: add account disable/deactivation (#104)
* feat: add account disable/deactivation for admin users Allow admins to soft-disable user accounts without deleting data. Disabled users get a 403 on all authenticated requests, their sessions are invalidated on disable, and they show as visually dimmed with a red "disabled" badge in the admin console. Includes migration for disabled_at column, auth middleware check, disable/enable endpoints with audit logging, and frontend toggle with confirmation dialog. * refactor: auto-discover migrations from embedded filesystem Replace hardcoded migration lists with fs.ReadDir on the embedded FS directories. New migrations are now picked up automatically by filename sort order — no need to manually register them in store.go. * fix: block disabled users at login and capture IDs before async calls Reject disabled accounts in the login handler before session creation, not just in RequireAuth middleware (which exempts auth routes). Also capture selectedId into a local const in all async admin panel functions to prevent stale updates if the selection changes during a request. * fix: enforce disabled check in OAuth and password reset flows, always invalidate sessions Block disabled users in all session-minting paths (OAuth login, password reset) not just password login. Also remove early return for already-disabled users in the disable endpoint so session invalidation always runs, handling retry after partial failure. |
||
|
|
79d7d26a00 |
feat: add admin password reset for other users (#103)
* feat: add admin password reset for other users
New POST /api/v1/admin/users/{id}/reset-password endpoint. When email is
configured, sends a password reset link. Otherwise generates a temporary
password and invalidates existing sessions. Includes audit logging via
new password_reset_by_admin action and frontend UI with confirmation.
* fix: treat session revocation and email send as hard failures
Make session invalidation failure abort the reset instead of silently
continuing, and send the reset email synchronously so delivery failures
are surfaced to the admin caller.
|
||
|
|
f97ab766f5 |
feat: add admin role management (promote/demote users) (#102)
* feat: add admin role management (promote/demote users) Allow admins to change user roles between admin and member from the admin console. Includes safety guards to prevent self-demotion and demoting the last admin, with full audit logging. * fix: make last-admin demotion guard atomic Move the admin count check into the SQL UPDATE itself so two concurrent demotion requests cannot both observe >1 admin and proceed. The conditional UPDATE only demotes when at least one other admin exists, eliminating the TOCTOU race. |
||
|
|
a83da0e241 |
feat: refactor admin console into tabbed layout (#101)
* feat: refactor admin console into tabbed layout Split the monolithic admin page into a tabbed layout with sub-pages: - Users tab (default) — user list, search, plan editing - Settings tab — email configuration and plan limits - Invitations tab (placeholder for TASK-559) - Audit Log tab (placeholder for TASK-560) Extract shared admin utilities (adminFetch, adminPatch, adminPost, types, reactive stats store) into lib/stores/admin.svelte.ts. Part of PLAN-552: Admin Console Enhancement (IDEA-242) * fix: show error state when admin users API fails Instead of silently swallowing fetch errors and showing an empty table, display an error message with a retry button. Addresses PR feedback. |
||
|
|
f276745478 |
fix: sidebar collection counts ignore terminal status settings (#100)
When all items in a collection had terminal statuses (e.g. all bugs "fixed"), the sidebar showed the total item count instead of 0. Root cause: ActiveItemCount used `json:"omitempty"`, so a zero value was omitted from the API response. The sidebar fallback logic then displayed item_count (total) instead. Additionally, ListCollections used a hardcoded global terminal status list instead of respecting each collection's configured terminal_options. - Remove omitempty from ItemCount/ActiveItemCount so 0 serializes - Compute active counts per-collection using schema terminal_options - Show count of 0 in sidebar when collection has items but all are done |
||
|
|
7ca0463e70 |
feat: browser-based CLI authentication flow (#97)
Replace the email/password terminal prompt in `pad auth login` with a
browser-based auth flow. The CLI creates a pending session, prints a URL
the user opens in their browser (works for localhost, remote VPS, or
Pad Cloud), and polls until the session is approved.
- Add CLI auth session endpoints (create, poll, approve)
- Add browser approval page at /auth/cli/{code}
- Rewrite `pad auth login` to use browser flow by default
- Keep `pad auth login --interactive` as email/password fallback
- Add login page redirect param support for post-login bounce-back
- Add SQLite and PostgreSQL migrations for cli_auth_sessions table
Closes PLAN-539, IDEA-404
|
||
|
|
1ba9c91992 |
feat: email unsubscribe for non-transactional emails (#96)
* feat: email unsubscribe for non-transactional emails Add CAN-SPAM compliant unsubscribe support: - New email_optouts table (by email address, not user ID) so uninvited recipients can opt out without an account - HMAC-signed unsubscribe tokens (derived from Maileroo API key) so links work without authentication - GET /api/v1/unsubscribe endpoint with simple HTML confirmation page - Invitation emails now include unsubscribe footer link - Welcome emails accept unsubscribe URL parameter - Before sending invitation emails, check opt-out table and silently skip opted-out addresses (prevents invite spam) - Password reset emails are exempt (transactional, user-initiated) Fixes BUG-256. * fix: hide "Copy invite link" when code is unrecoverable For hashed invitations the plaintext code can't be recovered, so the button was copying a broken URL. Now shows "Sent via email" label instead. Only shows the copy button when join_url or code is available. Fixes BUG-255. |
||
|
|
26af891432 |
fix: dashboard "New Idea" button now targets first collection (#95)
The dashboard had two buttons that both called requestQuickAdd() with no argument, so both created tasks. Now: - "New Task" explicitly targets the tasks collection - The second button dynamically targets the first non-system, non-task collection by sort order (showing its icon and name) - requestQuickAdd() accepts an optional collection slug, which the sidebar respects when choosing the target collection Fixes BUG-498. |
||
|
|
ac24fb742c |
fix: breadcrumbs show parent item path for child items (#94)
When viewing a child item (e.g. TASK-101 under PLAN-10), the breadcrumb now shows "Home / Plans / PLAN-10 / TASK-101" instead of the flat "Home / Tasks / TASK-101". - Add parent_slug and parent_collection_slug fields to Go Item model - Populate them in both single-item and bulk enrichment paths - Add corresponding TypeScript types - Update breadcrumb nav to show parent collection and parent item when the item has a parent, falling back to the item's own collection Fixes BUG-516. |
||
|
|
e49a020fc5 |
fix: mobile UI — sidebar buttons, avatar, share copy (#93)
* fix: mobile UI bugs — sidebar buttons, avatar, share link copy - Show sidebar + buttons on touch devices using @media (hover: none) instead of requiring hover (BUG-537) - Add user avatar and menu to mobile TopBar header, filling the blank space next to the workspace selector (BUG-536) - Wire ShareDialog copy into existing clipboard fallback utility so share link copy works over HTTP (BUG-513) * fix(mobile): remove extra right padding on mobile topbar The .topbar has 72px right padding to clear space for the absolutely- positioned desktop avatar. On mobile the avatar is in the normal flex flow, so that padding created a blank gap. Override to var(--space-3). |
||
|
|
1e464ffdac |
fix: apostrophe in slugs, split auto-close, and move navigation (#92)
- Strip apostrophes in slugify() so "Dave's Workspace" becomes "daves-workspace" instead of "dave-s-workspace" (BUG-517) - Use replaceState when navigating after item move to avoid polluting browser history (BUG-538) - Don't auto-close items when split children are done — splitting work out doesn't mean the original is complete (BUG-401) |
||
|
|
da2997c564 |
fix: check HTTP status in admin settings save (PR #91)
savePlatformSettings used raw fetch which doesn't throw on 4xx/5xx, so failed saves (CSRF rejection, auth errors) silently showed "Saved". Now checks resp.ok before reporting success. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
b027046605 |
fix: address review findings for PR #91 (iteration 1)
Ensure make test-pg cleans up Docker containers even when tests fail by capturing the exit code and running cleanup unconditionally. Remove dead CSS rules from root page after welcome template simplification. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
b2b4feecb9 |
feat: console navigation, PostgreSQL CI, and operational improvements
- Route root (/) to /console for centralized workspace management - Update TopBar user dropdown with console nav links (workspaces, settings, billing, admin) - Move account settings (profile, password, tokens) from workspace settings to /console/settings - Enhance admin page with email configuration UI and CSRF-protected writes - Add PostgreSQL CI job to GitHub Actions with race detector on main - Add `make test-pg` for local PostgreSQL testing via docker-compose - Expand health/ready endpoint with DB connection pool stats - Increase item number retry limit for high-concurrency environments - Add concurrent store benchmarks and FTS search quality tests - Add AGENTS.md for multi-agent development guidance |
||
|
|
b7808f12a1 |
fix: address Codex review findings for PR #90 (iteration 2)
Update admin frontend to handle new paginated user list response shape
({ users, total } instead of bare array). Add legacy pad_session cookie
fallback to SessionAuth middleware matching validateSessionCookie. Exempt
/api/v1/plan-limits from RequireAuth so billing page can read limits
without authentication.
Co-Authored-By: Claude <noreply@anthropic.com>
|