Unify calendar engines across /events and Custom Views #78

Open
opened 2026-05-25 11:23:29 +00:00 by mAi · 2 comments
Collaborator

m's report (2026-05-25 12:44)

https://paliad.de/events?type=appointment&view=calendar&cal_view=month&cal_date=2026-07-01

The calendar in the events view does not seem to be the same as in my custom view - are there different calendar engines? We need to unify this.

(Companion issue for the date-range filter simplification is separate — inventor design phase.)

Context

This duplicates / reinforces #55 ("Align calendar-view rendering between Events/Termine page and Custom Views' calendar view type"). m has now reported it again as a live bug — pick this up now.

What to do

  1. Identify both renderers:
    • /events?view=calendar → likely frontend/src/events.tsx + frontend/src/client/events.ts (calendar branch)
    • Custom-View calendar → frontend/src/client/views/calendar-view.ts or similar under the views directory
  2. Diff the two implementations: layout, header, navigation, event rendering, month/week/day modes, click behavior.
  3. Pick ONE canonical engine (whichever is more capable / has more tests). Coder's call but justify in the commit message.
  4. Extract the canonical renderer into a shared component (frontend/src/components/CalendarView.tsx or similar) consumed by both /events and the Custom-View calendar.
  5. Remove the orphaned renderer.
  6. Ensure both surfaces support the same controls (month/week/day, navigation, today button, click-to-edit if applicable).

Files most likely touched

  • frontend/src/events.tsx
  • frontend/src/client/events.ts
  • frontend/src/client/views/calendar-view.ts (or whatever the Custom-Views calendar lives in)
  • New shared component file
  • frontend/src/styles/global.css — consolidate any calendar-specific CSS

Hard rules

  • No behavior regression on either surface — both must keep their existing controls (filtering, navigation, etc.).
  • Date-range filtering pattern stays as-is for this issue (the symmetric picker is a separate inventor design).
  • go build ./... && go test ./internal/... && cd frontend && bun run build clean.
  • Branch: mai/<worker>/calendar-engine-unify.

Out of scope

  • The symmetric past/future date-range picker (separate inventor design).
  • Adding new calendar features (drag/drop event creation, etc.).
  • Changing how individual event rows look beyond what's needed for parity.

Reporting

mai report completed with branch + SHAs + the chosen canonical engine + verification path (open /events?view=calendar AND a Custom View with calendar layout → both render identically).

When filing the commit comment on this issue, also reference / close out the duplicate concern in #55 if appropriate (don't close the issue — set the done label instead per project convention).

## m's report (2026-05-25 12:44) > https://paliad.de/events?type=appointment&view=calendar&cal_view=month&cal_date=2026-07-01 > > The calendar in the events view does not seem to be the same as in my custom view - are there different calendar engines? We need to unify this. (Companion issue for the date-range filter simplification is separate — inventor design phase.) ## Context This duplicates / reinforces #55 ("Align calendar-view rendering between Events/Termine page and Custom Views' calendar view type"). m has now reported it again as a live bug — pick this up now. ## What to do 1. Identify both renderers: - `/events?view=calendar` → likely `frontend/src/events.tsx` + `frontend/src/client/events.ts` (calendar branch) - Custom-View calendar → `frontend/src/client/views/calendar-view.ts` or similar under the views directory 2. Diff the two implementations: layout, header, navigation, event rendering, month/week/day modes, click behavior. 3. Pick ONE canonical engine (whichever is more capable / has more tests). Coder's call but justify in the commit message. 4. Extract the canonical renderer into a shared component (`frontend/src/components/CalendarView.tsx` or similar) consumed by both `/events` and the Custom-View calendar. 5. Remove the orphaned renderer. 6. Ensure both surfaces support the same controls (month/week/day, navigation, today button, click-to-edit if applicable). ## Files most likely touched - `frontend/src/events.tsx` - `frontend/src/client/events.ts` - `frontend/src/client/views/calendar-view.ts` (or whatever the Custom-Views calendar lives in) - New shared component file - `frontend/src/styles/global.css` — consolidate any calendar-specific CSS ## Hard rules - No behavior regression on either surface — both must keep their existing controls (filtering, navigation, etc.). - Date-range filtering pattern stays as-is for this issue (the symmetric picker is a separate inventor design). - `go build ./... && go test ./internal/... && cd frontend && bun run build` clean. - Branch: `mai/<worker>/calendar-engine-unify`. ## Out of scope - The symmetric past/future date-range picker (separate inventor design). - Adding new calendar features (drag/drop event creation, etc.). - Changing how individual event rows look beyond what's needed for parity. ## Reporting `mai report completed` with branch + SHAs + the chosen canonical engine + verification path (open /events?view=calendar AND a Custom View with calendar layout → both render identically). When filing the commit comment on this issue, also reference / close out the duplicate concern in #55 if appropriate (don't close the issue — set the `done` label instead per project convention).
mAi self-assigned this 2026-05-25 11:23:29 +00:00
Author
Collaborator

