diff --git a/CLAUDE.md b/CLAUDE.md index 83c10bc..b4ca380 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -260,6 +260,37 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: ⚠ Die 40 vorhandenen `if (!el) return;`-Wächter feuern jetzt nicht mehr. Sie bleiben stehen (kein Nutzen im Entfernen, und sie greifen weiter, wenn jemand `$streng` benutzt) — beim Lesen aber mitdenken. +- **✅ G/V-LAG: 1-s-FALLBACK + EHRLICHER VERBINDUNGSPUNKT (2026-08-07, v=151).** + User: „G/V wird extrem lagged — 1 Sekunde". + ⚠⚠ **ZUERST GEMESSEN, DANN GEDREHT — und das Backend war NICHT das Problem:** + | Stufe | gemessen | + |---|---| + | `/api/snapshot` (75 Abrufe / 20 s) | **21 verschiedene `ts`** = ~1/s | + | **WebSocket-Push, direkt am Socket** | **Median 1,04 s** (min 1,00 · max 1,06) | + | G/V-Änderungen | Ø 1,44 s — das ist der **Markt**, keine Verzögerung | + **Die ganze Server-Kette liefert bereits 1 s.** Der Lag entsteht erst am Handy: + bricht der WebSocket **still** weg (WireGuard-Aussetzer, App-Wechsel), trägt nur + noch der REST-Poll — und der stand auf **4 s**. + ✅ **`POLL_MS` 4000 → 1000, aber MIT Sperre:** solange der WS liefert + (`lastWsMs`, Fenster `WS_FRISCH_MS`=2200 ms), überspringt der Poll den Abruf. + **Kein zusätzlicher Verkehr im Normalfall** (~14 KB/Abruf über WireGuard). + ⚠ **Denkfehler, den erst die Simulation zeigte:** der erste Entwurf las die + WS-Frische aus `lastRecvMs` (jede Quelle) — damit **drosselte sich der Poll + selbst** und kam bei totem WS nur auf **2,9 s** statt 1 s. Jetzt zwei getrennte + Uhren: `lastWsMs` (nur `sock.onmessage`) entscheidet über das Überspringen, + `lastRecvMs` (jede Quelle) über die Stale-Anzeige. + **Simuliert:** WS gesund → **0** Zusatz-Abrufe, max. Alter **0,9 s** · WS tot → + 1/s, max. Alter **0,9 s** · WS träge (5 s) → max. 2,9 s. Vorher bei totem WS: + 4,0 s. + ✅ **Der Verbindungspunkt log** — behoben. Er wurde **nur** bei `sock.onclose` + auf „off" gesetzt; genau der Fall, der den Lag erzeugt, feuert aber **kein** + `onclose`: der Socket hängt still, der Punkt blieb **grün**. Neu `.dot.stale` + (bernstein, pulsierend) ab **3,5 s** ohne frischen Snapshot — gemessen wird, + was ANKOMMT, nicht was der Socket über sich behauptet. Tooltip nennt das Alter. + ⚠ Kein Konflikt mit `render()`, das `className` komplett überschreibt: läuft + `render()`, ist gerade ein Snapshot angekommen und `stale` gehört ohnehin weg. + `.dot.stale` steht in `style.css` **nach** `.on`/`.off` — gleiche Spezifität, + die Reihenfolge entscheidet. - **⚠ NEUE HTML-Elemente IMMER null-sicher ansprechen (Fix 2026-08-01).** `index.html` und `app.js` werden vom Browser **unabhängig** gecacht. Trifft neue JS auf eine alte, gecachte HTML, wirft ein direkter Zugriff (`$("neu").textContent = …`) @@ -4151,7 +4182,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: („⚠ die Welle (steuert die Order) schweigt"). Live gegengerechnet, beide Zeilen erscheinen wie beabsichtigt. ⚠ Reine Anzeige — an Gewichten, Gates und Order-Logik ist nichts geändert. -- Asset-Version aktuell **v=150** (in `web/index.html` hochzählen, siehe Workflows). +- Asset-Version aktuell **v=151** (in `web/index.html` hochzählen, siehe Workflows). Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis 14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die `font: Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge` — diff --git a/web/app.js b/web/app.js index 2d68a35..52bd8a9 100644 --- a/web/app.js +++ b/web/app.js @@ -1124,15 +1124,27 @@ let sock = null, retry = null; // `engine.snapshot_cached().ts`) — nur die jeweils NEUERE Quelle rendert, kein // Flackern/Doppel-Render, wenn beide fast gleichzeitig eintreffen. let lastTs = 0; +// Wann kam zuletzt ein FRISCHER Snapshot an (Wanduhr, egal aus welcher Quelle)? +// Grundlage für den 1-s-Fallback und die Stale-Erkennung unten. +let lastRecvMs = 0; +// Getrennt davon: wann kam zuletzt eine WS-NACHRICHT? Nur diese Uhr entscheidet, +// ob der Poll uebersprungen wird (s. Kommentar in `sock.onmessage`). +let lastWsMs = 0; function _applySnapshot(d) { if (typeof d.ts === "number" && d.ts <= lastTs) return; // schon Neueres gezeigt if (typeof d.ts === "number") lastTs = d.ts; + lastRecvMs = Date.now(); render(d); } function connect() { const proto = location.protocol === "https:" ? "wss" : "ws"; sock = new WebSocket(`${proto}://${location.host}/ws`); sock.onmessage = (ev) => { + // ⚠ NUR hier setzen, NICHT in `_applySnapshot`. Der Poll darf die + // WS-Frische nicht mitschreiben, sonst drosselt er sich selbst: er würde + // nach jedem eigenen Abruf wieder WS_FRISCH_MS pausieren und käme bei totem + // WS nur auf ~2,9 s statt 1 s. (In der Simulation aufgefallen, s. unten.) + lastWsMs = Date.now(); try { _applySnapshot(JSON.parse(ev.data)); } catch (e) { console.warn(e); } }; sock.onclose = () => { @@ -1149,9 +1161,21 @@ function connect() { // mehr — z. B. schwaches WireGuard-Signal, App-Wechsel ohne Visibility-Event), // ohne auf einen Reconnect warten zu müssen. Der `lastTs`-Dedup verhindert // Doppel-Renders, solange der WS normal liefert — reine Redundanz, kein Konflikt. -const POLL_MS = 4000; +// ⚠⚠ 4000 → 1000 (2026-08-07, User: „G/V wird extrem lagged, 1 Sekunde"). +// GEMESSEN, bevor gedreht wurde: die gesamte Server-Kette liefert bereits 1 s +// (`_pos_loop` 1 s · Snapshot-Cache 0,8 s · WS-Push **Median 1,04 s**, direkt am +// Socket nachgemessen). Der Lag entsteht also NICHT im Backend, sondern wenn der +// WebSocket am Handy still wegbricht (WireGuard-Aussetzer, App-Wechsel): dann +// trägt nur noch dieser Poll — und der stand auf 4 s. +// ⚠ Das VERDOPPELT den Verkehr NICHT: solange der WS liefert, überspringt der +// Poll den Abruf (s. `WS_FRISCH_MS`). Er feuert nur, wenn der WS schweigt. +const POLL_MS = 1000; +const WS_FRISCH_MS = 2200; // > WS-Takt (1,04 s) + Jitter, sonst pollt es doppelt async function pollSnapshot() { if (document.hidden) return; // seite unsichtbar → spart Akku/Daten, Wake-Handler holt beim Zurückkommen sofort nach + // Liefert der WS gerade? Dann NICHT zusätzlich abfragen — reine Redundanz + // kostet über WireGuard sonst ~14 KB/s ohne jeden Nutzen. + if (Date.now() - lastWsMs < WS_FRISCH_MS) return; let d; try { d = await (await fetch("api/snapshot")).json(); @@ -1166,6 +1190,27 @@ async function pollSnapshot() { } setInterval(pollSnapshot, POLL_MS); +// ── Ehrlicher Verbindungspunkt (2026-08-07) ──────────────────────────────── +// ⚠ Bisher wurde er NUR bei `sock.onclose` auf „off" gesetzt. Genau der Fall, +// der den Lag verursacht, löst aber KEIN onclose aus: der Socket hängt still +// (WireGuard-Aussetzer, App-Wechsel) — der Punkt blieb grün und behauptete eine +// Verbindung, die nichts mehr liefert. Ein Zustandsanzeiger, der im +// Fehlerfall nicht anschlägt, ist schlimmer als keiner. +// Jetzt zählt, was WIRKLICH ankommt: liegt der letzte angewandte Snapshot mehr +// als STALE_MS zurück, wird der Punkt matt — unabhängig davon, was der Socket +// über sich selbst behauptet. +const STALE_MS = 3500; +setInterval(() => { + const el = $("conn"); + if (!el) return; + const alt = Date.now() - lastRecvMs; + el.classList.toggle("stale", lastRecvMs > 0 && alt > STALE_MS); + el.title = lastRecvMs + ? `letzte Aktualisierung vor ${(alt / 1000).toFixed(1)} s` + + (alt > STALE_MS ? " — Verbindung hängt, es wird nachgepollt" : "") + : "noch keine Daten empfangen"; +}, 1000); + // ── Trade-Steuerung ──────────────────────────────────────────────────────── const TOKEN_KEY = "oil_token"; diff --git a/web/index.html b/web/index.html index 68b6b9a..7cb3379 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ Oil · MT5 - + @@ -330,6 +330,6 @@ - + diff --git a/web/style.css b/web/style.css index 6cbbd50..550918d 100644 --- a/web/style.css +++ b/web/style.css @@ -45,6 +45,13 @@ header{ .dot{width:11px;height:11px;border-radius:50%;display:inline-block} .dot.on{background:var(--up);box-shadow:0 0 8px var(--up)} .dot.off{background:var(--down);box-shadow:0 0 8px var(--down)} +/* stale = der Socket behauptet Verbindung, liefert aber nichts mehr (2026-08-07). + Bernstein + Pulsieren, damit der Unterschied zu „verbunden" auf einen Blick + sichtbar ist — genau dieser Zustand erzeugte den G/V-Lag. Schlägt `.on` und + `.off`, deshalb hier NACH beiden. */ +.dot.stale{background:var(--warn,#f0a020);box-shadow:0 0 8px var(--warn,#f0a020); + animation:dotpuls 1.2s ease-in-out infinite} +@keyframes dotpuls{0%,100%{opacity:1}50%{opacity:.35}} .price-row{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px} /* flex-basis 28% → 3 Boxen je Reihe, die 5. Box (KURS/G/V) bricht sauber um */ .px{flex:1 1 28%;background:var(--card);border:1px solid var(--border);