Skip to main content

max / goingson

Every view reads its contexts, and vacation_days stops being the fact The day plan asks which contexts frame the day and derives is_vacation_day from them, instead of reading a per-week projection off the weekly review. The weekly review's seven checkboxes still exist and now write spans: the week's Vacation contexts dissolve into days, the seven are set, and the rest re-run into spans, so a holiday running in from the Friday before keeps its earlier half and a run crossing Sunday stays one context. A context carrying a provenance mark is never dissolved by a tick, because deleting it would put its event back on the timeline behind the user's back. An event converted by migration 067 is gone from every view read. list_all is one of those, so the backup takes list_all_for_backup, which carries the hidden rows: a backup that dropped them would take the reversal with it. Same split and same name as tasks, emails and contacts. is_all_day stays and stops meaning anything about kind. It is whether a bar covers the column, which is why such a bar goes in the strip and why the conflict pass skips it; a context is not a timeline item at all and cannot clash with anything. The day view draws one banner per context rather than one hardcoded "Day Off" off a boolean. A trip, an illness and a sprint had no way to be said before.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-18 22:40 UTC
Signed with PGP, not checked
Commit: 7b6d7ffdb9a5df67e5fa62f3db374fdac0b5ce01
Parent: da17043
15 files changed, +506 insertions, -70 deletions
M Cargo.lock +8 -8
@@ -8496,6 +8496,14 @@
8496 8496 "winnow 1.0.4",
8497 8497 ]
8498 8498
8499 + [[patch.unused]]
8500 + name = "makeover-immediate"
8501 + version = "0.30.0"
8502 +
8503 + [[patch.unused]]
8504 + name = "ops-status"
8505 + version = "0.1.0"
8506 +
8499 8507 [[patch.unused]]
8500 8508 name = "quasi-axum"
8501 8509 version = "0.29.1"
@@ -8511,11 +8519,3 @@
8511 8519 [[patch.unused]]
8512 8520 name = "quasi-store"
8513 8521 version = "0.1.0"
8514 -
8515 - [[patch.unused]]
8516 - name = "ops-status"
8517 - version = "0.1.0"
8518 -
8519 - [[patch.unused]]
8520 - name = "makeover-immediate"
8521 - version = "0.30.0"
@@ -68,7 +68,7 @@
68 68 ) -> Result<FullExport, goingson_core::CoreError> {
69 69 let projects = state.projects.list_all(user_id)?;
70 70 let tasks = state.tasks.list_all_for_backup(user_id)?;
71 - let events = state.events.list_all(user_id)?;
71 + let events = state.events.list_all_for_backup(user_id)?;
72 72 let emails = state.emails.list_all_for_backup(user_id)?;
73 73 let contacts = state.contacts.list_all_for_backup(user_id)?;
74 74 let time_sessions = state.tasks.list_all_time_sessions(user_id)?;
@@ -142,7 +142,7 @@
142 142 ) -> Result<(), BackupError> {
143 143 stream_one(sink, "projects", state.projects.list_all(user_id))?;
144 144 stream_one(sink, "tasks", state.tasks.list_all_for_backup(user_id))?;
145 - stream_one(sink, "events", state.events.list_all(user_id))?;
145 + stream_one(sink, "events", state.events.list_all_for_backup(user_id))?;
146 146 stream_one(sink, "emails", state.emails.list_all_for_backup(user_id))?;
147 147 stream_one(
148 148 sink,
@@ -43,8 +43,15 @@
43 43 /// plan gate, say) needs this one instead, and inferring it from `item_type`
44 44 /// alone is not possible.
45 45 pub linked_task_id: Option<Uuid>,
46 - /// Covers the rendered day end to end. These go in the all-day strip above
47 - /// the timeline: drawn in the column they would paper over all 24 hours of it.
46 + /// Covers the rendered day end to end.
47 + ///
48 + /// Geometry, and only geometry: these go in the all-day strip above the
49 + /// timeline because a bar over all 24 hours would paper over everything
50 + /// under it. It is NOT a claim about what kind of thing the item is. It was
51 + /// read as one until 2026-08-18 -- a conference running Wed 14:00 to Fri
52 + /// 17:00 came out an occupancy on Wednesday, all-day on Thursday and an
53 + /// occupancy again on Friday -- and the thing it was standing in for is now
54 + /// its own record. See [`crate::models::Context`].
48 55 pub is_all_day: bool,
49 56 /// Minutes from local midnight to this item's visible start, 0 when it began
50 57 /// on an earlier day.
@@ -102,9 +109,10 @@
102 109 visible_duration_minutes,
103 110 continues_before: start < day_start,
104 111 continues_after: end > day_end_excl,
105 - // Covering the window is what "all day" means to the view. It catches an
106 - // authored midnight-to-midnight event and the middle day of a three-day
107 - // one alike, without either having to be flagged when it was written.
112 + // Covering the window is what "all day" means to the view, and all it
113 + // means. It catches an authored midnight-to-midnight event and the
114 + // middle day of a three-day one alike; that the two used to be treated
115 + // as the same kind of thing is what `Context` exists to end.
108 116 is_all_day: day_offset_minutes == 0 && visible_duration_minutes >= day_minutes,
109 117 }
110 118 }
@@ -124,10 +132,14 @@
124 132 /// Compares all pairs of items and returns any overlapping time ranges.
125 133 /// Items without an explicit end time use their duration (defaulting to 30 minutes).
126 134 ///
127 - /// All-day items are skipped. One of them overlaps everything on the day by
128 - /// definition, so counting it would mark the whole column as conflicting and the
129 - /// flag would stop meaning "these two double-book you", which is the only thing
130 - /// it is read for.
135 + /// Items covering the whole column are skipped. One of them overlaps everything
136 + /// on the day by definition, so counting it would mark the whole column as
137 + /// conflicting and the flag would stop meaning "these two double-book you",
138 + /// which is the only thing it is read for.
139 + ///
140 + /// That is a statement about a bar's width rather than about a kind of item. A
141 + /// context frames the day and is never here at all: it is not a timeline item,
142 + /// consumes no hours, and cannot clash with anything.
131 143 pub fn detect_conflicts(items: &[TimelineItem]) -> Vec<Conflict> {
132 144 let mut conflicts = Vec::new();
133 145
@@ -156,6 +156,13 @@
156 156 pub struct WeeklyReviewInput {
157 157 pub week_start: NaiveDate,
158 158 pub review: Option<WeeklyReview>,
159 + /// Which weekdays of this week are inside a `Vacation` context (0=Mon).
160 + ///
161 + /// Supplied by the caller rather than read off `review.vacation_days`,
162 + /// which is the legacy per-week projection. A context is a span and the
163 + /// caller is what can ask which days of this week fall inside one. See
164 + /// [`crate::models::Context`].
165 + pub vacation_days: Vec<u8>,
159 166 pub tasks_completed: Vec<Task>,
160 167 pub tasks_overdue: Vec<Task>,
161 168 pub events_occurred: Vec<Event>,
@@ -221,11 +228,7 @@
221 228
222 229 let week_start_dt = civil_midnight_utc(week_start, tz);
223 230
224 - let vacation_days: Vec<u8> = input
225 - .review
226 - .as_ref()
227 - .map(|r| r.vacation_days.clone())
228 - .unwrap_or_default();
231 + let vacation_days: Vec<u8> = input.vacation_days.clone();
229 232
230 233 // Pending tasks count and carried-over tasks
231 234 let pending_count = input
@@ -1010,6 +1013,9 @@
1010 1013 let data = compute_weekly_review(WeeklyReviewInput {
1011 1014 week_start,
1012 1015 review: Some(review.clone()),
1016 + // The days now come from the caller's context read rather than off
1017 + // the review row, so the test supplies them the same way.
1018 + vacation_days: vec![5, 6],
1013 1019 tasks_completed: vec![done.clone()],
1014 1020 tasks_overdue: vec![carried.clone()],
1015 1021 events_occurred: vec![occurred],
@@ -1070,6 +1076,7 @@
1070 1076 let data = compute_weekly_review(WeeklyReviewInput {
1071 1077 week_start,
1072 1078 review: None,
1079 + vacation_days: vec![],
1073 1080 tasks_completed: vec![],
1074 1081 tasks_overdue: vec![],
1075 1082 events_occurred: vec![],
@@ -5890,7 +5890,25 @@
5890 5890 text-decoration: line-through;
5891 5891 }
5892 5892
5893 - /* Vacation Day Banner (Day Plan) */
5893 + /* Context banners (Day Plan): the states framing a day.
5894 + One per context, drawn above the axis rather than on it, because a context
5895 + consumes none of the day's hours. `.vacation-day-banner` is the tone a
5896 + Vacation-kind context takes and keeps the colour this rule always had; every
5897 + other kind gets the neutral base below. See wiki `layout-room-and-fallback`'s
5898 + sibling ruling on goingson task `c5f74e0f`. */
5899 + .context-banner {
5900 + text-align: center;
5901 + padding: var(--gap-peer) var(--gap-group);
5902 + background: var(--surface-overlay);
5903 + border: var(--border-width-sm) solid var(--border);
5904 + border-radius: var(--radius-sm);
5905 + font-family: var(--font-heading);
5906 + font-weight: 700;
5907 + font-size: var(--font-size-base);
5908 + color: var(--content-secondary);
5909 + margin-bottom: var(--gap-section);
5910 + }
5911 +
5894 5912 .vacation-day-banner {
5895 5913 text-align: center;
5896 5914 padding: var(--gap-peer) var(--gap-group);
@@ -37,14 +37,20 @@
37 37 const slotsContainer = document.getElementById('timeline-slots');
38 38 const itemsContainer = document.getElementById('timeline-items');
39 39
40 - // Vacation banner
41 - const existingBanner = document.getElementById('vacation-day-banner');
42 - if (existingBanner) existingBanner.remove();
43 - if (dayPlanData?.isVacationDay) {
40 + // The states framing this day, one banner each. They sit behind the day
41 + // rather than on it: a context consumes none of its hours and never
42 + // appears on the axis. Until 2026-08-18 this was one hardcoded "Day Off"
43 + // off a flag on the weekly review, which could say that and nothing
44 + // else -- a trip, an illness and a sprint had no way to be said at all.
45 + for (const stale of document.querySelectorAll('.context-banner')) stale.remove();
46 + for (const context of dayPlanData?.contexts ?? []) {
44 47 const banner = document.createElement('div');
45 - banner.id = 'vacation-day-banner';
46 - banner.className = 'vacation-day-banner';
47 - banner.textContent = 'Day Off';
48 + // The old id and class stay: the stylesheet dresses them, and a
49 + // vacation is still what most of these are.
50 + banner.className = context.kind === 'Vacation'
51 + ? 'context-banner vacation-day-banner'
52 + : 'context-banner';
53 + banner.textContent = context.label;
48 54 slotsContainer.parentElement.insertBefore(banner, slotsContainer);
49 55 }
50 56