Add API loading indicator and session-expiry auto-redirect

- Thin animated bar at page top shows while any fetch is in-flight
- api() centrally redirects to login on 401 (except login endpoint)
- Logo click triggers manual goal refresh
- cursor:pointer on logo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Kühn 2026-05-07 12:48:01 +02:00
parent bd0190cfde
commit 7ea6aeb98c
3 changed files with 15 additions and 1 deletions

View file

@ -253,14 +253,23 @@ function dlbl(g,i){
// ── API ──────────────────────────────────────────────────────────────────────
var _apiPending = 0;
function _apiBar(){ document.getElementById('api-bar').classList.toggle('loading', _apiPending > 0); }
function api(method, path, body){
var opts = {method:method, credentials:'include', headers:{'Content-Type':'application/json'}};
if(body) opts.body = JSON.stringify(body);
_apiPending++; _apiBar();
return fetch('api/' + path, opts).then(function(res){
return res.json().then(function(data){
if(!res.ok){ var e=new Error(data.error||'Fehler'); e.status=res.status; throw e; }
return data;
});
}).catch(function(e){
if(e.status===401 && path!=='login'){ showLogin(); }
throw e;
}).finally(function(){
_apiPending--; _apiBar();
});
}
@ -903,6 +912,7 @@ function updateHeader(){
document.getElementById('btnNew').onclick=openNew;
document.getElementById('btnData').onclick=openData;
document.querySelector('.hdr-logo').onclick=function(){ loadGoals().then(function(g){goals=g;render();}).catch(function(){}); };
updateHeader();
var _qs=new URLSearchParams(window.location.search);

View file

@ -2,10 +2,13 @@
:root{--bg:#f5f4f0;--bg2:#fff;--bg3:#f0eeea;--border:rgba(0,0,0,.07);--border2:rgba(0,0,0,.12);--text:#1a1a1a;--text2:#666;--text3:#aaa;--green:#16a34a;--green-bg:rgba(22,163,74,.08);--blue:#2563eb;--blue-bg:rgba(37,99,235,.08);--amber:#d97706;--amber-bg:rgba(217,119,6,.08);--red:#dc2626;--red-bg:rgba(220,38,38,.08);--r:14px;--rs:8px}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
body{font-family:'DM Sans',sans-serif;background:var(--bg);color:var(--text);min-height:100dvh;padding-bottom:80px}
#api-bar{position:fixed;top:0;left:0;right:0;height:2px;z-index:200;pointer-events:none;overflow:hidden}
#api-bar.loading::after{content:'';position:absolute;top:0;height:100%;width:40%;background:var(--blue);animation:api-bar-slide 1s linear infinite}
@keyframes api-bar-slide{0%{left:-40%}100%{left:140%}}
.main-wrap{max-width:480px;margin:0 auto}
.hdr{position:sticky;top:0;z-index:100;background:rgba(245,244,240,.92);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:16px 20px 14px;display:flex;align-items:center;justify-content:space-between}
.hdr-title{font-size:18px;font-weight:600;letter-spacing:-.3px}
.hdr-logo{height:100px;width:auto;mix-blend-mode:multiply;display:block}
.hdr-logo{height:100px;width:auto;mix-blend-mode:multiply;display:block;cursor:pointer}
.hdr-sub{font-size:12px;color:var(--text3);margin-top:1px;font-family:'DM Mono',monospace}
.hdr-btns{display:flex;gap:8px;align-items:center}
.sw{font-family:'DM Mono',monospace;font-size:15px;color:var(--text3);cursor:pointer;user-select:none;letter-spacing:-.5px;min-width:52px;text-align:center}

View file

@ -10,6 +10,7 @@
<link rel="stylesheet" href="style.css?v={{ cssv }}"/>
</head>
<body>
<div id="api-bar"></div>
<div class="main-wrap">
<div class="hdr">
<div>