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.