173d7ddbb23c48408abf6431109cb74d9880faff
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 173d7ddbb2 |
feat(views): Phase 5j slice A — paliad-shape schema redesign
Hard-replaces the 5i projax.views table per m's Q10 pick (2026-05-29):
no real data to preserve after a few hours, and the shape changes are
big enough that a clean recreate beats a 6-step ALTER.
Schema (migration 0017_views_redesign.sql):
- id (uuid), slug (text, format-CHECK'd, UNIQUE), name, icon,
filter_json (jsonb — INCLUDES view_type per m's Q2), sort_field,
sort_dir, group_by, sort_order, show_count, last_used_at,
created_at, updated_at.
- DROPPED: pinned, is_default_for, view_type column. m's Q9 picked
MRU (last_used_at) over per-page-default; Q2 placed view_type
inside filter_json so the JSON owns the canonical render spec.
- Constraints: slug regex, sort_dir enum. NO view_type CHECK — the
JSON-shape validator owns it now.
- Indexes: slug UNIQUE, (sort_order, name), (last_used_at DESC).
- updated_at trigger reused; projax_admin ownership preserved.
Store (store/views.go rewrite):
- View struct: Slug as the user-facing key; uuid kept on ID for the
legacy `?view=<uuid>` 302-redirect path that lands in slice C.
- ListViews ordered by sort_order, name (matches sidebar).
- GetView(slug) + GetViewByID(uuid). MostRecentView() drives the
/views landing redirect (slice B).
- TouchView(slug) bumps last_used_at fire-and-forget.
- ReorderViews([]slugs) wires the column for slice G's drag UI.
- CreateView server-assigns sort_order = MAX+1 inside the tx.
- UpdateView replaces every writeable field; renames are supported.
- Validation: slug format regex + reserved-list rejection +
filter_json JSON well-formed check before round-trip.
- ErrViewNotFound / ErrViewSlugTaken / ErrViewSlugReserved /
ErrViewSlugFormat surface to handlers as the typed error set.
Cleanup of the 5i overlay (drops what the new shape obsoletes):
- web/views.go: gutted to a stub. applySavedView, applyDefaultView,
overlayURLFields, filterQueryToJSON, filterJSONToQuery,
filterFromJSONPayload, anySliceToStrings + every old handler
(handleViewsIndex, handleViewCreate, handleViewWrite, handleViewEdit,
handleViewRedirect, handleViewDelete) deleted.
- web/server.go: dropped the /views route registrations and the
applySavedView + applyDefaultView calls in handleTree.
DefaultBanner data-map field removed.
- web/tree_filter.go: TreeFilter.ViewID field removed; ParseTreeFilter
and QueryString stop reading/emitting ?view=.
- web/templates/views.tmpl and view_edit.tmpl deleted.
- web/templates/tree_section.tmpl: default-banner block deleted.
- web/views_test.go: deleted (every test was against the 5i shape).
Between slice A and slice B, /views/* URLs return 404 by design.
Slice B reintroduces the route family in paliad-shape:
GET /views → MRU landing
GET /views/{slug} → render
GET /views/new → editor
GET /views/{slug}/edit → editor
POST /views, /views/{slug}, /views/{slug}/delete → CRUD
Tests (store/views_test.go, new):
- TestViewSlugCRUD — create / get-by-slug / get-by-id / rename /
delete round-trip, including rename-leaves-old-slug-gone.
- TestViewSlugFormatRejected — uppercase, underscore, leading dash,
length-cap, empty all surface ErrViewSlugFormat.
- TestViewReservedSlugRejected — tree/dashboard/calendar/timeline/graph
and friends all reject with ErrViewSlugReserved.
- TestViewSlugCollision — duplicate slug surfaces ErrViewSlugTaken.
- TestViewMRU — TouchView + MostRecentView ordering against a
controlled pair of slugs (resilient to other suites' touched views).
- TestViewReorder — ReorderViews rewrites sort_order ascending.
Web tests stay green (the 5i overlay tests are gone, the rest don't
touch the views shape).
|
|||
| 59a89ef044 |
fix(views): edit UI + URL chip overlay on saved-view pages
m's bug (verbatim from /views): "we cant edit views yet. and the filters on custom views dont seem to work. No apply button and no instant apply" Two distinct gaps, both surgically fixed. ## Gap 1 — edit UI missing Slice D shipped POST /views/<id> (update) but no GET form to drive it. The index page had delete + redirect-open links only. Fix: - New handleViewEdit serves GET /views/<id>/edit with the form pre-filled from the persisted row. - New templates/view_edit.tmpl mirrors the create form, selecting the current values on each <select>, populating each <input value="">. - filterJSONToQuery rebuilds the URL-query representation of filter_json so the `filter_query` text input round-trips on edit. - /views index row gets an "edit" link next to delete. - Route registered before the catch-all GET /views/ so the more specific pattern wins. handleViewRedirect also defensively forwards /edit suffix in case routing falls through. ## Gap 2 — URL chips clobbered by saved-view filter applySavedView did `*filter = filterFromJSONPayload(payload)` — wholesale replace. URL chip params parsed earlier in handleTree were thrown away. Compounded by chip URLs not preserving `?view=<id>`, so even if the overlay had worked, chip clicks would have stripped the saved view. Fix: - TreeFilter grows a `ViewID` field that round-trips through ParseTreeFilter + QueryString. Not a "filter dimension" in the matching sense (Matches ignores it); just a URL anchor that every chip URL emits forward. - applySavedView builds the saved filter, then overlayURLFields() selectively replaces any dimension the user set via URL chip on top (q/tag/mgmt/status/has/show-archived/public/project/project_descendants). - view_type: URL wins when explicitly set, saved value otherwise. - Drift is transient — URL bookmarkable as a "narrowed saved view" without auto-saving back to the row. To persist, user opens /edit. ## Tests - TestViewEditFlow — GET /<id>/edit pre-fills name + filter_query; POST /<id> updates name + view_type + filter_json round-trip in DB. - TestSavedViewPageFilterApply — seed two items + an empty saved view; /?view=<id> shows both; /?view=<id>&tag=work shows only the work one. Also asserts chip URLs contain view=<id> so navigation stays in the saved view. Out of scope (per brief): - No schema changes. - No view sharing / multi-user. - HTMX modal save UI deferred — the existing inline edit page is the surgical fix m's bug actually needs. |
|||
| 5f712c68d4 |
feat(views): Phase 5i slice B — view_type URL param + card view on /tree
m's Q1+Q3 picks (2026-05-26): five canonical view_types
(card/list/calendar/kanban/timeline). Slice B introduces the parameter and
the first non-default rendering: card view on /tree shows the filtered set
as a flat tile grid alongside the existing tree forest.
New web/view_type.go owns the enum, per-route allowed set, parser, and
the chip-strip builder. Per the design note, view_type is RENDER state,
not filter state — kept off TreeFilter so the same filter can render as
card or list.
PageViewTypes("/") = {default: list, allowed: [list, card]}.
Dashboard / calendar / timeline are LOCKED to their native shape in
slice B; switching templates on /dashboard for card vs list is mostly
already done via fuller's 5h tabbed-tiles surface and stays as-is for
now (the chip strip surfaces card as the only allowed value there).
Kanban + cross-page list/card swaps land in slice C onwards.
Render:
- handleTree parses `?view_type=` with the per-route catalog, builds
flatMatchedItems for the card consumer alongside the existing forest.
- tree_section.tmpl gains a view-type chip strip (locked entries shown
greyed-out with title tooltip) + branches into either `tree-card` or
the forest based on .ViewType.
- New templates/tree_card.tmpl renders a flat grid of tiles for the
matched set; per-item field set mirrors the list rendering.
- Hidden `view_type` input added to the search form so chip clicks
preserve the view choice.
Tests:
- view_type_test.go: parser fallback, per-route catalog, chip strip
active/locked flags, filter preservation in chip URLs.
- server_test.go: end-to-end dispatch — GET /?view_type=card renders
tree-card-grid, GET / renders forest, unknown values fall back to
list. Chip strip present on both views.
|
|||
| 13923aadb6 |
feat(views): Phase 5i slice A — project filter dim + descendants toggle
m's Q5 pick (2026-05-26): project scope on every Views-supporting page, with descendants exposed as an explicit on/off chip toggle rather than always-on. Slice A ships the smallest standalone piece of the Views system; slices B–E (view_type URL param, kanban, saved-views schema, defaults) follow on the same branch. TreeFilter grows two fields: - ProjectPath: scoped item's primary path; "" = no filter. - IncludeDescendants: default true; flipped via ?project_descendants=0. Matching extends to path-prefix across `it.Paths` when ProjectPath is set; equality-only when IncludeDescendants is off. Multi-parent items pass when ANY of their paths qualifies. Picker is a shared partial (templates/project_chip.tmpl) that every Views-supporting filter strip includes (tree, dashboard, timeline, calendar). Two states: <select> picker when no project is set; active chip with × clear + descendants on/off chip when scoped. Hidden inputs added to each form so non-picker chip clicks preserve the project state. Graph and admin tools are NOT Views consumers (per design.md / docs/plans/views-system.md §5) and stay untouched. Test-source edits (per the 5c sharpened rule): - dashboard_test.go, public_listing_test.go, timeline_test.go: row membership assertions tightened from `Contains(body, slug)` to `Contains(body, href="/i/path")`. The picker now renders every item's primary path inside a <select>, so coarse slug substring matches falsely passed across filtered-out picker options. Behaviour preserved (filtered rows still don't render); the impl-detail assertion moved to the row link. New tests: TestProjectFilterIncludesDescendants, TestProjectFilterDescendantsOff, TestParseTreeFilterProjectFields, TestTreeFilterProjectRoundTrip, TestSetProjectAndToggleHelpers, TestProjectFilterScopesTreeToDescendants (end-to-end via /). |
|||
| 6f0a318979 |
fix(filters): preserve every value from <select multiple> filter strips
Symptom (m-reported): /calendar filters don't work.
Root cause: ParseTreeFilter and calendar's ?kind parser both used
`r.URL.Query().Get(key)` to read tag/mgmt/has/status/kind. `Get()`
returns ONLY the first value when a URL has the same key repeated, and
the HTMX filter-strip forms (calendar_section.tmpl, timeline_section,
dashboard_section, graph, bulk) all use `<select multiple name="tag">`
which the browser serialises as `?tag=foo&tag=bar` — repeated params,
not the comma-joined `?tag=foo,bar` the tree page emits from its hidden
input. Every second-and-beyond chip silently dropped on every filter
submission across every page with a multi-select strip; m happened to
catch it on /calendar.
Fix (single helper, four call-site swaps):
- web/server.go parseValues(q, key): collects q[key] (the full slice of
values), joins on comma, runs parseCSV. Accepts both URL shapes:
?tag=foo,bar → ["foo", "bar"]
?tag=foo&tag=bar → ["foo", "bar"]
?tag=foo,bar&tag=baz → ["foo", "bar", "baz"]
- web/tree_filter.go ParseTreeFilter: tag / mgmt / status / has all
switch from `parseCSV(q.Get(...))` to `parseValues(q, ...)`. q / show-
archived / public stay on `q.Get` — they're single-value by design.
- web/calendar.go parseCalendarQuery: ?kind handling drops the bespoke
q.Get + strings.Split + dedup-map and uses `parseValues(..., "kind")`
for the same reason. Behaviour preserved for legacy comma-joined
`?kind=event,doc` AND new repeated-param submission.
Regression test:
- TestCalendarFilterMultiValueTagsFromForm seeds three items — one with
both test tags (A+B), one with only A, one with only B — drops a
dated link on each, then probes `/calendar?tag=A&tag=B`. Before the
fix the A-only note leaked through (the parser kept just tag=A);
after, only the A+B item appears per the AND-across-tags contract.
Full web suite green. Pre-existing db/TestBackfillTagsFromArea failure
unchanged (independent of this change).
Same fix transparently repairs /timeline, /dashboard, /graph, /bulk —
they all consume ParseTreeFilter and shared the bug.
|
|||
| f6cf050c3f |
feat(phase 4d): public-listing fields so projax becomes the portfolio source of truth
Adds five additive columns on projax.items and propagates them through
every read/write path. flexsiebels.de (and any future portfolio renderer)
can now pull the public set via the MCP `list_items(public=true)` filter
and stop hard-coding project lists.
## Schema (migration 0014)
- public boolean default false (partial index when true)
- public_description text default ''
- public_live_url text default ''
- public_source_url text default ''
- public_screenshots text[] default '{}'
- items_unified view rebuilt to include the five new columns
- items_public_idx PARTIAL INDEX WHERE public = true (5% of rows)
## Store
- Item struct + scan/scanItems extended (5 cols)
- UpdateInput accepts the new fields with full-replace semantics
- new SetPublic(ids, bool) for bulk write
- SearchFilters gains Public *bool — nil = no filter
## MCP
- list_items: new `public` boolean filter (input schema + handler)
- update_item: 5 new partial-update fields (nil pointer = leave alone)
- itemView always emits the 5 fields (even when public=false) so consumers
can preview "what would publish" without a second round-trip
- 2 new integration tests against the DB
## Web
- /i/{path} grows a "Public listing" fieldset: toggle + textarea + 2 URL
inputs + screenshot list editor with add/remove rows + inline JS for
the editor. Values persist when public is off so toggling never
destroys typed-in content.
- /admin/bulk action bar gains "Make public" / "Make private" via a new
select; SQL update is a single statement per action.
- /?public=1 and /?public=0 chip parameters narrow the tree page.
Active() + QueryString() + TogglePublic() round-trip the state.
- parseScreenshotList helper trims + drops empties + preserves order
- 5 integration tests: migration landed, form round-trip, bulk action
round-trip, detail-page affordances, tree-filter narrowing
## docs/design.md §15
Documents the schema, MCP contract, UI surfaces, flexsiebels consumption
pattern, and what's NOT in scope (flexsiebels-side render, asset hosting,
approval workflows).
## Out of scope (per task brief)
- Flexsiebels rendering — separate task in m/flexsiebels.de after this ships
- Asset hosting (projax stores URLs, never bytes — same PER discipline)
- Multi-stage publish workflow (boolean is enough)
|
|||
| d5e7796cf6 |
feat(phase 3b filtering): full tree-page filter bar (search + chips + counts + HTMX swap)
Tree page (/) gains every navigation dimension m asked for: - Debounced search input matching title/slug/aliases/content_md/paths case-insensitively (?q=…) - Tag chip row (?tag=a,b — AND within tags, as before) - Management chip row with ?mgmt=mai,self,external,unmanaged (OR within management; "unmanaged" is the synthetic empty-array case) - Status chip row with ?status=active,done,archived (default = active; archived rows only surface when the separate show-archived toggle is on) - Has-link chip row ?has=caldav-list,gitea-repo - Each chip carries the count it would yield if toggled — honest user cue, computed via per-dimension recomputation in pure Go (cheap at m's scale) - URL is the source of truth — every filter goes through the query string, so any view is bookmarkable; HTMX swaps the tree-section in place with hx-push-url=true on every chip click and on search keyup - Empty-state copy with a clear-all link Implementation: - web/tree_filter.go new: TreeFilter struct + ParseTreeFilter + QueryString/URL + Toggle* helpers + Matches + applyTreeFilter (replacement for buildForest) + computeChipCounts. - web/tree_filter_test.go: parse defaults + every dimension's match + URL round-trip + ancestor-keep semantics + chip counting. - Linkages: linkKindsByItem on Server fans across the two has-link ref_types in one pass and feeds the filter. - tree.tmpl reduced to a one-liner that calls tree-section; new tree_section partial powers both the initial page render and HTMX fragment swaps (matches the pattern from phases 2.a/b/d). docs/design.md §4: tree-filter contract — URL keys, AND/OR rules, count semantics, archived ergonomics. |