From b90045b6c9def22dd9538327e0045823b3a7e4b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 19:01:48 +0000 Subject: [PATCH] Add PWA meta tags, auto-refresh, display mode, and touch targets PWA: Add theme-color, apple-mobile-web-app-capable, apple-touch-icon, and application-name meta tags for proper iOS home screen support. Auto-refresh: Home page polls /home (JSON) every 2 minutes and updates card content in-place without full reload. Card template now wraps content in .card-content div to support targeted updates. Display mode: Visit /home?mode=display for a kiosk/wall display view that hides header, nav, footer, and status form. Refreshes every 60s and requests Screen Wake Lock to prevent display sleep. Touch: Buttons now min-height 44px with larger padding (10px 16px). Nav links increased to min-height 44px on both desktop and mobile. https://claude.ai/code/session_01GRGLA9yj7BpqKiyi6xFwnm --- home/home.go | 72 ++++++++++++++++++++++++++++++++++++++-- internal/app/app.go | 8 ++++- internal/app/html/mu.css | 52 ++++++++++++++++++++++++++--- 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/home/home.go b/home/home.go index ca72b641..5d8670a7 100644 --- a/home/home.go +++ b/home/home.go @@ -255,6 +255,33 @@ func RefreshHandler(w http.ResponseWriter, r *http.Request) { } func Handler(w http.ResponseWriter, r *http.Request) { + // JSON endpoint for auto-refresh polling + if app.WantsJSON(r) { + RefreshCards() + cacheMutex.RLock() + type cardData struct { + ID string `json:"id"` + Title string `json:"title"` + HTML string `json:"html"` + Column string `json:"column"` + } + var result []cardData + for _, card := range Cards { + if strings.TrimSpace(card.CachedHTML) == "" { + continue + } + result = append(result, cardData{ + ID: card.ID, + Title: card.Title, + HTML: card.CachedHTML, + Column: card.Column, + }) + } + cacheMutex.RUnlock() + app.RespondJSON(w, result) + return + } + // Refresh cards if cache expired (2 minute TTL) RefreshCards() @@ -356,10 +383,49 @@ func Handler(w http.ResponseWriter, r *http.Request) { strings.Join(rightHTML, "\n"))) } - // Use RenderHTMLWithLang directly to inject a body class that hides the page title, - // keeping the agent prompt as the primary visual element. + // Auto-refresh: poll every 2 minutes, update card content in-place + displayMode := r.URL.Query().Get("mode") == "display" + refreshInterval := 120000 // 2 minutes + if displayMode { + refreshInterval = 60000 // 1 minute in display mode + } + wakeLockJS := "" + if displayMode { + wakeLockJS = ` + // Screen Wake Lock — keep display on in kiosk mode + if('wakeLock' in navigator){ + var wl=null; + function reqWake(){navigator.wakeLock.request('screen').then(function(l){wl=l;l.addEventListener('release',function(){setTimeout(reqWake,1000)})}).catch(function(){})} + reqWake();document.addEventListener('visibilitychange',function(){if(document.visibilityState==='visible')reqWake()}); + }` + } + b.WriteString(fmt.Sprintf(``, refreshInterval, wakeLockJS)) + + // Display mode: hide nav, header, footer for kiosk/wall display + bodyClass := ` class="page-home"` + if displayMode { + bodyClass = ` class="page-home display-mode"` + } + lang := app.GetUserLanguage(r) - html := app.RenderHTMLWithLangAndBody("Home", "The home screen", b.String(), lang, ` class="page-home"`) + html := app.RenderHTMLWithLangAndBody("Home", "The home screen", b.String(), lang, bodyClass) w.Write([]byte(html)) } diff --git a/internal/app/app.go b/internal/app/app.go index 6945aec1..c6ee02e6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -193,6 +193,12 @@ var Template = ` + + + + + + @@ -277,7 +283,7 @@ var CardTemplate = `

%s

- %s +
%s
` diff --git a/internal/app/html/mu.css b/internal/app/html/mu.css index af65077f..68b144ae 100644 --- a/internal/app/html/mu.css +++ b/internal/app/html/mu.css @@ -112,11 +112,12 @@ button { color: #fff; border: 1px solid var(--btn-primary); border-radius: var(--border-radius); - padding: 6px 12px; + padding: 10px 16px; cursor: pointer; font-size: 14px; line-height: 1.4; text-decoration: none; + min-height: 44px; transition: all var(--transition-fast); } @@ -134,8 +135,9 @@ a.btn { color: #fff !important; border: 1px solid var(--btn-primary); border-radius: var(--border-radius); - padding: 6px 12px; + padding: 10px 16px; cursor: pointer; + min-height: 44px; font-size: 14px; font-weight: normal; line-height: 1.4; @@ -391,10 +393,11 @@ td { color: var(--text-primary); font-weight: var(--font-weight-medium); text-decoration: none; - padding: 10px 14px; + padding: 12px 14px; border-radius: var(--border-radius); display: flex; align-items: center; + min-height: 44px; transition: all var(--transition-fast); } @@ -2194,8 +2197,9 @@ a.highlight { } #nav a { - padding: 8px 14px; + padding: 12px 14px; flex-direction: row; + min-height: 44px; } #nav img { @@ -4916,3 +4920,43 @@ a.btn-secondary:hover, .btn-secondary:hover { grid-template-columns: 1fr; } } + +/* ============================================ + DISPLAY / KIOSK MODE + Activated via ?mode=display on /home + Hides nav, header, footer for wall-mounted displays + ============================================ */ +body.display-mode #head, +body.display-mode #nav-container, +body.display-mode #nav-overlay, +body.display-mode #footer, +body.display-mode #menu-toggle, +body.display-mode #page-title, +body.display-mode #home-status-form, +body.display-mode #head-mail { + display: none !important; +} + +body.display-mode #container { + margin: 0; + padding: 0; + max-width: 100%; +} + +body.display-mode #content { + margin: 0; + padding: 16px; + max-width: 100%; +} + +body.display-mode #home { + gap: 16px; +} + +body.display-mode .card { + font-size: 15px; +} + +body.display-mode .card h4 { + font-size: 16px; +}