max / goingson
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
15 files changed,
+506 insertions,
-70 deletions
| @@ -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 |