Investigation — calendar engines are already unified (t-paliad-224 / m/paliad#55)

Before touching code I checked the live state on paliad.de + the worktree. Both /events?view=calendar and Custom-View shape=calendar already render through the same enginefrontend/src/client/calendar/mount-calendar.ts. t-paliad-224 (m/paliad#55) landed this on 2026-05-20 and is deployed.

Proof — both prod bundles ship the same mountCalendar code

Downloaded https://paliad.de/assets/events.js and https://paliad.de/assets/views.js and grepped for every calendar-feature string. Every one appears exactly once in each bundle (i.e. one copy of mountCalendar, no orphan):

String events.js views.js
views-calendar-toolbar 1 1
views-calendar-cell--today 1 1
views-calendar-pill--more 1 1
views-calendar-week-grid 1 1
views-calendar-day-list 1 1
views-calendar-mobile-notice 1 1
views-calendar-back-to-month 1 1
cal.day.no_entries 4 4

The minified bodies around views-calendar-toolbar are byte-equivalent modulo minifier variable letters (B vs _, D vs S, …). Same module compiled twice into two pages.

Source-side

  • frontend/src/client/events.ts:11 imports mountCalendar from ./calendar/mount-calendar.
  • frontend/src/client/events.ts:605 calls mountCalendar(host, items, { urlState: true, defaultView: "month" }).
  • frontend/src/client/views/shape-calendar.ts:12 calls mountCalendar(host, items, { defaultView: render.calendar?.default_view ?? "month", urlState: true }) — same module, same options.
  • The orphaned renderCalendar() / openCalPopup() / events-cal-dot-* code from before t-224 is gone (verified by grep -rn frist-cal\|events-cal returning nothing in frontend/src/).
  • CSS: only .views-calendar-* remains for the full-page calendar. .frist-cal-* was deleted.

What can still feel different to a user — and isn't engine

  1. Data source. /events?view=calendar calls /api/events and only returns rows of kind deadline and appointment — so the pill palette only ever shows the lime-deadline + blue-appointment dots. A Custom-View with sources=[deadline, appointment, project_event, approval_request] calls /api/views/<slug>/run and can render purple project_event and amber approval_request pills too. Same engine, different data.
  2. Chrome above the calendar. /events has the type/status/project/event-type filter row + view selector chips; a Custom View has the shape chips + filter bar from client/filter-bar/. Both surfaces sit above an identical .views-calendar element.
  3. Default-view spec. /events hard-codes defaultView: "month"; Custom Views honour render_spec.calendar.default_view (defaults to month, but a saved view can pin to week). If your view's spec stored a non-month default, you'd land on a different initial view even though the engine is the same.

Recommendation

The unify-the-engine work is done; the canonical engine is mount-calendar.ts and the orphans are removed. No code change shipped on this branch — there's nothing left to consolidate without first knowing what specific divergence you're seeing.

Could you post a screenshot of /events?type=appointment&view=calendar&cal_view=month&cal_date=2026-07-01 side-by-side with your custom view at the same anchor? If a real visual diff is still present I'll dig into it; if it turns out to be the data-source / chrome difference above, we'd want a different scope (e.g. let /events?view=calendar widen to include project_event + approval_request, or align the filter chrome).

No done label set — re-labeling to needs-review and leaving the floor with you.

— gitster (demeter)

## Investigation — calendar engines are already unified (t-paliad-224 / m/paliad#55) Before touching code I checked the live state on paliad.de + the worktree. **Both `/events?view=calendar` and Custom-View `shape=calendar` already render through the same engine** — `frontend/src/client/calendar/mount-calendar.ts`. t-paliad-224 (m/paliad#55) landed this on 2026-05-20 and is deployed. ### Proof — both prod bundles ship the same `mountCalendar` code Downloaded `https://paliad.de/assets/events.js` and `https://paliad.de/assets/views.js` and grepped for every calendar-feature string. Every one appears **exactly once in each bundle** (i.e. one copy of `mountCalendar`, no orphan): | String | events.js | views.js | |---|---|---| | `views-calendar-toolbar` | 1 | 1 | | `views-calendar-cell--today` | 1 | 1 | | `views-calendar-pill--more` | 1 | 1 | | `views-calendar-week-grid` | 1 | 1 | | `views-calendar-day-list` | 1 | 1 | | `views-calendar-mobile-notice` | 1 | 1 | | `views-calendar-back-to-month` | 1 | 1 | | `cal.day.no_entries` | 4 | 4 | The minified bodies around `views-calendar-toolbar` are byte-equivalent modulo minifier variable letters (B vs _, D vs S, …). Same module compiled twice into two pages. ### Source-side - `frontend/src/client/events.ts:11` imports `mountCalendar` from `./calendar/mount-calendar`. - `frontend/src/client/events.ts:605` calls `mountCalendar(host, items, { urlState: true, defaultView: "month" })`. - `frontend/src/client/views/shape-calendar.ts:12` calls `mountCalendar(host, items, { defaultView: render.calendar?.default_view ?? "month", urlState: true })` — same module, same options. - The orphaned `renderCalendar()` / `openCalPopup()` / `events-cal-dot-*` code from before t-224 is gone (verified by `grep -rn frist-cal\|events-cal` returning nothing in `frontend/src/`). - CSS: only `.views-calendar-*` remains for the full-page calendar. `.frist-cal-*` was deleted. ### What can still feel different to a user — and isn't engine 1. **Data source.** `/events?view=calendar` calls `/api/events` and only returns rows of kind `deadline` and `appointment` — so the pill palette only ever shows the lime-deadline + blue-appointment dots. A Custom-View with `sources=[deadline, appointment, project_event, approval_request]` calls `/api/views/<slug>/run` and can render purple `project_event` and amber `approval_request` pills too. **Same engine, different data.** 2. **Chrome above the calendar.** /events has the type/status/project/event-type filter row + view selector chips; a Custom View has the shape chips + filter bar from `client/filter-bar/`. Both surfaces sit above an identical `.views-calendar` element. 3. **Default-view spec.** /events hard-codes `defaultView: "month"`; Custom Views honour `render_spec.calendar.default_view` (defaults to `month`, but a saved view can pin to `week`). If your view's spec stored a non-month default, you'd land on a different initial view even though the engine is the same. ### Recommendation The unify-the-engine work is done; the canonical engine is `mount-calendar.ts` and the orphans are removed. **No code change shipped on this branch** — there's nothing left to consolidate without first knowing what specific divergence you're seeing. Could you post a screenshot of `/events?type=appointment&view=calendar&cal_view=month&cal_date=2026-07-01` side-by-side with your custom view at the same anchor? If a real visual diff is still present I'll dig into it; if it turns out to be the data-source / chrome difference above, we'd want a different scope (e.g. let `/events?view=calendar` widen to include `project_event` + `approval_request`, or align the filter chrome). No `done` label set — re-labeling to `needs-review` and leaving the floor with you. — gitster (demeter)
Author
Collaborator

Re-verification — engine still unified, build clean (hermes shift)

Picked this up after demeter's investigation on 2026-05-25 (#78 (comment)). Re-verified the state at the current main HEAD (d1d0cf9) — same conclusion: /events?view=calendar and Custom-View shape=calendar go through one engine, frontend/src/client/calendar/mount-calendar.ts, called from both surfaces.

Code-side proof (re-checked now)

Call site Path
/events Kalender chip frontend/src/client/events.ts:11 imports + :622 calls mountCalendar(host, items, { urlState: true, defaultView: "month" })
Custom-View shape=calendar frontend/src/client/views/shape-calendar.ts:12 calls mountCalendar(host, items, { defaultView: render.calendar?.default_view ?? "month", urlState: true })

No .frist-cal-* / .events-cal-* orphan classes anywhere in frontend/src (grep -rn). Only one orphan-y identifier remains — .events-calendar-wrap — and that's just the host <div> that mountCalendar paints into on /events. It's a 2-line margin rule (global.css:13353), not a separate engine.

Bundle-side proof (locally built + downloaded prod)

Every calendar-engine string appears exactly once per bundle in both my local bun run build output and the current prod bundles at https://paliad.de/assets/{events,views}.js:

String events.js views.js
views-calendar-toolbar 1 1
views-calendar-cell--today 1 1
views-calendar-pill--more 1 1
views-calendar-week-grid 1 1
views-calendar-day-list 1 1
views-calendar-mobile-notice 1 1
views-calendar-back-to-month 1 1
dashboard-cal-* (any) 0 0

The dashboard-cal-* row is the interesting one — see next section.

Build hygiene (per issue hard rules)

  • go build ./... — clean
  • go test ./internal/... — auth/branding/calc/changelog/db/handlers/services all ok
  • cd frontend && bun run build — clean (2892 keys, data-i18n attributes clean, dist/ written)

Nothing to ship on mai/hermes/calendar-engine-unify — branch is empty of commits.

The only other calendar engine in the codebase

Grepping frontend/src end-to-end, there's one other thing that paints a calendar: renderMiniCalendar() in frontend/src/client/dashboard.ts:602. It's the mini multi-month dot grid that the dashboard widgets (upcoming-deadlines, upcoming-appointments) render when their per-widget view is set to calendar. DOM is .dashboard-cal-*, not .views-calendar-*. Very different UX:

mountCalendar (/events + /views/{slug}) renderMiniCalendar (dashboard widgets)
Scope single month / week / day at a time multi-month grid (today's month → last item, max +2)
Toolbar view-switcher (Monat/Woche/Tag), prev/next/today, back-to-month none
Day cell clickable; opens a day-list with full pills non-clickable; up to 3 dots + +N more, each dot is the link
URL state ?cal_view=, ?cal_date= none
Source frontend/src/client/calendar/mount-calendar.ts (579 LoC) frontend/src/client/dashboard.ts:602-680 (~80 LoC)

It is a deliberately separate engine because the widget needs a glanceable, fixed-height card; mountCalendar's toolbar + full pills would not fit. Database check: no user currently has any widget set to view=calendar (SELECT … FROM paliad.user_dashboard_layouts WHERE layout_json::text ILIKE '%calendar%' → empty), so this isn't what you're seeing in your example URL either.

What could still look different — perceptually

  1. Data source. /events?type=appointment&view=calendar only renders rows of kind appointment (one pill colour). A Custom-View with sources=[deadline, appointment, project_event, approval_request] renders up to 4 pill colours on the same date. Same engine, different inputs.
  2. Chrome above the calendar. /events stacks: Type chips + view selector (Karten/Liste/Kalender) + the calendar toolbar. /views/{slug} stacks: shape chips + filter-bar + the calendar toolbar. Below the chrome, the .views-calendar-* element is identical.
  3. Default view. /events hard-codes defaultView: "month". Custom Views honour render_spec.calendar.default_view — a saved view can pin to week.
  4. Surface margin. .events-calendar-wrap has margin: 0.25rem 0 1rem; .views-shape-host has margin-top: 16px. Cosmetic only — could be aligned in a separate one-line PR if you want them visually flush.

Where this leaves us

The "unify the engines" work was already shipped by t-paliad-224 / m/paliad#55 (2026-05-20). I won't ship a no-op commit on mai/hermes/calendar-engine-unify — the branch is clean and the worktree retires here.

If you still see a visual diff between the two URLs in your report, the most useful thing for the next worker would be two screenshots at the same cal_date=2026-07-01 anchor — one of /events?type=appointment&view=calendar&cal_view=month&cal_date=2026-07-01 and one of whatever Custom-View URL you're comparing against. Then either:

  • (a) the diff is data / chrome / margin → tiny scoped fix on whichever side you want to win, or
  • (b) the diff turns out to be the dashboard mini-calendar → separate issue to consider unifying renderMiniCalendar into mountCalendar with a "mini" mode (non-trivial — different UX contract), or
  • (c) none of the above → this issue closes as resolved-by-t-224.

Labels: leaving as-is (only deferred / done exist on this repo, no needs-review). Per project convention I won't close the issue; flip to done when you've confirmed there's nothing left to chase.

— gitster (hermes), branch mai/hermes/gitster-unify-calendar, no commits.

Commit: n/a (no-op; verification only).

## Re-verification — engine still unified, build clean (hermes shift) Picked this up after demeter's investigation on 2026-05-25 (https://mgit.msbls.de/m/paliad/issues/78#issuecomment-9523). Re-verified the state at the current main HEAD (`d1d0cf9`) — same conclusion: `/events?view=calendar` and Custom-View `shape=calendar` go through **one** engine, `frontend/src/client/calendar/mount-calendar.ts`, called from both surfaces. ### Code-side proof (re-checked now) | Call site | Path | |---|---| | `/events` Kalender chip | `frontend/src/client/events.ts:11` imports + `:622` calls `mountCalendar(host, items, { urlState: true, defaultView: "month" })` | | Custom-View `shape=calendar` | `frontend/src/client/views/shape-calendar.ts:12` calls `mountCalendar(host, items, { defaultView: render.calendar?.default_view ?? "month", urlState: true })` | No `.frist-cal-*` / `.events-cal-*` orphan classes anywhere in `frontend/src` (`grep -rn`). Only one orphan-y identifier remains — `.events-calendar-wrap` — and that's just the **host `<div>`** that mountCalendar paints into on /events. It's a 2-line margin rule (`global.css:13353`), not a separate engine. ### Bundle-side proof (locally built + downloaded prod) Every calendar-engine string appears **exactly once** per bundle in both my local `bun run build` output and the current prod bundles at `https://paliad.de/assets/{events,views}.js`: | String | events.js | views.js | |---|---|---| | `views-calendar-toolbar` | 1 | 1 | | `views-calendar-cell--today` | 1 | 1 | | `views-calendar-pill--more` | 1 | 1 | | `views-calendar-week-grid` | 1 | 1 | | `views-calendar-day-list` | 1 | 1 | | `views-calendar-mobile-notice` | 1 | 1 | | `views-calendar-back-to-month` | 1 | 1 | | `dashboard-cal-*` (any) | **0** | **0** | The `dashboard-cal-*` row is the interesting one — see next section. ### Build hygiene (per issue hard rules) - `go build ./...` — clean - `go test ./internal/...` — auth/branding/calc/changelog/db/handlers/services all `ok` - `cd frontend && bun run build` — clean (`2892 keys, data-i18n attributes clean, dist/ written`) Nothing to ship on `mai/hermes/calendar-engine-unify` — branch is empty of commits. ### The only other calendar engine in the codebase Grepping `frontend/src` end-to-end, there's **one** other thing that paints a calendar: `renderMiniCalendar()` in `frontend/src/client/dashboard.ts:602`. It's the **mini multi-month dot grid** that the **dashboard widgets** (`upcoming-deadlines`, `upcoming-appointments`) render when their per-widget view is set to `calendar`. DOM is `.dashboard-cal-*`, not `.views-calendar-*`. Very different UX: | | mountCalendar (`/events` + `/views/{slug}`) | renderMiniCalendar (dashboard widgets) | |---|---|---| | Scope | single month / week / day at a time | multi-month grid (today's month → last item, max +2) | | Toolbar | view-switcher (Monat/Woche/Tag), prev/next/today, back-to-month | none | | Day cell | clickable; opens a day-list with full pills | non-clickable; up to 3 dots + `+N` more, each dot is the link | | URL state | `?cal_view=`, `?cal_date=` | none | | Source | `frontend/src/client/calendar/mount-calendar.ts` (579 LoC) | `frontend/src/client/dashboard.ts:602-680` (~80 LoC) | It is a deliberately separate engine because the widget needs a glanceable, fixed-height card; mountCalendar's toolbar + full pills would not fit. **Database check:** no user currently has any widget set to view=`calendar` (`SELECT … FROM paliad.user_dashboard_layouts WHERE layout_json::text ILIKE '%calendar%'` → empty), so this isn't what you're seeing in your example URL either. ### What could still look different — perceptually 1. **Data source.** `/events?type=appointment&view=calendar` only renders rows of kind `appointment` (one pill colour). A Custom-View with `sources=[deadline, appointment, project_event, approval_request]` renders up to **4** pill colours on the same date. Same engine, different inputs. 2. **Chrome above the calendar.** /events stacks: Type chips + view selector (Karten/Liste/Kalender) + the calendar toolbar. /views/{slug} stacks: shape chips + filter-bar + the calendar toolbar. Below the chrome, the `.views-calendar-*` element is identical. 3. **Default view.** /events hard-codes `defaultView: "month"`. Custom Views honour `render_spec.calendar.default_view` — a saved view can pin to `week`. 4. **Surface margin.** `.events-calendar-wrap` has `margin: 0.25rem 0 1rem`; `.views-shape-host` has `margin-top: 16px`. Cosmetic only — could be aligned in a separate one-line PR if you want them visually flush. ### Where this leaves us The "unify the engines" work was already shipped by t-paliad-224 / m/paliad#55 (2026-05-20). I won't ship a no-op commit on `mai/hermes/calendar-engine-unify` — the branch is clean and the worktree retires here. If you still see a visual diff between the two URLs in your report, the most useful thing for the next worker would be **two screenshots at the same `cal_date=2026-07-01` anchor** — one of `/events?type=appointment&view=calendar&cal_view=month&cal_date=2026-07-01` and one of whatever Custom-View URL you're comparing against. Then either: - **(a)** the diff is data / chrome / margin → tiny scoped fix on whichever side you want to win, **or** - **(b)** the diff turns out to be the dashboard mini-calendar → separate issue to consider unifying `renderMiniCalendar` into `mountCalendar` with a "mini" mode (non-trivial — different UX contract), **or** - **(c)** none of the above → this issue closes as resolved-by-t-224. Labels: leaving as-is (only `deferred` / `done` exist on this repo, no `needs-review`). Per project convention I won't close the issue; flip to `done` when you've confirmed there's nothing left to chase. — gitster (hermes), branch `mai/hermes/gitster-unify-calendar`, no commits. Commit: n/a (no-op; verification only).
mAi added the
done
label 2026-05-26 13:00:28 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#78
No description provided.