The Go server now runs sync -> infer -> flush-cache by itself at the times in SYNC_SCHEDULE (comma-separated HH:MM, default 04:00, "off" disables), read in SYNC_TZ (default Europe/Prague). Nothing runs at startup; an invalid value stops the server with an error. - internal/scheduler: time-of-day parsing, Next, and the run loop - banksync.Runner: one lock shared by /sync-bank and the scheduler so runs never overlap, plus the status of the last run - footer shows the last sync since start and the next scheduled run - binary embeds time/tzdata because the Alpine image has no zone data Deliberately not implemented in the legacy Python app, which is being phased out; recorded in CLAUDE.md under "Deliberately Go-only features". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
4.0 KiB
Go
153 lines
4.0 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"fuj-management/go/internal/web/api"
|
|
"net/http"
|
|
"runtime/debug"
|
|
)
|
|
|
|
// HTMLHandler serves the Go-native HTML frontend.
|
|
type HTMLHandler struct {
|
|
renderer *Renderer
|
|
build BuildInfo
|
|
apiHandler *api.Handler
|
|
actions ActionHandlers
|
|
}
|
|
|
|
// NewHTMLHandler constructs an HTMLHandler.
|
|
func NewHTMLHandler(r *Renderer, b BuildInfo, ah *api.Handler, actions ActionHandlers) *HTMLHandler {
|
|
return &HTMLHandler{renderer: r, build: b, apiHandler: ah, actions: actions}
|
|
}
|
|
|
|
// page builds the data shared by every page (nav highlight, build info, sync summary).
|
|
func (h *HTMLHandler) page(active string) PageData {
|
|
pd := PageData{Active: active, Build: h.build}
|
|
if h.actions.SyncInfo != nil {
|
|
pd.SyncInfo = h.actions.SyncInfo()
|
|
}
|
|
return pd
|
|
}
|
|
|
|
func (h *HTMLHandler) ServeAdults(w http.ResponseWriter, r *http.Request) {
|
|
data, err := h.apiHandler.AssembleAdults(r.Context())
|
|
if err != nil {
|
|
h.renderer.Render(w, "adults", AdultsPageData{
|
|
PageData: h.page("adults"),
|
|
Error: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
h.renderer.Render(w, "adults", AdultsPageData{
|
|
PageData: h.page("adults"),
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
func (h *HTMLHandler) ServeJuniors(w http.ResponseWriter, r *http.Request) {
|
|
data, err := h.apiHandler.AssembleJuniors(r.Context())
|
|
if err != nil {
|
|
h.renderer.Render(w, "juniors", JuniorsPageData{
|
|
PageData: h.page("juniors"),
|
|
Error: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
h.renderer.Render(w, "juniors", JuniorsPageData{
|
|
PageData: h.page("juniors"),
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
func (h *HTMLHandler) ServePayments(w http.ResponseWriter, r *http.Request) {
|
|
data, err := h.apiHandler.AssemblePayments(r.Context())
|
|
if err != nil {
|
|
h.renderer.Render(w, "payments", PaymentsPageData{
|
|
PageData: h.page("payments"),
|
|
Error: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
h.renderer.Render(w, "payments", PaymentsPageData{
|
|
PageData: h.page("payments"),
|
|
Data: data,
|
|
})
|
|
}
|
|
|
|
// ServeSync handles GET /sync-bank: runs sync+infer+flush then renders the result.
|
|
func (h *HTMLHandler) ServeSync(w http.ResponseWriter, r *http.Request) {
|
|
pd := h.page("sync")
|
|
|
|
if h.actions.BankSync == nil {
|
|
h.renderer.Render(w, "sync", SyncPageData{
|
|
PageData: pd,
|
|
Output: "Bank sync is not configured.",
|
|
Success: false,
|
|
})
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
success := true
|
|
if err := h.actions.BankSync(r.Context(), &buf); err != nil {
|
|
fmt.Fprintf(&buf, "\nError: %s\n\nStack trace:\n%s", err.Error(), debug.Stack())
|
|
success = false
|
|
}
|
|
|
|
fmt.Fprintln(&buf, "\n=== Flush Cache ===")
|
|
n, err := h.apiHandler.FlushCache()
|
|
if err != nil {
|
|
fmt.Fprintf(&buf, "flush error: %s\n", err.Error())
|
|
success = false
|
|
} else {
|
|
fmt.Fprintf(&buf, "%d cache file(s) deleted.\n", n)
|
|
}
|
|
|
|
h.renderer.Render(w, "sync", SyncPageData{
|
|
PageData: h.page("sync"), // rebuilt so the footer reflects this run
|
|
Output: buf.String(),
|
|
Success: success,
|
|
})
|
|
}
|
|
|
|
// ServeFlushCacheGET handles GET /flush-cache: renders the confirmation form.
|
|
func (h *HTMLHandler) ServeFlushCacheGET(w http.ResponseWriter, r *http.Request) {
|
|
h.renderer.Render(w, "flush_cache", FlushPageData{
|
|
PageData: h.page("flush"),
|
|
})
|
|
}
|
|
|
|
// ServeFlushCachePOST handles POST /flush-cache: flushes and re-renders with count.
|
|
func (h *HTMLHandler) ServeFlushCachePOST(w http.ResponseWriter, r *http.Request) {
|
|
n, _ := h.apiHandler.FlushCache()
|
|
h.renderer.Render(w, "flush_cache", FlushPageData{
|
|
PageData: h.page("flush"),
|
|
Flushed: true,
|
|
Deleted: n,
|
|
})
|
|
}
|
|
|
|
// ServeQR handles GET /qr: generates and returns a Czech QR Platba PNG.
|
|
func (h *HTMLHandler) ServeQR(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
account := q.Get("account")
|
|
amount := q.Get("amount")
|
|
message := q.Get("message")
|
|
if account == "" {
|
|
account = h.apiHandler.Config.QRAccount
|
|
}
|
|
if amount == "" {
|
|
amount = "0"
|
|
}
|
|
|
|
payload := BuildSPD(account, amount, message, h.apiHandler.Config.QRAccount)
|
|
png, err := RenderQRCode(payload)
|
|
if err != nil {
|
|
http.Error(w, "qr encode: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write(png)
|
|
}
|