Files
compass_teams_nhs/internal/web/templates/item.html
Sam Rolfe f6be17fe4f Phase 6: write important school events to the Home Assistant calendar
Add the Home Assistant REST calendar client and the idempotent sync that
writes important school events to Sam's Home Assistant calendar (FR-8). The
school stays read-only; the only write is to the calendar (rule 7).

- internal/calendar: HA REST client (states, create_event, get_events,
  delete_event) and the sync that selects important future events, adopts an
  existing event by its description marker, records the HA event id on the
  item, and records every attempt.
- internal/store: migration 003 (items.ha_event_uid, items.ha_pushed_at and
  the append-only calendar_pushes table), candidates, mark-synced and the
  push log.
- internal/parse: a school event is important; a routine class period is not.
- cmd/school: `school calendar entities|status|push|events`.
- web: an "Add to calendar" item action and a fail-loud calendar banner, so a
  failed write is never swallowed (FR-12).
- scripts/calendar-push.sh, Makefile target, env example, docs and tests.

The target is configurable (SCHOOL_HA_CALENDAR_ENTITY). Home Assistant has no
non-Google writable calendar today, so the entity is asked on AUD-110.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-15 18:01:30 +11:00

94 lines
4.0 KiB
HTML

{{define "item.html"}}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>{{.Title}} — Compass / Teams</title>
<link rel="stylesheet" href="/static/tokens.css">
<link rel="stylesheet" href="/static/app.css">
<script src="/static/htmx.min.js" defer></script>
<script src="/static/theme.js" defer></script>
</head>
<body class="app">
<nav class="nav-bar">
<a class="nav-bar__brand" href="/">
<span class="nav-bar__mark" aria-hidden="true"></span>
Compass / Teams
</a>
<div class="nav-bar__right">
<button class="button-utility" type="button" data-theme-toggle>Dark</button>
<span class="nav-bar__user">{{.Username}}</span>
<form method="post" action="/logout" class="nav-bar__logout">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<button class="button-utility" type="submit">Sign out</button>
</form>
</div>
</nav>
<main class="container">
<p class="breadcrumb"><a href="/?child={{.Child.ID}}">← {{.Child.Name}}</a></p>
<article class="detail">
{{if .Flash}}<p class="flash flash--{{if eq .FlashLevel "ok"}}ok{{else}}error{{end}}" role="alert">{{.Flash}}</p>{{end}}
<header class="detail__head">
<h1 class="heading-1">{{.Item.Title}}</h1>
<p class="detail__meta">
<span class="tag tag--type">{{.Item.TypeLabel}}</span>
{{if .Item.Subject}}<span>{{.Item.Subject}}</span>{{end}}
{{if .Item.Teacher}}<span>Teacher: {{.Item.Teacher}}</span>{{end}}
{{if .Item.DueLabel}}<span class="item__due">due {{.Item.DueLabel}}</span>{{end}}
{{if .Item.StatusLabel}}<span class="tag tag--status">{{.Item.StatusLabel}}</span>{{end}}
</p>
</header>
{{if .Item.BodyLines}}
<section class="detail__body">
{{range .Item.BodyLines}}<p>{{.}}</p>{{end}}
</section>
{{end}}
<dl class="detail__facts">
<dt>Child</dt><dd>{{.Child.Name}}</dd>
<dt>Source</dt><dd>{{.Item.Source}}{{if .Item.SourcePage}} · {{.Item.SourcePage}}{{end}}</dd>
{{if .Item.Importance}}<dt>Importance</dt><dd>{{.Item.Importance}}</dd>{{end}}
<dt>Calendar</dt>
<dd>{{if .Calendar.InCalendar}}in the calendar{{if .Calendar.PushedAt}} · {{.Calendar.PushedAt}}{{end}}{{if .Calendar.UID}} · {{.Calendar.UID}}{{end}}
{{else if not .Calendar.Configured}}not configured
{{else if .Calendar.Eligible}}not written yet
{{else}}not a calendar item{{end}}</dd>
{{if .Item.ExternalRef}}<dt>Reference</dt><dd>{{.Item.ExternalRef}}</dd>{{end}}
{{if .Item.DueAt}}<dt>Due</dt><dd>{{.Item.DueLabel}}</dd>{{end}}
</dl>
<div class="detail__actions">
{{if .Item.URL}}<a class="button-primary" href="{{.Item.URL}}" rel="noreferrer noopener">Open in Compass</a>{{end}}
{{if and .Calendar.Configured .Calendar.Eligible (not .Calendar.InCalendar)}}
<form method="post" action="/items/{{.Item.ID}}/calendar">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<button class="button-primary" type="submit">Add to calendar</button>
</form>
{{end}}
<form method="post" action="/items/{{.Item.ID}}/delete"
hx-post="/items/{{.Item.ID}}/delete" hx-confirm="Delete this item? It returns on the next scan if the school still lists it.">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<button class="button-utility button-utility--danger" type="submit">Delete</button>
</form>
</div>
{{if not .Scan.OK}}
<p class="flash flash--error" role="alert">{{.Scan.Message}}</p>
{{end}}
</article>
</main>
<footer class="app-foot">
<span>Compass / Teams · {{.Version}} · {{.AppEnv}}</span>
<span class="app-foot__sep">·</span>
<span>up {{.ServerUp}}</span>
<span class="app-foot__sep">·</span>
<span>{{year}} Family Home Lab</span>
</footer>
</body>
</html>{{end}}