Files
paliad/internal/handlers/agenda_shell.go
m 0d6c58a337 feat(agenda): unified timeline of deadlines + appointments across projects
t-paliad-030. Adds `/agenda` — a single page that merges every visible
deadline and appointment into a day-grouped timeline, the third overview
surface alongside Dashboard and the per-resource lists.

- AgendaService: merges paliad.deadlines + paliad.appointments, gated by
  the same team-membership predicate used everywhere else; personal
  appointments stay creator-only. Items are sorted by date and tagged
  with urgency (overdue / today / tomorrow / this_week / later) so the
  client can apply the traffic-light colours without re-deriving buckets.
- GET /api/agenda?from&to&types and GET /agenda with the same server-side
  hydration pattern as /dashboard (JSON payload spliced into the shell so
  the timeline paints on first frame).
- Frontend: agenda.tsx + client/agenda.ts render a day-grouped timeline
  with type/range chips; filters round-trip through the URL.
- Sidebar entry under "Übersicht"; DE+EN i18n across all new keys.
2026-04-22 23:38:03 +02:00

59 lines
1.7 KiB
Go

package handlers
import (
"bytes"
"log"
"net/http"
"os"
"path/filepath"
"sync"
)
// Same server-side hydration trick as the dashboard: the agenda page shell
// is pre-rendered by bun (`renderAgenda()` → dist/agenda.html) with a
// placeholder token; the handler splices in the JSON payload at request time
// so the client paints without a second round-trip.
const agendaDataPlaceholder = "/*__PALIAD_AGENDA_DATA__*/"
var (
agendaShellOnce sync.Once
agendaShellBytes []byte
agendaShellErr error
)
func loadAgendaShell() ([]byte, error) {
agendaShellOnce.Do(func() {
path := filepath.Join("dist", "agenda.html")
agendaShellBytes, agendaShellErr = os.ReadFile(path)
if agendaShellErr != nil {
return
}
if !bytes.Contains(agendaShellBytes, []byte(agendaDataPlaceholder)) {
log.Printf("warning: agenda.html is missing the data placeholder — client will fall back to /api/agenda")
}
})
return agendaShellBytes, agendaShellErr
}
func serveAgendaShell(w http.ResponseWriter, _ *http.Request, payload []byte) {
shell, err := loadAgendaShell()
if err != nil {
http.Error(w, "agenda shell unavailable", http.StatusInternalServerError)
return
}
var body []byte
if len(payload) > 0 {
inline := append([]byte("window.__PALIAD_AGENDA__="), escapeForScript(payload)...)
inline = append(inline, ';')
body = bytes.Replace(shell, []byte(agendaDataPlaceholder), inline, 1)
} else {
body = bytes.Replace(shell, []byte(agendaDataPlaceholder),
[]byte("window.__PALIAD_AGENDA__=null;"), 1)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}