New /dashboard route serves the authenticated home screen with a server-rendered payload (no skeleton→fetch waterfall, per design audit §2.3). / now redirects authenticated visitors to /dashboard and keeps the marketing landing for anonymous visitors. - DashboardService aggregates deadline + matter summaries, the next 7d of Fristen/Termine, and the last 10 akten_events, all scoped by the standard office-visibility predicate. - Dashboard handler splices the JSON payload into dist/dashboard.html as window.__PALIAD_DASHBOARD__ so the client paints on first frame; client re-fetches /api/dashboard every 60s to stay current. - Sidebar gains an "Übersicht" group with the Dashboard entry at the top; DE/EN i18n keys + traffic-light card styles added. - Empty-state copy, onboarding hint, and 503 handling keep the page intact when DATABASE_URL is unset.
71 lines
2.1 KiB
Go
71 lines
2.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"mgit.msbls.de/m/patholo/internal/auth"
|
|
)
|
|
|
|
// GET /api/dashboard — returns the DashboardData JSON for the logged-in user.
|
|
// Returns 503 if DATABASE_URL is unset.
|
|
func handleDashboardAPI(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
data, err := dbSvc.dashboard.Get(r.Context(), uid)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, data)
|
|
}
|
|
|
|
// GET /dashboard — protected shell page. The client boots, reads the initial
|
|
// payload inlined by the server into window.__PALIAD_DASHBOARD__, and renders
|
|
// without a second round-trip (audit §2.3: no skeleton→fetch waterfall).
|
|
func handleDashboardPage(w http.ResponseWriter, r *http.Request) {
|
|
uid, hasUser := auth.UserIDFromContext(r.Context())
|
|
var payload []byte
|
|
if hasUser && dbSvc != nil {
|
|
// Best-effort server-render. If the DB read fails we still serve the
|
|
// shell; the client will show the inline error state instead of the
|
|
// zero-count cards.
|
|
if data, err := dbSvc.dashboard.Get(r.Context(), uid); err == nil {
|
|
payload = mustJSON(data)
|
|
}
|
|
}
|
|
serveDashboardShell(w, r, payload)
|
|
}
|
|
|
|
// handleRootPage is the public `/` route. Unauthenticated visitors get the
|
|
// marketing landing; authenticated users get a 302 to /dashboard so `/` feels
|
|
// like a no-op they can bookmark.
|
|
func handleRootPage(w http.ResponseWriter, r *http.Request) {
|
|
if hasValidSession(r) {
|
|
http.Redirect(w, r, "/dashboard", http.StatusFound)
|
|
return
|
|
}
|
|
http.ServeFile(w, r, "dist/index.html")
|
|
}
|
|
|
|
// hasValidSession returns true when a session cookie is present, parses, and
|
|
// hasn't expired. Kept intentionally loose: we don't reach out to Supabase
|
|
// here. A near-expiry token will still be accepted and the downstream
|
|
// Middleware handles refresh on the next protected request.
|
|
func hasValidSession(r *http.Request) bool {
|
|
cookie, err := r.Cookie(auth.SessionCookieName)
|
|
if err != nil || cookie.Value == "" {
|
|
return false
|
|
}
|
|
exp, err := auth.DecodeJWTExpiry(cookie.Value)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return time.Now().Before(exp)
|
|
}
|