diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index e30b2c8..d46ee5b 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -810,6 +810,14 @@
+{#if versionInfo?.message && (versionInfo.messageForce || localStorage.getItem('vc_dismissed_msg') !== versionInfo.message)} +
+ {versionInfo.message} + {#if !versionInfo.messageForce} + + {/if} +
+{/if} {#if !lastScanned && scanning}
diff --git a/frontend/src/app.css b/frontend/src/app.css index 423443b..2d16576 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -159,6 +159,10 @@ code{ /* ─── App layout ─── */ .app-content{margin-left:200px;min-height:100vh} +.broadcast-banner{background:var(--primary);color:#fff;padding:.5rem 1rem;font-size:.85rem;display:flex;justify-content:space-between;align-items:center} +.broadcast-dismiss{background:none;border:none;color:#fff;cursor:pointer;font-size:1rem;padding:0 .25rem;opacity:.7} +.broadcast-dismiss:hover{opacity:1} +.broadcast-force{background:var(--danger,#dc2626)} /* ─── Page bar (consistent header across pages) ─── */ .page-bar{ diff --git a/internal/web/server.go b/internal/web/server.go index ee0d65d..5c232b0 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -66,9 +66,9 @@ type server struct { bindAddr string // "127.0.0.1" or whatever the user passed via --bind authToken string // bearer token; required for any non-loopback request - versionMu sync.Mutex - latestVersion string - latestFetched time.Time + versionMu sync.Mutex + cachedVersion *versionResponse + versionFetched time.Time } type apiSession struct { @@ -797,7 +797,7 @@ func detectTerminal() string { } const ( - githubLatestURL = "https://api.github.com/repos/NJannasch/vibecockpit/releases/latest" + versionCheckURL = "https://vibecockpit.njannasch.workers.dev/api/version" latestVersionTTL = time.Hour latestFetchTimeout = 3 * time.Second ) @@ -807,6 +807,8 @@ type versionResponse struct { Latest string `json:"latest,omitempty"` UpdateAvailable bool `json:"updateAvailable"` ReleaseURL string `json:"releaseUrl,omitempty"` + Message string `json:"message,omitempty"` + MessageForce bool `json:"messageForce,omitempty"` } func (s *server) handleCosts(w http.ResponseWriter, r *http.Request) { @@ -985,12 +987,8 @@ func (s *server) handleScanStatus(w http.ResponseWriter, r *http.Request) { } func (s *server) handleVersion(w http.ResponseWriter, r *http.Request) { - resp := versionResponse{Current: s.version} - if latest := s.getLatestVersion(); latest != "" { - resp.Latest = latest - resp.UpdateAvailable = compareSemver(s.version, latest) < 0 - resp.ReleaseURL = "https://github.com/NJannasch/vibecockpit/releases/tag/" + latest - } + resp := s.getLatestVersion() + resp.Current = s.version w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } @@ -2013,53 +2011,46 @@ func (s *server) handleJobRuns(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(runs) } -// getLatestVersion returns the cached latest release tag, refreshing it -// from the GitHub API at most once per latestVersionTTL. A failed fetch -// is silent — the caller just gets the previously-cached value (or ""). -func (s *server) getLatestVersion() string { +func (s *server) getLatestVersion() versionResponse { s.versionMu.Lock() - if s.latestVersion != "" && time.Since(s.latestFetched) < latestVersionTTL { - v := s.latestVersion + if s.cachedVersion != nil && time.Since(s.versionFetched) < latestVersionTTL { + v := *s.cachedVersion s.versionMu.Unlock() return v } s.versionMu.Unlock() - tag := fetchLatestTag() - if tag == "" { - return "" - } + v := fetchVersionCheck(s.version) s.versionMu.Lock() - s.latestVersion = tag - s.latestFetched = time.Now() + s.cachedVersion = &v + s.versionFetched = time.Now() s.versionMu.Unlock() - return tag + return v } -func fetchLatestTag() string { +func fetchVersionCheck(currentVersion string) versionResponse { + u := fmt.Sprintf("%s?v=%s&os=%s&arch=%s", + versionCheckURL, currentVersion, runtime.GOOS, runtime.GOARCH) ctx, cancel := context.WithTimeout(context.Background(), latestFetchTimeout) defer cancel() - req, err := http.NewRequestWithContext(ctx, "GET", githubLatestURL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { - return "" + return versionResponse{} } - req.Header.Set("Accept", "application/vnd.github+json") - req.Header.Set("User-Agent", "vibecockpit") + req.Header.Set("User-Agent", "vibecockpit/"+currentVersion) resp, err := http.DefaultClient.Do(req) if err != nil { - return "" + return versionResponse{} } defer resp.Body.Close() if resp.StatusCode != 200 { - return "" - } - var body struct { - TagName string `json:"tag_name"` + return versionResponse{} } - if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { - return "" + var v versionResponse + if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { + return versionResponse{} } - return body.TagName + return v } // compareSemver returns negative if a < b, zero if equal, positive if a > b.