Plan the Panama calendar agenda
This commit is contained in:
@@ -0,0 +1,811 @@
|
|||||||
|
# Calendar Agenda Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build the approved A · Daybook date menu using the workstation's existing Evolution Data Server calendars, with a quiet upcoming-event capsule and GNOME Calendar handoff.
|
||||||
|
|
||||||
|
**Architecture:** A Python/GObject bridge queries enabled EDS calendar sources through `EDataServer` and `ECal`, expands recurring instances, and streams normalized JSON snapshots. A Quickshell singleton owns that stream and exposes date/event selectors to a two-column Daybook, while GNOME Calendar remains responsible for accounts and event editing.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, PyGObject, Evolution Data Server 3.60 (`EDataServer`, `ECal`, `ICalGLib`), Quickshell 0.3/QML, Gio D-Bus, Bash contract tests, jq, Hyprland 0.56
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-17-calendar-agenda-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Use only enabled EDS event sources; never read authentication extensions or credential stores.
|
||||||
|
- GNOME Calendar owns account configuration and event editing.
|
||||||
|
- Live event content must never be written to logs, settings, test fixtures, screenshots committed to the repository, or documentation.
|
||||||
|
- The bridge may emit event content only to its Quickshell consumer over stdout.
|
||||||
|
- Descriptions and attendees never enter the bridge output; descriptions may only be inspected to locate an allowed HTTPS meeting URL.
|
||||||
|
- Allowed meeting providers are Google Meet, Zoom, Microsoft Teams, Webex, and Jitsi.
|
||||||
|
- Calendar failures must not break notifications, media, weather, or the static month grid.
|
||||||
|
- The bar capsule appears from 15 minutes before through 5 minutes after a timed event starts; all-day events never produce it.
|
||||||
|
- No calendar toasts, sounds, pulse, shimmer, or continuously repainting animation.
|
||||||
|
- No test may create, modify, or delete a real calendar event.
|
||||||
|
- Wi-Fi QR sharing is removed from Panama's roadmap and package guidance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File map
|
||||||
|
|
||||||
|
### Calendar data boundary
|
||||||
|
|
||||||
|
- `config/dot/quickshell/scripts/calendar-agenda` — executable Python bridge; the only code that imports EDS libraries.
|
||||||
|
- `config/dot/quickshell/services/CalendarAgenda.qml` — singleton state consumed by shell UI; supervises the bridge and owns date-derived selectors.
|
||||||
|
- `tests/quickshell/calendar_agenda_bridge_test.py` — pure normalization and URL-policy tests using synthetic components only.
|
||||||
|
- `tests/quickshell/calendar-agenda-helper-contract.sh` — executable/probe/live-query contract that redacts live content.
|
||||||
|
- `tests/quickshell/calendar-agenda-contract.sh` — live Quickshell IPC/state contract using synthetic in-memory fixtures.
|
||||||
|
|
||||||
|
### Daybook presentation
|
||||||
|
|
||||||
|
- `config/dot/quickshell/modules/datemenu/AgendaEvent.qml` — one event row.
|
||||||
|
- `config/dot/quickshell/modules/datemenu/AgendaPane.qml` — Up Next, all-day section, timed agenda, empty/error states, and notification summary.
|
||||||
|
- `config/dot/quickshell/modules/datemenu/NotificationSummary.qml` — quiet bridge from Agenda to the existing grouped notification list.
|
||||||
|
- `config/dot/quickshell/modules/datemenu/DateMenuPanel.qml` — approved two-column layout and agenda/notification routing.
|
||||||
|
- `config/dot/quickshell/modules/datemenu/CalendarGrid.qml` — date selection, adjacent-month navigation, and event dots.
|
||||||
|
- `config/dot/quickshell/modules/datemenu/DateMenu.qml` — panel sizing, preparation, and notification read-state behavior.
|
||||||
|
- `config/dot/quickshell/modules/datemenu/qmldir` — explicit registrations for new components.
|
||||||
|
|
||||||
|
### Shell and bar integration
|
||||||
|
|
||||||
|
- `config/dot/quickshell/services/ShellState.qml` — `dateMenuPage` and explicit Agenda/Notifications open semantics.
|
||||||
|
- `config/dot/quickshell/modules/bar/Clock.qml` — clock opens Agenda rather than an undifferentiated notification overlay.
|
||||||
|
- `config/dot/quickshell/modules/bar/CalendarIndicator.qml` — quiet 15-minute capsule.
|
||||||
|
- `config/dot/quickshell/modules/bar/Bar.qml` — mounts the capsule in the right status area.
|
||||||
|
- `config/dot/quickshell/shell.qml` — calendar IPC fixture/diagnostics surface and notification-page routing.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `config/dot/hypr/DESKTOP-PARITY.md` — calendar becomes Live and Wi-Fi QR is removed.
|
||||||
|
- `config/dot/hypr/README.md` — EDS/GNOME Calendar handoff and capsule behavior.
|
||||||
|
- `setup/packages/hyprland-packages` — remove `qrencode` if it is present only for the abandoned Wi-Fi QR feature.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: EDS calendar bridge
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `config/dot/quickshell/scripts/calendar-agenda`
|
||||||
|
- Create: `tests/quickshell/calendar_agenda_bridge_test.py`
|
||||||
|
- Create: `tests/quickshell/calendar-agenda-helper-contract.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: enabled sources returned by `EDataServer.SourceRegistry.list_enabled(EDataServer.SOURCE_EXTENSION_CALENDAR)`.
|
||||||
|
- Produces: `calendar-agenda probe`, `calendar-agenda query START END`, and `calendar-agenda watch START END`.
|
||||||
|
- Produces: one-line JSON snapshots with `ok`, `generatedAt`, `sources`, `events`, and `errors`.
|
||||||
|
- Produces event objects with exactly `id`, `sourceId`, `uid`, `summary`, `start`, `end`, `allDay`, `location`, and `joinUrl`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing pure bridge tests**
|
||||||
|
|
||||||
|
Create `tests/quickshell/calendar_agenda_bridge_test.py` with import-by-path and synthetic policy cases:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import importlib.machinery
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||||
|
HELPER = ROOT / "config/dot/quickshell/scripts/calendar-agenda"
|
||||||
|
|
||||||
|
loader = importlib.machinery.SourceFileLoader("calendar_agenda", str(HELPER))
|
||||||
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||||
|
calendar_agenda = importlib.util.module_from_spec(spec)
|
||||||
|
loader.exec_module(calendar_agenda)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarAgendaBridgeTest(unittest.TestCase):
|
||||||
|
def test_meeting_url_allowlist(self):
|
||||||
|
values = ["Planning: https://meet.google.com/abc-defg-hij"]
|
||||||
|
self.assertEqual(calendar_agenda.find_join_url(values), values[0].split()[-1])
|
||||||
|
self.assertEqual(calendar_agenda.find_join_url(["https://example.com/private"]), "")
|
||||||
|
self.assertEqual(calendar_agenda.find_join_url(["http://zoom.us/j/123"]), "")
|
||||||
|
|
||||||
|
def test_stable_instance_id_includes_recurrence(self):
|
||||||
|
self.assertEqual(
|
||||||
|
calendar_agenda.stable_event_id("source", "uid", "20260817T133000Z"),
|
||||||
|
"source:uid:20260817T133000Z",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_snapshot_sort_order(self):
|
||||||
|
events = [
|
||||||
|
{"id": "b", "start": 20, "end": 30, "allDay": False},
|
||||||
|
{"id": "a", "start": 10, "end": 20, "allDay": True},
|
||||||
|
]
|
||||||
|
self.assertEqual([event["id"] for event in calendar_agenda.sort_events(events)], ["a", "b"])
|
||||||
|
|
||||||
|
def test_normalizes_an_all_day_recurrence_instance(self):
|
||||||
|
component_id = Mock()
|
||||||
|
component_id.get_uid.return_value = "uid"
|
||||||
|
component_id.get_rid.return_value = "20260817"
|
||||||
|
summary = Mock()
|
||||||
|
summary.get_value.return_value = "Synthetic event"
|
||||||
|
component = Mock()
|
||||||
|
component.get_id.return_value = component_id
|
||||||
|
component.get_summary.return_value = summary
|
||||||
|
component.get_location.return_value = ""
|
||||||
|
component.get_url.return_value = None
|
||||||
|
component.get_descriptions.return_value = []
|
||||||
|
start = Mock()
|
||||||
|
start.as_timet.return_value = 1786924800
|
||||||
|
start.is_date.return_value = True
|
||||||
|
end = Mock()
|
||||||
|
end.as_timet.return_value = 1787011200
|
||||||
|
end.is_date.return_value = True
|
||||||
|
|
||||||
|
event = calendar_agenda.normalize_instance(
|
||||||
|
{"id": "source", "name": "Synthetic", "color": "#82aaff"},
|
||||||
|
component,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
)
|
||||||
|
self.assertTrue(event["allDay"])
|
||||||
|
self.assertEqual(event["id"], "source:uid:20260817")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the unit test and verify RED**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tests/quickshell/calendar_agenda_bridge_test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because `config/dot/quickshell/scripts/calendar-agenda` does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the bridge's pure contract and CLI boundary**
|
||||||
|
|
||||||
|
Create `config/dot/quickshell/scripts/calendar-agenda` with a Python shebang and no top-level execution on import. Begin with these pure helpers:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MEETING_HOST_SUFFIXES = (
|
||||||
|
"meet.google.com",
|
||||||
|
"zoom.us",
|
||||||
|
"teams.microsoft.com",
|
||||||
|
"teams.live.com",
|
||||||
|
"webex.com",
|
||||||
|
"meet.jit.si",
|
||||||
|
)
|
||||||
|
|
||||||
|
def stable_event_id(source_id: str, uid: str, recurrence_id: str) -> str:
|
||||||
|
return f"{source_id}:{uid}:{recurrence_id}"
|
||||||
|
|
||||||
|
def sort_events(events: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||||
|
return sorted(events, key=lambda event: (not bool(event["allDay"]), int(event["start"]), str(event["id"])))
|
||||||
|
|
||||||
|
def is_allowed_meeting_host(host: str) -> bool:
|
||||||
|
normalized = host.lower().rstrip(".")
|
||||||
|
return any(normalized == suffix or normalized.endswith("." + suffix) for suffix in MEETING_HOST_SUFFIXES)
|
||||||
|
```
|
||||||
|
|
||||||
|
`find_join_url` must parse URLs with `urllib.parse.urlsplit`, require `https`, lowercase the hostname, and match either an exact allowed host or a subdomain boundary (`host == suffix or host.endswith("." + suffix)`). Strip trailing sentence punctuation before parsing.
|
||||||
|
|
||||||
|
Then implement these exact public boundaries: `find_join_url(values: list[str]) -> str`, `normalize_instance(source_meta, component, instance_start, instance_end) -> dict[str, object]`, `probe() -> dict[str, object]`, `collect_snapshot(start: int, end: int) -> dict[str, object]`, `watch(start: int, end: int) -> int`, and `main(argv: list[str]) -> int`.
|
||||||
|
|
||||||
|
Import GI with exact version requirements:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import gi
|
||||||
|
gi.require_version("ECal", "2.0")
|
||||||
|
gi.require_version("EDataServer", "1.2")
|
||||||
|
from gi.repository import ECal, EDataServer, Gio, GLib
|
||||||
|
```
|
||||||
|
|
||||||
|
For each enabled source:
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = ECal.Client.connect_sync(source, ECal.ClientSourceType.EVENTS, 5, None)
|
||||||
|
client.generate_instances_sync(start, end, None, on_instance, None)
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback signature is `(component, instance_start, instance_end, cancellable, callback_data)`. Convert callback times with `as_timet()`. Use `component.get_id().get_uid()` and `.get_rid()`, `component.get_summary().get_value()`, `component.get_location()`, `component.get_url()`, and `component.get_descriptions()`.
|
||||||
|
|
||||||
|
Use source metadata from `source.get_uid()`, `source.get_display_name()`, and the calendar extension's `get_color()`. Fall back to `#82aaff` only when color is absent or invalid. Catch failures per source and append a generic `{sourceId, code: "unavailable"}` error without raw exception text.
|
||||||
|
|
||||||
|
`watch` must emit the initial snapshot, subscribe to EDS source-registry changes and GNOME CalendarServer invalidation signals when available, debounce refreshes by 350 ms, and emit a fallback snapshot every 600 seconds. Failure to subscribe to GNOME CalendarServer must not stop periodic EDS reads.
|
||||||
|
|
||||||
|
Write each snapshot with `json.dumps(snapshot, separators=(",", ":")) + "\n"` and immediately flush stdout. Never print event data to stderr.
|
||||||
|
|
||||||
|
Make the helper executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x config/dot/quickshell/scripts/calendar-agenda
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run pure tests and verify GREEN**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tests/quickshell/calendar_agenda_bridge_test.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: four tests PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add the redacted live helper contract**
|
||||||
|
|
||||||
|
Create `tests/quickshell/calendar-agenda-helper-contract.sh` with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
fail() { printf 'calendar agenda helper contract: %s\n' "$1" >&2; exit 1; }
|
||||||
|
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
helper="$project_root/config/dot/quickshell/scripts/calendar-agenda"
|
||||||
|
|
||||||
|
[[ -x "$helper" ]] || fail 'helper is missing or not executable'
|
||||||
|
probe="$($helper probe)"
|
||||||
|
jq -e '.eds == true and .sourceRegistry == true and .enabledSources > 0' <<<"$probe" >/dev/null \
|
||||||
|
|| fail 'EDS readiness probe failed'
|
||||||
|
|
||||||
|
range_start="$(date +%s)"
|
||||||
|
range_end="$((range_start + 1209600))"
|
||||||
|
snapshot="$($helper query "$range_start" "$range_end")"
|
||||||
|
jq -e '
|
||||||
|
.ok == true and
|
||||||
|
(.sources | type == "array" and length > 0) and
|
||||||
|
(.events | type == "array") and
|
||||||
|
([.events[] | keys_unsorted - ["id","sourceId","uid","summary","start","end","allDay","location","joinUrl"]] | all(length == 0))
|
||||||
|
' <<<"$snapshot" >/dev/null || fail 'live snapshot shape is invalid'
|
||||||
|
|
||||||
|
printf 'calendar agenda helper contract: PASS (%s sources, %s events; contents redacted)\n' \
|
||||||
|
"$(jq '.sources | length' <<<"$snapshot")" "$(jq '.events | length' <<<"$snapshot")"
|
||||||
|
```
|
||||||
|
|
||||||
|
Make it executable and run it. Expected: PASS with counts only; no source names, summaries, locations, or URLs in terminal output.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the bridge**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/quickshell/scripts/calendar-agenda \
|
||||||
|
tests/quickshell/calendar_agenda_bridge_test.py \
|
||||||
|
tests/quickshell/calendar-agenda-helper-contract.sh
|
||||||
|
git commit -m "Add the EDS calendar bridge"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Quickshell calendar service and deterministic contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `config/dot/quickshell/services/CalendarAgenda.qml`
|
||||||
|
- Create: `tests/quickshell/calendar-agenda-contract.sh`
|
||||||
|
- Modify: `config/dot/quickshell/shell.qml`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `calendar-agenda watch START END` newline-delimited snapshots.
|
||||||
|
- Produces: singleton properties `phase`, `available`, `sources`, `events`, `selectedDate`, `selectedDateKey`, `selectedEvents`, `nextEvent`, `nextEventForSelectedDate`, `rangeStart`, `rangeEnd`, `capsuleVisible`, and `capsuleText`.
|
||||||
|
- Produces: functions `selectDate(date)`, `selectIsoDate(dateString)`, `setVisibleMonth(year, month)`, `eventsForDate(date)`, `markersForDate(date)`, `openEvent(event)`, `openCalendar(date)`, `join(event)`, `refresh()`, `applyFixture(name)`, and `clearFixture()`.
|
||||||
|
- Produces IPC target `calendar-agenda` with `fixture`, `reset`, `open`, `select`, `refresh`, and `status`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing live service contract**
|
||||||
|
|
||||||
|
Create `tests/quickshell/calendar-agenda-contract.sh` that first asserts the IPC target, loads fixture `daybook`, and validates only synthetic state:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
fail() { printf 'calendar agenda contract: %s\n' "$1" >&2; exit 1; }
|
||||||
|
cleanup() {
|
||||||
|
qs ipc call calendar-agenda reset >/dev/null 2>&1 || true
|
||||||
|
qs ipc call calendar-agenda close >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
qs ipc show | rg -q '^target calendar-agenda$' || fail 'calendar IPC target is missing'
|
||||||
|
qs ipc call calendar-agenda fixture daybook >/dev/null
|
||||||
|
status="$(qs ipc call calendar-agenda status)"
|
||||||
|
jq -e '.fixture == true and .sourceCount == 3 and .eventCount == 5 and .selectedDate == "2026-08-17"' \
|
||||||
|
<<<"$status" >/dev/null || fail 'daybook fixture state is malformed'
|
||||||
|
|
||||||
|
qs ipc call calendar-agenda select 2026-08-18 >/dev/null
|
||||||
|
jq -e '.selectedDate == "2026-08-18" and .selectedEventCount == 1' \
|
||||||
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null || fail 'date selection did not update events'
|
||||||
|
|
||||||
|
qs ipc call calendar-agenda reset >/dev/null
|
||||||
|
jq -e '.fixture == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'fixture reset did not restore live mode'
|
||||||
|
printf 'calendar agenda contract: PASS\n'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the contract and verify RED**
|
||||||
|
|
||||||
|
Run `tests/quickshell/calendar-agenda-contract.sh`.
|
||||||
|
|
||||||
|
Expected: FAIL with `calendar IPC target is missing`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement `CalendarAgenda.qml` stream supervision**
|
||||||
|
|
||||||
|
Create the singleton with `pragma Singleton`, `Quickshell`, `Quickshell.Io`, and `QtQuick` imports. Use a streaming parser, not `StdioCollector`:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
Process {
|
||||||
|
id: watchProc
|
||||||
|
command: [root.helperPath, "watch", String(root.rangeStart), String(root.rangeEnd)]
|
||||||
|
running: !root.fixtureMode && root.rangeStart > 0
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => root.consumeSnapshot(data)
|
||||||
|
}
|
||||||
|
onExited: (code, status) => {
|
||||||
|
if (!root.fixtureMode)
|
||||||
|
restartTimer.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The restart timer uses 2 seconds and does not loop while fixture mode is active. `consumeSnapshot(data)` parses one line, rejects snapshots without `ok === true`, assigns arrays atomically, and sets `phase` to `ready` or `degraded` when `errors.length > 0`.
|
||||||
|
|
||||||
|
Implement date keys in local time with `Qt.formatDate(new Date(epoch * 1000), "yyyy-MM-dd")`. For multi-day/all-day markers, treat `end` as exclusive and include each local date touched before `end - 1`.
|
||||||
|
|
||||||
|
`setVisibleMonth(year, month)` computes a range from seven days before the first day through seven days after the final 42-cell grid day. Only restart the bridge when the requested range falls outside the loaded range.
|
||||||
|
|
||||||
|
`nextEvent` excludes all-day and ended events. `capsuleVisible` is true when `start - now` is at most 900 seconds and at least -300 seconds. `capsuleText` is `Now` after start, otherwise the ceiling minute count plus `m`.
|
||||||
|
|
||||||
|
`applyFixture("daybook")` must install three synthetic sources and five synthetic events spanning August 17–18, 2026. Use clearly synthetic labels (`Fixture standup`, `Fixture planning`, `Fixture focus`, `Fixture dinner`, `Fixture tomorrow`) and no real account or event data.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add calendar IPC without exposing live content**
|
||||||
|
|
||||||
|
Add to `shell.qml`:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
IpcHandler {
|
||||||
|
target: "calendar-agenda"
|
||||||
|
function fixture(name: string): void { CalendarAgenda.applyFixture(name); }
|
||||||
|
function reset(): void { CalendarAgenda.clearFixture(); }
|
||||||
|
function refresh(): void { CalendarAgenda.refresh(); }
|
||||||
|
function open(): void { ShellState.openDateMenu("agenda"); }
|
||||||
|
function close(): void { ShellState.close(); }
|
||||||
|
function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
|
||||||
|
function status(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
phase: CalendarAgenda.phase,
|
||||||
|
fixture: CalendarAgenda.fixtureMode,
|
||||||
|
sourceCount: CalendarAgenda.sources.length,
|
||||||
|
eventCount: CalendarAgenda.events.length,
|
||||||
|
selectedDate: CalendarAgenda.selectedDateKey,
|
||||||
|
selectedEventCount: CalendarAgenda.selectedEvents.length,
|
||||||
|
capsuleVisible: CalendarAgenda.capsuleVisible,
|
||||||
|
capsuleText: CalendarAgenda.capsuleText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not include titles, source names, locations, identifiers, or URLs in status output.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run service contracts and verify GREEN**
|
||||||
|
|
||||||
|
Allow Quickshell to hot reload, then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
tests/quickshell/calendar-agenda-helper-contract.sh
|
||||||
|
qs log -n 80 | rg -i 'calendar|error|warn'
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: both contracts PASS; no calendar QML error or bridge restart loop.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the service**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/quickshell/services/CalendarAgenda.qml \
|
||||||
|
config/dot/quickshell/shell.qml \
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
git commit -m "Add live calendar state to Quickshell"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Explicit date-menu routing and interactive month grid
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `config/dot/quickshell/services/ShellState.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/bar/Clock.qml`
|
||||||
|
- Modify: `config/dot/quickshell/shell.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/datemenu/CalendarGrid.qml`
|
||||||
|
- Modify: `tests/quickshell/calendar-agenda-contract.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `CalendarAgenda.selectDate(date)` and `CalendarAgenda.markersForDate(date)`.
|
||||||
|
- Produces: `ShellState.dateMenuPage`, `ShellState.toggleDateMenu(page)`, and `ShellState.openDateMenu(page)`.
|
||||||
|
- Produces from CalendarGrid: signals `dateActivated(date)` and `visibleMonthChanged(year, month)`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend the failing contract with page-routing assertions**
|
||||||
|
|
||||||
|
Append fixture assertions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qs ipc call calendar-agenda open >/dev/null
|
||||||
|
jq -e '.open == true and .page == "agenda"' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'calendar open did not route to Agenda'
|
||||||
|
|
||||||
|
qs ipc call notifications open >/dev/null
|
||||||
|
jq -e '.open == true and .page == "notifications"' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'notification open did not route to Notifications'
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the calendar status response to include only `open: ShellState.notificationsOpen` and `page: ShellState.dateMenuPage` in addition to the non-sensitive fields from Task 2.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the contract and verify RED**
|
||||||
|
|
||||||
|
Expected: FAIL because `dateMenuPage` and notification `open()` do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement explicit ShellState routing**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
property string dateMenuPage: "agenda"
|
||||||
|
|
||||||
|
function openDateMenu(page: string): void {
|
||||||
|
root.dateMenuPage = page === "notifications" ? "notifications" : "agenda";
|
||||||
|
root.activeOverlay = "notifications";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDateMenu(page: string): void {
|
||||||
|
const target = page === "notifications" ? "notifications" : "agenda";
|
||||||
|
if (root.activeOverlay === "notifications" && root.dateMenuPage === target) {
|
||||||
|
root.activeOverlay = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.dateMenuPage = target;
|
||||||
|
root.activeOverlay = "notifications";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Change Clock activation to `ShellState.toggleDateMenu("agenda")`. Change notification IPC `toggle()` to `ShellState.toggleDateMenu("notifications")`, add `open()` using `openDateMenu("notifications")`, and leave `Super+B` routed through that IPC target.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Make CalendarGrid date-aware and event-aware**
|
||||||
|
|
||||||
|
Replace numeric-only cell objects with full local `Date` values and add:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
signal dateActivated(date date)
|
||||||
|
signal visibleMonthChanged(int year, int month)
|
||||||
|
property date selectedDate: CalendarAgenda.selectedDate
|
||||||
|
|
||||||
|
function activateCell(cell): void {
|
||||||
|
if (!cell.current) {
|
||||||
|
root.viewYear = cell.date.getFullYear();
|
||||||
|
root.viewMonth = cell.date.getMonth();
|
||||||
|
}
|
||||||
|
root.dateActivated(cell.date);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every cell receives `markers: CalendarAgenda.markersForDate(cell.date).slice(0, 3)`. Render dots in a centered row beneath the day number with each marker's source color. Today remains solid accent; selected non-today uses `Theme.alpha(Theme.accent, 0.18)` plus accent text. Add a hover surface only now that cells are actionable.
|
||||||
|
|
||||||
|
Month changes call `visibleMonthChanged(viewYear, viewMonth)`. Adjacent-month date activation changes month and selection in one click.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify routing and grid contracts**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
rg -q 'markersForDate' config/dot/quickshell/modules/datemenu/CalendarGrid.qml
|
||||||
|
rg -q 'dateActivated' config/dot/quickshell/modules/datemenu/CalendarGrid.qml
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: contract PASS and both interaction hooks present.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit routing and grid behavior**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/quickshell/services/ShellState.qml \
|
||||||
|
config/dot/quickshell/modules/bar/Clock.qml \
|
||||||
|
config/dot/quickshell/shell.qml \
|
||||||
|
config/dot/quickshell/modules/datemenu/CalendarGrid.qml \
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
git commit -m "Route the interactive Panama date menu"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Build the approved A · Daybook panel
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `config/dot/quickshell/modules/datemenu/AgendaEvent.qml`
|
||||||
|
- Create: `config/dot/quickshell/modules/datemenu/AgendaPane.qml`
|
||||||
|
- Create: `config/dot/quickshell/modules/datemenu/NotificationSummary.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/datemenu/DateMenuPanel.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/datemenu/DateMenu.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/datemenu/MediaCard.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/datemenu/qmldir`
|
||||||
|
- Modify: `tests/quickshell/calendar-agenda-contract.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `CalendarAgenda.selectedEvents`, `nextEventForSelectedDate`, `phase`, and action functions.
|
||||||
|
- Consumes: `ShellState.dateMenuPage`, `Notifs.hasNotifications`, and existing `NotificationList`.
|
||||||
|
- Produces: a 760-pixel two-column date menu with left width 310 and independently bounded right content.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing structural and live-surface assertions**
|
||||||
|
|
||||||
|
Extend the contract to assert the loaded layer and approved components:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hyprctl layers | rg -q 'namespace: qs-popover-datemenu' || fail 'Daybook layer did not map'
|
||||||
|
rg -q 'implicitWidth: 760' config/dot/quickshell/modules/datemenu/DateMenu.qml \
|
||||||
|
|| fail 'Daybook is not the approved width'
|
||||||
|
for component in AgendaPane AgendaEvent NotificationSummary; do
|
||||||
|
rg -q "${component} 1.0 ${component}.qml" config/dot/quickshell/modules/datemenu/qmldir \
|
||||||
|
|| fail "$component is not registered"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the contract and expect failure on width or missing components.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement focused event and summary components**
|
||||||
|
|
||||||
|
`AgendaEvent.qml` accepts `property var event`, `property bool past`, and signals `activated` and `joinRequested`. It renders a 48-pixel tabular time column, 3-pixel source-color rail, elided summary, source/location metadata, and optional Join pill. All-day rows replace time with `All day`.
|
||||||
|
|
||||||
|
`NotificationSummary.qml` is visible only when `Notifs.hasNotifications`; it displays history count and emits `activated`. It never marks notifications read by itself.
|
||||||
|
|
||||||
|
`AgendaPane.qml` renders exactly one of:
|
||||||
|
|
||||||
|
- loading copy (`Loading calendars…`);
|
||||||
|
- unavailable copy plus `Open Calendar`;
|
||||||
|
- empty-day copy plus `Open Calendar`;
|
||||||
|
- Up Next plus the selected day's ordered list.
|
||||||
|
|
||||||
|
The Up Next card is omitted for selected dates other than today and when no future timed event exists. Use `ScrollColumn` with a maximum content height derived from the panel's available height. Event clicks call `CalendarAgenda.openEvent(event)`; Join calls `CalendarAgenda.join(event)`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Recompose DateMenuPanel as Daybook**
|
||||||
|
|
||||||
|
Keep the existing header styling and Prism container, but replace the single `Column` with this concrete structure:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
Column {
|
||||||
|
id: frame
|
||||||
|
spacing: 0
|
||||||
|
Item {
|
||||||
|
id: header
|
||||||
|
width: parent.width
|
||||||
|
height: 42
|
||||||
|
}
|
||||||
|
Rectangle { width: parent.width; height: 1; color: Theme.alpha(Theme.fg, 0.1) }
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
Item {
|
||||||
|
id: leftColumn
|
||||||
|
width: 310
|
||||||
|
height: rightColumn.height
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
width: 1
|
||||||
|
height: rightColumn.height
|
||||||
|
color: Theme.alpha(Theme.fg, 0.1)
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
id: rightColumn
|
||||||
|
width: frame.width - 311
|
||||||
|
height: Math.max(438, activeContent.implicitHeight)
|
||||||
|
property Item activeContent: ShellState.dateMenuPage === "agenda" ? agendaPane : notificationList
|
||||||
|
AgendaPane { visible: ShellState.dateMenuPage === "agenda" }
|
||||||
|
NotificationList { visible: ShellState.dateMenuPage === "notifications" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Populate `leftColumn` inline in `DateMenuPanel.qml` with a `Column` containing CalendarGrid, conditional MediaCard, and WeatherCard. Populate `header` with the existing date text and pill controls. Bind date activation to `CalendarAgenda.selectDate(date)` and month changes to `CalendarAgenda.setVisibleMonth(year, month)`.
|
||||||
|
|
||||||
|
When Agenda's notification summary activates, set `ShellState.dateMenuPage = "notifications"` and call `Notifs.markAllRead()`. Notification view header exposes `Back to agenda`, DND, and Clear All. Returning to agenda retains selected date and month.
|
||||||
|
|
||||||
|
Resize MediaCard typography/controls responsively for the 310-pixel column; do not duplicate its playback logic.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update DateMenu lifecycle and bounds**
|
||||||
|
|
||||||
|
Set `DateMenu.implicitWidth: 760`. Bound height to the active screen's available geometry, with the panel content choosing a maximum right-column scroll height instead of overflowing the monitor.
|
||||||
|
|
||||||
|
On opening:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
CalendarAgenda.refresh();
|
||||||
|
if (ShellState.dateMenuPage === "notifications")
|
||||||
|
Notifs.markAllRead();
|
||||||
|
panel.prepare(ShellState.dateMenuPage);
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a connection that marks notifications read when an already-open panel switches into Notifications. Opening from the clock must not mark them read.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify fixture visuals on the real layer surface**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qs ipc call calendar-agenda fixture daybook
|
||||||
|
qs ipc call calendar-agenda open
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture a temporary full-screen screenshot with `grim`, inspect the Daybook at original detail, and verify:
|
||||||
|
|
||||||
|
- no clipped text at 1.5 scale;
|
||||||
|
- month and agenda are simultaneously visible;
|
||||||
|
- right content has no unexplained empty glass;
|
||||||
|
- three source colors remain restrained;
|
||||||
|
- notification summary is quieter than Up Next;
|
||||||
|
- no continuously repainting animation.
|
||||||
|
|
||||||
|
Close the panel and reset fixture mode after inspection. Do not add screenshots to git.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit the Daybook**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/quickshell/modules/datemenu \
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
git commit -m "Build the Prism calendar Daybook"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Quiet upcoming-event capsule
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `config/dot/quickshell/modules/bar/CalendarIndicator.qml`
|
||||||
|
- Modify: `config/dot/quickshell/modules/bar/Bar.qml`
|
||||||
|
- Modify: `config/dot/quickshell/services/CalendarAgenda.qml`
|
||||||
|
- Modify: `tests/quickshell/calendar-agenda-contract.sh`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `CalendarAgenda.capsuleVisible`, `capsuleText`, and `nextEvent`.
|
||||||
|
- Produces: a right-side Pill showing calendar glyph plus `12m` or `Now`.
|
||||||
|
- Produces: `applyFixture("upcoming")` and `applyFixture("outside-window")` for boundary tests.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing capsule boundary assertions**
|
||||||
|
|
||||||
|
Append:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qs ipc call calendar-agenda fixture upcoming >/dev/null
|
||||||
|
jq -e '.capsuleVisible == true and .capsuleText == "12m"' \
|
||||||
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null || fail 'upcoming capsule window is wrong'
|
||||||
|
|
||||||
|
qs ipc call calendar-agenda fixture outside-window >/dev/null
|
||||||
|
jq -e '.capsuleVisible == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'capsule appears outside the quiet window'
|
||||||
|
```
|
||||||
|
|
||||||
|
Run and expect RED because those fixtures do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement deterministic timing fixtures and capsule state**
|
||||||
|
|
||||||
|
Add `fixtureNow` to CalendarAgenda. Production uses `Date.now() / 1000`; fixture modes use their explicit clock. `upcoming` creates one timed event exactly 12 minutes ahead. `outside-window` creates one 20 minutes ahead plus an all-day event. Verify all-day never wins `nextEvent`.
|
||||||
|
|
||||||
|
Use a one-minute `Timer` only while events are loaded to update relative time. Midnight refresh remains separate. There is no sub-minute repaint.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build CalendarIndicator and mount it in Bar**
|
||||||
|
|
||||||
|
Use `Pill` with `visible: CalendarAgenda.capsuleVisible`, horizontal padding 8, a calendar Nerd Font glyph, and tabular relative text. `onActivated` opens Agenda. Import `QtQuick.Controls` and use `ToolTip.visible: root.hovered && root.visible` with the next event's title; the title must not otherwise appear in the bar.
|
||||||
|
|
||||||
|
Mount the indicator at the beginning of Bar's right row, before ActivityIndicator, so it does not shift the centered clock.
|
||||||
|
|
||||||
|
Use a single 120 ms opacity/width entrance tied to visibility. No looping animation.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run capsule and full shell contracts**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
for test in tests/quickshell/*.sh; do "$test"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: calendar fixture boundaries PASS and every existing Quickshell contract remains green.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit the capsule**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/quickshell/modules/bar/CalendarIndicator.qml \
|
||||||
|
config/dot/quickshell/modules/bar/Bar.qml \
|
||||||
|
config/dot/quickshell/services/CalendarAgenda.qml \
|
||||||
|
tests/quickshell/calendar-agenda-contract.sh
|
||||||
|
git commit -m "Add the quiet calendar capsule"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Documentation, live EDS verification, and delivery
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `config/dot/hypr/DESKTOP-PARITY.md`
|
||||||
|
- Modify: `config/dot/hypr/README.md`
|
||||||
|
- Modify: `setup/packages/hyprland-packages`
|
||||||
|
- Modify: `docs/superpowers/plans/2026-08-17-calendar-agenda.md`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: completed helper, service, Daybook, capsule, and test suite.
|
||||||
|
- Produces: documented live behavior and a pushed sequence of reviewed commits.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update parity and operator documentation**
|
||||||
|
|
||||||
|
In DESKTOP-PARITY:
|
||||||
|
|
||||||
|
- change Calendar Agenda to Live with EDS/Google/iCloud/Nextcloud wording;
|
||||||
|
- remove Wi-Fi QR from remaining work;
|
||||||
|
- remove `qrencode` from the privileged package command;
|
||||||
|
- keep KDE Connect, Home Assistant, printer UI, and RustDesk lock-screen validation as future work.
|
||||||
|
|
||||||
|
In Hyprland README document:
|
||||||
|
|
||||||
|
- clock opens Agenda;
|
||||||
|
- `Super+B` opens Notifications inside the same Daybook;
|
||||||
|
- event clicks hand off to GNOME Calendar;
|
||||||
|
- account management remains in GNOME Calendar/Online Accounts;
|
||||||
|
- the bar capsule's 15-minute to +5-minute window.
|
||||||
|
|
||||||
|
Remove `qrencode` from `setup/packages/hyprland-packages` only if `rg -n qrencode .` confirms no remaining runtime consumer.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run static verification**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -n tests/quickshell/*.sh config/dot/quickshell/scripts/screen-intelligence
|
||||||
|
python3 -c 'compile(open("config/dot/quickshell/scripts/calendar-agenda", encoding="utf-8").read(), "calendar-agenda", "exec")'
|
||||||
|
python3 tests/quickshell/calendar_agenda_bridge_test.py
|
||||||
|
desktop-file-validate config/local/share/applications/*.desktop
|
||||||
|
Hyprland --verify-config
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: every command exits 0 and Hyprland reports `config ok`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run every Quickshell contract**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for test in tests/quickshell/*.sh; do
|
||||||
|
"$test"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all prior contracts plus both calendar contracts report PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify the live calendar without leaking content**
|
||||||
|
|
||||||
|
Reset fixture mode and refresh:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qs ipc call calendar-agenda reset
|
||||||
|
qs ipc call calendar-agenda refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
Poll status until `phase` is `ready` or `degraded`. Assert `sourceCount > 0`; report only source/event counts. Open Agenda from the clock path, inspect the real panel locally, test one date containing events, and confirm event clicks launch GNOME Calendar. Do not capture or print a screenshot containing real event details.
|
||||||
|
|
||||||
|
Test a Join action only when a recognized URL is already present; opening that URL is an external navigation and requires an explicit click in the local UI. Do not fabricate or modify an event for testing.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Review logs and the complete diff**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qs log -n 250 | rg -i 'calendar|qml|error|warn'
|
||||||
|
git status --short
|
||||||
|
git diff --stat 5248883..HEAD
|
||||||
|
git diff --check 5248883..HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no calendar QML errors, no bridge restart loop, no secrets, and only calendar/planned-roadmap changes beyond the already-approved desktop snapshot.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Mark this plan complete and commit documentation**
|
||||||
|
|
||||||
|
Mark completed checkboxes only after their commands actually passed, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add config/dot/hypr/DESKTOP-PARITY.md \
|
||||||
|
config/dot/hypr/README.md \
|
||||||
|
setup/packages/hyprland-packages \
|
||||||
|
docs/superpowers/plans/2026-08-17-calendar-agenda.md
|
||||||
|
git commit -m "Document the Panama calendar agenda"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Push without rewriting history**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: clean worktree and `main` advances on `origin`. If Gitea still returns HTTP 502, preserve all commits locally, report the remote outage, and do not force-push or change remote history.
|
||||||
Reference in New Issue
Block a user