1f8c626aed23ba6567c3e1023c6ca685e1dd67a3
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 1f8c626aed |
feat(views): Phase 5j slice E — sidebar Views section with user views
The Phase 5j sidebar's Views entry already linked to /views; slice E
extends the section to LIST every saved user view with its name + icon
glyph + active state, plus a "+ New view" shortcut at the bottom. The
system views (Tree / Dashboard / Calendar / Timeline / Graph) stay in
the main nav block above so muscle memory holds.
Render plumbing (web/server.go):
- render() pulls ListViews() into the data map under UserViews when
the template is not "login" (login renders without layout). Stub
servers without a real Pool skip cleanly via the name guard.
- One indexed lookup per chrome-bearing render. Slice G can add a
per-request memoisation if profiling bites.
Template (web/templates/layout.tmpl):
- New "Views" sub-section below the main /views entry. Each user view
emits as a nav-item-user-view link with icon glyph + name. Active
marker fires when path == /views/<slug>. Bottom anchor: "+ New view"
link to /views/new for one-click creation from anywhere.
- The icon-glyph stays a placeholder diamond (◆) in this slice; slice
G ships the registry SVGs.
CSS (web/static/style.css):
- nav-item-user-view: slightly smaller font, indented 24px so user
views sit visually under the Views section header.
- nav-item-new-view: muted color to distinguish the action from
navigation.
- sidebar-user-views: flex column with 2px gap matches the existing
sidebar's spacing rhythm.
Tests:
- TestSidebarListsUserViews — seeds one view, asserts the sidebar
surfaces /views/{slug} href + display name + the + New view link.
Active marker fires on /views/{slug}.
|
|||
| f820fa5830 |
feat(views): Phase 5j slice C — full URL migration + system views
Per m's Q1 pick (b) (2026-05-29): legacy `/`, `/dashboard`, `/calendar`,
`/timeline`, `/graph` become `/views/{system-slug}`. Old routes
301-redirect to the new ones with chip params preserved; the legacy
?view=<uuid> param from 5i is resolved through the uuid → slug map
when present so old bookmarks land on the right user view.
System views (web/system_views.go):
- SystemView struct (Slug / Name / Icon / URL) — code-resident, never
rows in projax.views.
- AllSystemViews() returns the canonical five: tree, dashboard,
calendar, timeline, graph. Display order matches the existing
sidebar.
- LookupSystemView(slug) returns the matching entry or nil; the
reserved-slug list in store.IsReservedViewSlug (slice A) is kept
in sync.
- legacyRedirect(systemSlug) handler 301s with chip-param preservation
+ uuid → slug resolution for any leftover ?view=<uuid>.
Routes (web/server.go):
- GET /views/tree → handleTree (was GET /)
- GET /views/dashboard → handleDashboard
- GET /views/timeline → handleTimeline
- GET /views/calendar → handleCalendar
- GET /views/graph → handleGraph
- GET / → 301 → /views/tree
- GET /dashboard → 301 → /views/dashboard
- GET /timeline → 301 → /views/timeline
- GET /calendar → 301 → /views/calendar
- GET /graph → 301 → /views/graph
- POST action endpoints (/dashboard/task/*, /dashboard/pin, /admin/*)
stay where they are — those are RPC-ish, not page renders.
handleTree: dropped the `r.URL.Path != "/"` guard — the only entry
point now is /views/tree, mounted via the new route. Slice F removes
any residual references; this slice keeps the handler reachable.
computeChipCounts grew a `base string` arg so chip URLs anchor on the
caller's route (/views/tree for the system tree, /views/{slug} for
saved views). PageViewTypes recognises both legacy and /views/ keys
during the transition.
Template hrefs / hx-gets bulk-updated to the new URLs:
- layout.tmpl: every sidebar + bottom-nav entry points at
/views/{system-slug}. Active-state checks updated alongside.
- tree_section.tmpl, tree_card.tmpl, tree_kanban.tmpl: clear-filter
/ clear-all hrefs → /views/tree.
- calendar*.tmpl, timeline_section.tmpl, graph.tmpl,
dashboard_section.tmpl: every internal nav + filter link points at
the /views/{slug} surface.
- detail.tmpl, error.tmpl: cancel / back-to-tree → /views/tree.
Test-source updates (per the 5c sharpened rule):
- ~100 test paths bulk-rewritten from /dashboard /calendar /timeline
/graph (and `/`) to their /views/{slug} counterparts. The
behaviour-preservation contract holds: status codes + body shapes
for the rendered pages stay the same; only the URL anchoring the
test changes.
- layout_test.go: sidebar href assertions updated to /views/{slug}.
- view_type_test.go (Q2 + Q3 follow-up): PageViewTypes lookup table
updated to use the new route keys.
- 2 deliberate behaviour-change assertions land: TestLegacyRedirects
expects 301 on the old URLs (was 200); TestTreeRenders fetches
/views/tree (the new home) instead of /.
Internal go-source URL emissions (dashboard.go, calendar.go,
timeline.go) updated to the new BasePath so chip + refresh URLs round
through /views/{slug} correctly.
New tests:
- TestSystemViewLookup — AllSystemViews shape + LookupSystemView
round-trip + unknown-slug nil.
- TestLegacyRedirects — every legacy URL 301s to its new home with
chip params preserved.
- TestLegacyViewUUIDRedirect — old `?view=<uuid>` URLs land on the
resolved slug per m's Q3 pick.
|
|||
| 2f47b28f39 |
feat(views): Phase 5i slice D — saved views table + CRUD + sidebar entry
Persists named bundles of (filter + view_type + sort + group_by). Per m's
Q2 pick (2026-05-26), views are page-agnostic — `is_default_for` lets a
view become the auto-applied default for a page, otherwise views render
on whichever page accepts their view_type.
Schema (db/migrations/0016_views.sql):
- projax.views table with check constraints on view_type (5-value enum),
sort_dir, is_default_for, and the kanban-needs-group rule.
- Case-insensitive unique name index (live rows only).
- One-default-per-page partial unique index.
- updated_at trigger; projax_admin ownership / grants.
Store (store/views.go):
- View struct + ViewInput; ListViews / GetView / CreateView / UpdateView
/ SoftDeleteView / DefaultViewFor.
- CreateView and UpdateView clear the prior default for a page in the
same transaction when IsDefaultFor is set — defends against the
partial unique index outside the SECURITY DEFINER path.
- Validation mirrors the DB check constraints so handlers can surface
friendlier errors before round-tripping.
Handlers (web/views.go) + routes (web/server.go):
- GET /views list + create form (templates/views.tmpl).
- POST /views create (filter_query form field is parsed into
canonical filter_json shape — design.md §2).
- GET /views/<id> redirect to the target page + ?view=<id>.
- POST /views/<id> update.
- POST /views/<id>/delete soft delete.
Resolution path:
- handleTree now calls applySavedView when ?view=<uuid> is present;
fields the saved filter_json + view_type back into the TreeFilter and
the view-type slot. view_type then revalidates against the route
catalog so a saved kanban-view URL on / lands on list with kanban
shown locked until slice C ships it. Failures fall back gracefully
(log + URL-derived filter), no 500.
UI:
- Sidebar gains a Views entry (4-square icon) next to Admin in
layout.tmpl.
- /views renders a flat table + inline create form. The form accepts a
URL-query filter string (e.g. `tag=work&mgmt=mai`) which is canonised
into filter_json on save.
Tests:
- TestViewsCRUDRoundTrip — full create / list / open-redirect / soft-
delete cycle via HTTP, plus filter_json shape assertion.
- TestSavedViewAppliedOnQueryParam — seed a card view scoped to dev,
hit /?view=<id>, assert the page renders card grid + scoped chip-on.
Out of scope for slice D (per design.md §7):
- HTMX modal save UI from any page (the inline-create-on-/views/ form
works; a modal lands in a polish pass).
- MCP read tools for views (deferred to a follow-up — m manages views
via the UI).
|
|||
| bd600633c9 |
feat(layout): mobile bottom-nav + drawer
Phase 5g slice B. Fills the ≤767px gap left by slice A (sidebar
display:none on mobile) with a fixed-bottom 5-slot nav + a drawer for
overflow items. iOS PWA install respects safe-area-inset-bottom so the
nav clears the home indicator.
web/templates/layout.tmpl:
- New <nav class="projax-bottom-nav"> with five slots:
Tree (/) → Dashboard (/dashboard) → +New (/new, raised circle)
→ Calendar (/calendar) → Menu (drawer).
- Center "+ New" slot is a raised .capture-circle (margin-top: -10px,
44×44px, accent background) — mBrian's capture-button pattern, but
pointing at /new because projax has no separate capture flow.
- Menu slot is a <details class="projax-mobile-drawer"> whose <summary>
IS the bottom-nav-item. Tapping pops a drawer-sheet absolutely
positioned 8px above the bottom-nav with overflow items: Timeline,
Graph, Admin, theme toggle, sign-out. Browser-default <details>
handles open/close + tap-outside-dismiss — no JS, no gesture wiring.
- Active class on bottom-nav-item + drawer-item via same .Path-driven
server-side pattern slice A introduced.
- Theme toggle handler now binds to BOTH #theme-toggle (sidebar) AND
#theme-toggle-drawer (drawer). Flipping either updates the icon on
both buttons, sets data-theme on <html>, writes the cookie.
web/static/style.css:
- .projax-bottom-nav: fixed bottom, height = calc(56px +
env(safe-area-inset-bottom, 0)), flex justify-around, z-index 1021.
- .bottom-nav-item: 44×44px min, column-flex, touch-action: none for the
capture-button so iOS doesn't intercept the tap.
- .capture-circle: 44×44px raised circle, accent background.
- .projax-mobile-drawer .drawer-sheet: fixed, bottom-right anchored
above the nav, min(260px, calc(100vw - 16px)) wide, slide-up animation
via @keyframes projax-drawer-up (translateY 8→0, 160ms ease-out).
- @media (min-width: 768px): bottom-nav hidden.
- @media (max-width: 767px): main.projax-main gets padding-bottom =
calc(56px + 1rem + env(safe-area-inset-bottom)) so rows aren't hidden
behind the nav.
docs/design.md:
- New §18 (Layout: sidebar + bottom-nav, Phase 5g). Documents both
surfaces' breakpoints, the .Path-driven active marker, the pre-paint
localStorage restore, the theme-toggle dual-binding, and the four
features I deliberately did not port from mBrian (resize handle,
capture modal, quick-switcher/saved-searches/Today/Work, slide-up
gesture).
Tests (web/layout_test.go):
- TestLayoutBottomNavMarkup: 5 slots present in documented order, +New
is .capture-btn with .capture-circle, Menu is <details>, drawer holds
Timeline/Graph/Admin/theme/sign-out.
- TestLayoutBottomNavActiveClass: /calendar render highlights Calendar
slot only.
- TestLayoutThemeToggleBoundToBothButtons: handler enumerates both
button ids so flipping either flips the theme.
All 10 layout tests pass (7 from slice A + 3 from slice B). Full web
suite green. No test source edits to pre-existing tests — the bottom-
nav is additive markup.
|
|||
| 9d0dd74695 |
feat(layout): desktop sidebar replaces top-nav
Phase 5g slice A. m wants projax aligned with mBrian's nav layout: fixed-
left sidebar on desktop, bottom-nav on mobile (slice B). This slice drops
the top-nav <header> and ships the desktop sidebar; the ≤767px viewport
temporarily renders nav-less until slice B lands the bottom-nav.
web/templates/layout.tmpl:
- Delete the old <header><nav>...</nav></header>. Replace with
<aside class="projax-sidebar"> carrying:
* .sidebar-top: brand (▦ + "projax")
* .sidebar-nav: 6 items (Tree → Dashboard → Calendar → Timeline →
Graph → Admin) with inline SVG icons. Active class set server-side
via `{{if eq $path "/dashboard"}}active{{end}}`.
* .sidebar-bottom: theme toggle + sign-out form + collapse toggle.
- Content wrapped in <main class="projax-main">.
- New pre-paint <script> in <head> reads
localStorage["projax.sidebar.collapsed"] and sets
data-sidebar-collapsed="true" on <html> BEFORE first paint so the
main-content margin doesn't flash 220px→56px on every navigation.
- Existing theme-toggle JS unchanged (the button is just relocated). New
body-end <script> wires the #sidebar-collapse button: toggle the
attribute, persist to localStorage, sync aria-expanded + title.
- DO NOT port mBrian's resize handle — that's the $effect-feedback bug
mBrian debugged at length. Static 220/56px is fine for v1.
web/static/style.css:
- Strip the pre-5g `header { ... }`, `header nav { ... }`,
`header .logout-form { ... }`, `header .brand { ... }`,
`header .theme-toggle { ... }` rules and the matching @media
overrides (320×, 480× targeted `header`).
- New `main.projax-main` rule: `margin-left: var(--projax-sidebar-width,
220px)` on desktop, transitions on collapse. The
`html[data-sidebar-collapsed="true"]` selector flips the var to 56px.
Mobile (≤767px) zeros the margin.
- New `.projax-sidebar` block: fixed-left, z-index 50, .nav-item /
.nav-icon / .nav-label rules, .active border-left accent (matches
mBrian's `border-left: 2px solid #8cf` pattern but uses var(--accent)
so it round-trips dark/light theme).
- @media (max-width: 767px) hides the sidebar so the phone isn't stuck
with a 220px-wide hole until slice B.
web/server.go:
- render() injects `Path: r.URL.Path` into the template data map (unless
caller pre-set it for tests) so the layout can mark the active nav
item without any per-handler boilerplate.
Tests (web/layout_test.go):
- TestLayoutSidebarOnDesktop: aside present, all six href + label pairs
rendered.
- TestLayoutActiveClass: /dashboard render has the Dashboard item with
.active and Tree without.
- TestLayoutCollapseScript: pre-paint localStorage restore + the
collapse-toggle handler both present.
- TestLayoutNoTopHeader: belt-and-braces — the pre-5g <header> and
.logout-btn classes are gone.
All existing tests stay green (TestLayoutHasAdminNavLink,
TestLayoutHasManifestAndAppleTouchIcon, TestLayoutHasViewportMeta,
TestCalendar*, TestTreeRenders, etc.). No test source edits required —
existing assertions look at page CONTENT, not chrome.
|
|||
| e5dd31144a |
feat(calendar): /calendar month-grid view with VEVENT/VTODO/DOC sources
Phase 5e slice A. New surface alongside /timeline (chronological spine) and
/dashboard (today/week buckets) — a 7×N month grid that answers "show me my
month at a glance." Monday-leading weeks per the German convention, with
adjacent-month lead-in/trail-out cells greyed to keep the grid rectangular.
web/calendar.go (new):
- calendarPayload / calendarWeek / calendarDay / calendarRow types.
- parseCalendarQuery: reads ?month=YYYY-MM (defaults to current month),
?kind=event,todo,doc (defaults to all three; creation excluded by design),
inherits the full TreeFilter via ParseTreeFilter so ?tag=work / ?mgmt=mai
scope identically to /timeline.
- handleCalendar: TTL-cached at 60s per (filter, month, kinds).
- buildCalendar: items → TreeFilter narrow → aggregate.{Todos,Events,Docs}
for the grid window → bin by YYYY-MM-DD → stable per-cell sort (timed
first, then by kind rank, then summary).
- layoutCalendarWeeks: pure function building the rectangular grid; lead
days computed from mondayWeekday(monthStart), trailing pad from
(totalCells % 7). Each cell caps visible rows at 3 and surfaces the
remainder via ExtraCount so the template emits a "+N more" drill-down
link to /timeline scoped to that single day.
- formatMonthLabel: German month names (Mai, März, Juni, Dezember).
- docSummary: prefers item_link.note, falls back to last path segment of
ref_id, then ref_id verbatim.
web/templates/calendar.tmpl (new):
- Grid markup as a <table role="grid"> — semantically a calendar grid,
works without JS, and the layout calc already pre-chunks weeks.
- Header carries h1 (German month label), prev/next/today nav, and the
cached/fresh + total-rows counts line.
- Each cell: .calendar-cell, .is-today, .adjacent-month conditional
classes; .today-pill rendered when IsToday.
- Rows: .row-event / .row-todo (+ .overdue) / .row-doc with a leading
time slot and an <a> to /i/<itemPath>.
- "+N more" link drills into /timeline?from=YYYY-MM-DD&to=YYYY-MM-DD.
web/static/style.css:
- ~95 lines of minimal grid styling: 7-column table-fixed, 110px cell
height, today border accent, adjacent-month opacity 0.4, per-kind row
border-left colour. Slice B will refine cell sizing + add the mobile
breakpoint + chip strip.
web/server.go:
- New calendar template parse (layout.tmpl + calendar.tmpl), calendar
field on Server (cache.TTLCache[*calendarPayload]), route registration
GET /calendar.
web/templates/layout.tmpl:
- Nav anchor added between timeline and graph.
web/server_test.go:
- TestLayoutHasViewportMeta now probes /calendar too.
Tests (web/calendar_test.go — pure unit):
- TestCalendarLayoutMondayLead, TestCalendarLayoutTrailingPad: grid math
for Friday-leading (May 2026) and Monday-trailing (June 2026) months.
- TestCalendarTodayCell: IsToday flag lands on the right cell only.
- TestCalendarCellRowOverflow: >3 seeded rows → 3 visible + ExtraCount=2.
- TestMondayWeekday: Sunday→6, Monday→0 conversion.
- TestFormatMonthLabel: German month strings.
- TestParseCalendarQuery{Defaults,MonthParam,KindFilter}: URL parsing.
Tests (web/calendar_integration_test.go — DB integration):
- TestCalendarRendersMonthGrid: empty-data smoke through srv.Routes().
- TestCalendarSurfacesDatedLink: seeds an item_link on today, asserts
the rendered cell carries the note text + .is-today class.
- TestCalendarFilterScopeByTag: seeds two tagged items, confirms
?tag=<work-tag> only renders the work-item rows.
- TestCalendarAdjacentMonthDays: May 2026 (Fri-leading) renders the
Apr 27 lead-in cell with .adjacent-month.
- TestCalendarNavPrevNextLinks: prev → 2026-04, next → 2026-06 links
present.
Slice B follows: refined CSS, mobile breakpoint (≤480px → vertical list
of days), HTMX filter chip strip, docs/design.md §17.
|
|||
| 5dcacff520 |
feat(phase 4b): dark/light theme toggle + file-upload permanently out-of-scope
## Slice A — explicit dark/light toggle projax now ships with two palettes and a 1y cookie to remember the choice. Dark is the new default; ☀ button in the header nav flips to light and writes projax_theme=light. Server reads the cookie via themeFromRequest(r) and injects Theme + ThemeColor into every template via the centralised render(w, r, …) path, so first paint never flashes the wrong theme. Inline JS in layout.tmpl handles the toggle without a server roundtrip. Every panel colour now lives in a CSS variable under :root[data-theme=dark|light]; the only hardcoded hex values left are inside those two :root blocks. A future palette tweak is one edit, not 30 selectors. Graph node colours, kind-badges, highlights and warn/ok/bad all have parallel dark/light values picked for contrast. Standalone SVG download bakes the light palette inline because the downloaded asset has no parent :root providing vars — m's existing snapshots stay print-friendly regardless of his current cookie. Login page keeps its embedded dark CSS — it's the gateway, intentionally always dark. Tests: TestThemeDefaultIsDark, TestThemeCookieRoundTrips, TestThemeCookieUnknownFallsBackToDark, TestThemeTogglePagesShareSameTheme, TestThemeToggleScriptPresent, TestThemeColorMetaHelper. Full suite green. ## Slice B — file-upload permanently out of scope (m, 2026-05-17) docs/design.md moves "File uploads / in-projax storage" from the §3c parked list to a permanent "Out of scope (decided 2026-05-17)" clause with the rationale: PER is the cross-reference index, not the file system. docs/standards/per.md gains the same explicit clause so future shifts working from the PER standard see the constraint where they look. Memory note filed so future workers don't re-propose multipart uploads, attachments tables, or documents buckets. ## docs/design.md §13 Theming Documents the toggle approach, cookie semantics, palette table, the standalone-SVG carve-out, the login-page exception, and the 4b out-of-scope (prefers-color-scheme detection, per-page overrides, transitions on swap). |
|||
| 7ed0a4d46c |
feat(phase 4a): chronological timeline at /timeline + dashboard VTODO edit/delete
/timeline braids every dated thing in projax into a single chronological spine:
CalDAV VTODOs (DUE anchor), VEVENTs (DTSTART), dated item_links (event_date),
and item-creation markers. Default window past-30d to future-90d; ?order=
toggles asc/desc; ?kind= narrows by row type; tree filter (?tag/?mgmt/?has)
applies across kinds. Today / Tomorrow get sticky pills; rows > today+30d
fade. 90s in-memory TTL cache keyed by (filter, window, order, kinds);
busted on any VTODO writeback or dated-link change.
Scope expansion (per head message during 4a): the dashboard Tasks card now
has edit + delete affordances on every row, matching the detail page. New
/dashboard/task/{edit,delete} endpoints share a writeback path with /done.
Timeline VTODO rows reuse the same handlers; HX-Target=timeline-section
selects the re-render surface. Timeline item_link rows reuse the existing
/i/{path}/links/remove handler with the same surface-switch.
VEVENT rows on the timeline remain read-only at v1 (3l decision stands).
Item-creation events render as muted "added X to projax" markers.
Tests cover empty state, dated-doc surfacing, kind-filter narrowing, order
toggle, mixed CalDAV todos + all-day events (with the (2 days) duration
hint), and tag-filter cross-kind. New dashboard test asserts the edit/
delete affordances are wired up.
docs/design.md gains §12 with the full source list, layout rules, time
window, filter integration, cache TTL, and deferred items.
|
|||
| c486a8b028 |
feat(phase 3o admin-index): /admin landing + system panel + nav consolidation
The three admin pages (classify, caldav, bulk) had no shared entry point — m navigated around and couldn't find them. /admin is now their index: - 3 cards, each linking to the underlying tool, with live counts (orphan count via projax.items_unified predicate; calendar count via ListCalendars; item count via projax.items where deleted_at IS NULL AND archived = false) - CalDAV card auto-disables when DAV_URL isn't configured - System panel: version (build-time ldflags hook), last migration (projax.schema_migrations top row), MCP status (token present yes/no — token itself never displayed), upstream health (DAV + Gitea + Supabase, parallel-probed with 1s HTTP timeout each, cached 30s) web/admin.go houses the handler + cache + probeURL helper + count queries. Templates/admin.tmpl renders the cards + system grid. admin_test.go covers /admin render + nav-link presence on every chrome-bearing route. Nav consolidation: the three separate admin links in layout.tmpl collapse to one /admin entry. Pre-existing TestTreeRenders updated to assert the new shape. Probe-URL caveat: probeURL counts any HTTP response as "alive" (incl. 4xx) — the admin panel measures reachability, not authorisation. CalDAV returns 401 on bare GET; Gitea returns 200 at the root; Supabase same. All show green when alive. |
|||
| 1d5db0fe7b |
feat(phase 3j pwa): manifest + service worker + icons → installable PWA
- web/static/manifest.webmanifest: name/short_name/start_url=/dashboard/ display=standalone/theme_color/background_color + three icons (192, 512, 512-maskable with ~12% safe-zone padding) - web/static/sw.js: minimal SW — install caches /static/* shell assets, fetch is network-first with cache fallback on GETs only, skips /mcp/ and non-GETs entirely. CACHE_NAME versioned for clean activate-time prune. - cmd/icongen: stdlib-only generator that produces the three PNG icons from a stylised "p" monogram. Run once at brand-change, commit output. - web.init() registers .webmanifest → application/manifest+json with mime.AddExtensionType so Chrome accepts the manifest at all - layout.tmpl + login.tmpl: manifest link, apple-touch-icon, theme-color, apple-mobile-web-app-* metas, inline SW-register on load (silent on failure — older browsers still work) - design.md gets §"PWA install (Phase 3j)"; CLAUDE.md "Out of scope" drops the Phase-3j line and adds push/background-sync as the remaining Otto-PWA territory - 4 new tests cover manifest MIME, sw.js delivery, all 3 icons, layout meta tags |
|||
| 522b7489d3 |
feat(phase 3i mobile): responsive CSS across all projax pages
- viewport meta on layout.tmpl + login.tmpl (iOS won't render legibly without) - two breakpoints: tablet (≤768px), phone (≤480px) - chip strips: horizontal-scroll with sticky labels instead of wrapping - tables → card lists: classify + bulk render as stacked cards on mobile - forms: single column on phone; min 44px touch targets on buttons - dashboard: cards already 1-col, polish for narrow widths; grid jumps to 2 columns at ≥1280px with stale card spanning both - /graph: SVG scrolls inside .graph-canvas (max-width 100vw, max-height 75vh, overflow auto); "fit to screen" toggle flips natural vs viewport - TestLayoutHasViewportMeta verifies every chrome-bearing route ships the meta tag - CLAUDE.md "Out of scope" drops mobile/Otto-PWA exclusion (head approved on m/mAi#1861); replaced with native-PWA-install line for Phase 3j - design.md adds §"Mobile responsiveness" with breakpoint + principle notes |
|||
| 3901a1888e |
feat(phase 3f graph): visual /graph view, server-rendered SVG, layered DAG
- internal/graph package: pure-Go layered top-down DAG layout - LayerByLongestPath (multi-parent sits at max(parent-layer)+1) - OrderInLayer (slug-sort, deterministic) - Compute returns positions + edges + canvas size - cycle-safe (depth-cap) - web/graph.go handler: filter chips reused from tree_filter - dim mode default (opacity 0.15 on non-matches) - ?isolate=1 hides non-matches + prunes orphaned edges - ?download=svg serves raw SVG attachment - graph_svg.tmpl renders inline SVG: border colour by management (mai blue / self green / external orange / mixed dashed purple), opacity by status, tag pills, ×N multi-parent badge, click-navigate - nav adds "graph" link; design.md §"Graph view" documents the surface - 4 integration tests cover render, dim, isolate, SVG download - 6 layout unit tests cover layering, ordering, cycle-guard |
|||
| f3e5adf358 |
feat(phase 3e dashboard): cross-project /dashboard with tasks, issues, recent docs
- store.RecentDocuments(since, limit) returns dated item_links + parent item - web/dashboard.go handler aggregates VTODOs + Gitea issues + dated links across every linked item, fanout via 4-worker goroutine pool, 60s TTL cache keyed by encoded TreeFilter - Tasks card: bucketed Overdue/Today/Tomorrow/Week/NoDue, sort by bucket then due asc; ✓ button completes via existing PutTodo path + busts cache - Issues card: read-only, reuses GiteaDeps.Cache - Recent docs card: last-30d event_date links, canonical PER rendered - Filter chips on top reuse tree_filter URL params (tag/mgmt/has) - nav adds "dashboard" link; design.md §"Dashboard" documents the surface - 4 integration tests (empty render, dated-link surfacing, tag filter, cache hit) |
|||
| 0e490bb600 |
feat(phase 3d auto-tag): backfill area tags, bulk-edit UI, soft-delete cleanup
- migration 0012: one-shot populate empty tags from each item's area-roots (so chips on /?tag=work etc. actually filter the 40+ mai-backfilled rows) - migration 0013: cleanup 12 orphan item_links + BEFORE-UPDATE trigger that cascades soft-delete to item_links going forward — closes the data drift that made TestItemsUnifiedSurfacesMaiPointer fail since 3c - /admin/bulk page: flat filter+checkbox list with one-tx Apply for add/ remove tag, set management, set status. Per-row inline chip add/remove via /admin/bulk/chip. Reuses tree_filter URL params 1:1. - design.md §3.2 + §4.1 updated; tag+management section notes 0012 - bulk + tag-backfill + soft-delete-cascade tests cover the new surface |
|||
| 96b61f7ed4 |
feat(phase 2 caldav): list + link + create CalDAV calendars
m's CalDAV server (dav.msbls.de, SabreDAV) now feeds projax via a thin
read-only-plus-create-on-demand integration. No background sync; tasks
fetched live on detail-page render.
New caldav/ package
- ListCalendars (PROPFIND Depth: 1, filters non-calendar collections)
- ListTodos (REPORT calendar-query for VTODO; hand-rolled iCalendar
parser for UID/SUMMARY/STATUS/DUE/PRIORITY/LAST-MODIFIED — RFC 5545
line-folding aware)
- CreateCalendar (MKCALENDAR, 405 → ErrCalendarExists for the "link
instead" branch)
- httptest-stubbed tests cover all four paths.
Store
- ItemLink shape + LinksByType / LinksByRefType / AddLink / DeleteLink.
AddLink upserts on (item_id, ref_type, ref_id, rel) so re-linking the
same calendar is idempotent.
Web
- GET /admin/caldav — discovery + auto-suggested matches + manual
linker. Suggestion = lowercased displayname == projax slug or title.
- POST /admin/caldav/link — insert item_links row.
- POST /admin/caldav/unlink — delete by link id.
- POST /i/{path}/caldav/create — MKCALENDAR at <base>/<slug>/, then
AddLink. On 405 (already exists), fall back to link-only.
- Detail page Tasks section: per-calendar block with open VTODOs +
collapsed completed (30d window). Errors per calendar logged and
skipped, so one bad calendar does not blank the page.
- nav adds /admin/caldav link.
main.go
- DAV_URL + DAV_USER + DAV_PASSWORD optional. Missing DAV_URL → CalDAV
off (admin page renders "not configured" notice). DAV_URL set but
user/pass missing → fail fast at boot.
docs/design.md gains §5 documenting the integration shape.
deploy/dokploy.yaml lists the two new secrets + the env var.
Phase 2.b (writeback / two-way / background sync) is parked.
|
|||
| 360060b152 |
feat(auth): rip federation, give projax its own /login
mgmt.msbls.de is being retired; depending on it for auth was the wrong
direction. Match the mBrian / flexsiebels pattern instead — same
Supabase backend, but every tool runs its own login page and scopes
cookies to its own host.
Routes
- GET /login render a sign-in form (mBrian dark visual). If the
request already has a valid session, jump to a safe
redirectTo (or /).
- POST /login exchange email+password at /auth/v1/token?grant_type=
password, set cookies, 302 → redirectTo or /. On
Supabase 4xx, re-render the form with the error.
- POST /logout clear both cookies (Max-Age=-1) + 302 → /login.
Cookies
- access_token + refresh_token only. No Domain attribute → scope is
projax.msbls.de exclusively. HttpOnly, Secure, SameSite=Lax, Path=/,
Max-Age=1y. Matches mBrian + flexsiebels per-host pattern.
Middleware
- /healthz, /login, /logout always pass through (otherwise infinite
redirect on the probe / login page).
- On invalid/expired session → 302 /login?redirectTo=<safe-path>,
RELATIVE to projax. No more cross-host bounce.
- Cookie refresh on expiry still rotates both cookies in place.
- Bearer header path kept for scripted clients.
safeRedirect
- Path-only. Rejects "", "//*", "https://*", "\*", control-char
injection. Cross-host or scheme bounces fall back to "/". Tested
against the obvious bypasses.
Cleanup
- Drop PROJAX_LOGIN_URL + PROJAX_COOKIE_DOMAIN env vars (unused now).
- main.go: log "auth: own-login enabled" with the supabase URL on
startup; warn loudly when SUPABASE_URL is unset.
- README trust-model section rewritten: own login, per-host cookies,
same backend.
- layout.tmpl gains a "sign out" form-button in the nav so the tree /
detail / classify pages can log out without curl.
Tests (14, no DB needed): stub Supabase via httptest covers
healthz/login/logout exemption, anonymous→/login redirect, valid
cookie + Bearer pass-through, stale-refresh rotation with NO Domain
attribute, hard-fail redirect, GET form render with redirectTo carry,
already-signed-in short-circuit, POST success with correct cookies,
POST bad-creds error surface, redirectTo safety (path-only, no //,
no absolute URLs), logout cookie clearance.
Full suite (incl. DB-backed): 27/27 green with PROJAX_SKIP_MIGRATE=1.
|
|||
| 9f905de461 |
feat: Go HTTP server with tree / detail / new / classify
cmd/projax/main.go boots a pgxpool against PROJAX_DB_URL (falls back to
SUPABASE_DATABASE_URL), auto-applies embedded migrations on start
(disable with PROJAX_AUTO_MIGRATE=off), and serves on PROJAX_LISTEN_ADDR
(default :8080).
store package wraps the unified view + projax.items writes. Item has
helper methods for templates: IsArea, Editable, SourceRefDeref. The
Promote() flow runs the insert + item_links link inside a single
transaction so the source row drops out of items_unified atomically.
web package: per-page html/template instances parsed against a shared
layout.tmpl, embedded static/style.css, HTMX from CDN. Pages:
GET / tree of items_unified
GET /i/{path} detail (editable for projax, read-only +
promote form for mai.projects)
POST /i/{path} update projax-native item
POST /i/{path}/promote one-page promote (HTMX-aware fragment for
inline classify)
GET /new?parent={path} create form
POST /new create projax-native item
GET /admin/classify orphan list with inline HTMX promote
GET /healthz DB ping
GET /static/* embedded assets
Auth is intentionally out of scope for v1 — service binds to whatever
PROJAX_LISTEN_ADDR points at, deploy guidance pins it to the Tailscale
interface (covered in 1d README).
Tests (skip when DB env is unset):
TestTreeRenders, TestHealthz,
TestDetailProjaxNativeEditable, TestDetailMaiProjectsReadOnly,
TestClassifyListsOrphans, TestPromoteRoundTrip.
|