From e1b532b35cb1cbda9e1102824cc5fcaeccf39ead Mon Sep 17 00:00:00 2001 From: team3 Date: Sat, 19 Sep 2026 22:22:51 +0200 Subject: [PATCH] update --- Makefile | 2 +- README.md | 10 +++++--- desktop@marha.de/notes.js | 3 +++ desktop@marha.de/sync.js | 53 ++++++++++++++++++++++++++++++++------- dev/restart.sh | 4 +-- dev/tests/input.sh | 1 + dev/tests/sync.sh | 14 ++++++++++- 7 files changed, 70 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 44e960f..1d80cbe 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ sync-config: echo "Sync-Zugang vorhanden: $(SYNC_JSON)" elif [ -f .env ] && grep -q "^SYNC_TOKEN=.\+" .env; then mkdir -p "$(dir $(SYNC_JSON))" - printf '{"url": "$(SYNC_URL)", "token": "%s", "interval": 5}\n' "$$(grep ^SYNC_TOKEN= .env | cut -d= -f2-)" > "$(SYNC_JSON)" + printf '{"url": "$(SYNC_URL)", "token": "%s"}\n' "$$(grep ^SYNC_TOKEN= .env | cut -d= -f2-)" > "$(SYNC_JSON)" chmod 600 "$(SYNC_JSON)" echo "Sync-Zugang angelegt: $(SYNC_JSON)" else diff --git a/README.md b/README.md index c140e00..f8986f5 100644 --- a/README.md +++ b/README.md @@ -50,18 +50,20 @@ Gelöschte Termine und Todos bleiben mit `deleted: true` in der Datei und werden ## Sync -Der Server (`backend/`, FastAPI + SQLite) hält den Kern. Jedes Gerät gleicht alle paar Sekunden ab, -und sofort nach jeder eigenen Änderung: eigene Änderungen hoch, fremde Änderungen runter. Bei Konflikten gewinnt der Eintrag mit dem +Der Server (`backend/`, FastAPI + SQLite) hält den Kern. Jedes Gerät gleicht regelmäßig ab und sofort +nach jeder eigenen Änderung: eigene Änderungen hoch, fremde Änderungen runter. Der Takt richtet sich nach +der Aktivität: nach einer Änderung (eigene oder vom Server) eine Minute lang jede Sekunde, dann alle +5 Sekunden, nach 5 Minuten Ruhe alle 10 Sekunden. Bei Konflikten gewinnt der Eintrag mit dem neueren `updated` (Notizen: Änderungszeit der Datei, ganze Datei). Offline geht weiter, beim Wiederverbinden wird nachgeliefert. Eine gerade offene Notiz wird nicht von außen überschrieben. Zugang auf jedem Gerät in `~/.local/share/desktop-app/sync.json`: ``` -{"url": "https://planer.marha.de", "token": "", "interval": 5} +{"url": "https://planer.marha.de", "token": ""} ``` -Ohne diese Datei läuft die Extension rein lokal. +Ohne diese Datei läuft die Extension rein lokal. Optional `"interval"`: Ruhetakt in Sekunden statt 10. Server-API (alle Aufrufe mit `Authorization: Bearer `): diff --git a/desktop@marha.de/notes.js b/desktop@marha.de/notes.js index e9a5792..784f663 100644 --- a/desktop@marha.de/notes.js +++ b/desktop@marha.de/notes.js @@ -135,6 +135,9 @@ class NotesPanel extends St.BoxLayout { } this._noteTitle.text = selected.name; this._content.clutter_text.set_markup(renderMarkdown(text)); + // Clutter merkt sich die Textattribute des vorigen Inhalts für die Höhenberechnung. + // Ohne diesen Reset wird die Notiz mit größeren Überschriften unten abgeschnitten. + this._content.clutter_text.set_attributes(null); } else { this._noteTitle.text = ''; this._content.text = ''; diff --git a/desktop@marha.de/sync.js b/desktop@marha.de/sync.js index 6e5e20a..079995d 100644 --- a/desktop@marha.de/sync.js +++ b/desktop@marha.de/sync.js @@ -6,11 +6,19 @@ import {readJson, writeJson} from './store.js'; // Abgleich mit dem Server. Zugang steht in sync.json im Datenordner: // {"url": "https://planer.marha.de", "token": "...", "interval": 5} -// Alle paar Sekunden und kurz nach jeder eigenen Änderung: eigene Änderungen hochladen, -// Änderungen des Servers seit dem letzten Stand holen. Bei Konflikten gewinnt der Eintrag mit dem neueren updated. +// Kurz nach jeder eigenen Änderung und in einem Takt, der sich nach der Aktivität richtet: +// eigene Änderungen hochladen, Änderungen des Servers seit dem letzten Stand holen. +// Bei Konflikten gewinnt der Eintrag mit dem neueren updated. // sync-state.json merkt sich den Server-Stand (cursor) und was zuletzt übertragen wurde (pushed). -const DEFAULT_INTERVAL = 5; +// Takt nach der letzten Aktivität (eigene Änderung oder Änderung vom Server): +// erste Minute jede Sekunde, dann alle 5 Sekunden, nach 5 Minuten Ruhe alle 10 Sekunden. +// "interval" in sync.json ersetzt den Ruhetakt. +const INTERVALS = [ + {after: 0, seconds: 1}, + {after: 60, seconds: 5}, + {after: 5 * 60, seconds: 10}, +]; // Kurze Wartezeit nach einer eigenen Änderung, damit mehrere Klicks in einem Lauf landen const SOON_MS = 500; @@ -32,6 +40,8 @@ export class Sync { this._timerId = null; this._soonId = null; this._applying = false; + // Beim Start gilt: lange nichts passiert + this._lastActivity = 0; } get active() { @@ -41,18 +51,40 @@ export class Sync { start() { if (!this.active) return; - const interval = this._config.interval ?? DEFAULT_INTERVAL; - this._timerId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, interval, () => { - this._cycle().catch(e => this._fail(e.message)); - return GLib.SOURCE_CONTINUE; - }); // Eigene Änderungen gehen sofort raus, nicht erst beim nächsten Takt this._store.onChange(() => this._scheduleSoon()); this._cycle().catch(e => this._fail(e.message)); + this._scheduleNext(); + } + + // Aktueller Takt in Sekunden, je nachdem wie lange die letzte Aktivität her ist. + get interval() { + const idle = (GLib.get_monotonic_time() - this._lastActivity) / 1e6; + const step = [...INTERVALS].reverse().find(s => idle >= s.after); + const idleInterval = this._config.interval ?? INTERVALS.at(-1).seconds; + return step === INTERVALS.at(-1) ? idleInterval : step.seconds; + } + + _touchActivity() { + this._lastActivity = GLib.get_monotonic_time(); + } + + // Nächsten Lauf planen. Der Abstand wird jedes Mal neu bestimmt. + _scheduleNext() { + if (this._stopped) + return; + this._timerId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this.interval, () => { + this._timerId = null; + this._cycle().catch(e => this._fail(e.message)).finally(() => this._scheduleNext()); + return GLib.SOURCE_REMOVE; + }); } _scheduleSoon() { - if (this._stopped || this._applying || this._soonId) + if (this._stopped || this._applying) + return; + this._touchActivity(); + if (this._soonId) return; this._soonId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, SOON_MS, () => { this._soonId = null; @@ -82,6 +114,9 @@ export class Sync { try { const payload = this._collect(); const response = await this._post(payload); + // Kam etwas vom Server, ist das andere Gerät gerade aktiv: schneller nachfragen + if (['events', 'todos', 'notes', 'folders'].some(c => (response[c] ?? []).length)) + this._touchActivity(); // Übernommene Änderungen lösen selbst onChange aus, das soll keinen neuen Lauf starten this._applying = true; try { diff --git a/dev/restart.sh b/dev/restart.sh index 1e9a31f..c5dac5e 100755 --- a/dev/restart.sh +++ b/dev/restart.sh @@ -4,10 +4,10 @@ DIR=$(cd "$(dirname "$0")" && pwd) "$DIR/stop.sh" pgrep -f "^gnome-shell --(nested|headless)" | xargs -r kill 2>/dev/null rm -rf /tmp/desktop-app-nested/data/desktop-app -# Mit SYNC_URL bekommt die Test-Shell einen Server-Zugang (Token "test", alle 2 Sekunden) +# Mit SYNC_URL bekommt die Test-Shell einen Server-Zugang (Token "test") if [ -n "$SYNC_URL" ]; then mkdir -p /tmp/desktop-app-nested/data/desktop-app - echo "{\"url\": \"$SYNC_URL\", \"token\": \"test\", \"interval\": 2}" > /tmp/desktop-app-nested/data/desktop-app/sync.json + echo "{\"url\": \"$SYNC_URL\", \"token\": \"test\"}" > /tmp/desktop-app-nested/data/desktop-app/sync.json fi sleep 2 (nohup "$DIR/nested.sh" "$1" > /tmp/desktop-app-nested.log 2>&1 &) diff --git a/dev/tests/input.sh b/dev/tests/input.sh index c6cac37..c86716d 100755 --- a/dev/tests/input.sh +++ b/dev/tests/input.sh @@ -149,6 +149,7 @@ click 'cal._viewButtons.month' check "Fokusverlust beendet den Editor, Anzeige gerendert" "$(val '[notes._editing, notes._viewScroll.visible, notes._content.text].join("|")')" "false|true|Hallo Erste Notiz" +check "Notiz mit Überschrift wird nicht abgeschnitten" "$(val '(l => Math.round(l.get_preferred_height(l.width)[1]) >= l.clutter_text.get_layout().get_pixel_size()[1] && l.get_allocation_box().get_height() >= l.clutter_text.get_layout().get_pixel_size()[1])(notes._content)')" "true" click 'notes._viewScroll' check "Klick in die Fläche startet den Editor" "$(val '[notes._editing, global.stage.get_key_focus() === notes._editor.clutter_text].join(" ")')" "true true" type_ " mehr" diff --git a/dev/tests/sync.sh b/dev/tests/sync.sh index 67701cc..3470097 100755 --- a/dev/tests/sync.sh +++ b/dev/tests/sync.sh @@ -54,6 +54,7 @@ check "Server läuft" "$(curl -s $URL/api/health)" '{"ok":true}' echo "=== Shell mit Sync starten" SYNC_URL="$URL" "$DIR/restart.sh" headless >/dev/null check "Sync aktiv" "$(val 'ext._sync.active')" "true" +check "Ohne Aktivität Ruhetakt 10 s" "$(val 'ext._sync.interval')" "10" echo "=== Lokal → Server" "$E" "$PRE store.saveEvent({title: 'Zahnarzt', date: '2026-09-21', start: '09:00', end: '10:00', endDate: null}); store.saveTodo({title: 'Einkaufen', date: null, time: null}); 'ok'" >/dev/null @@ -61,11 +62,12 @@ sleep 3 check "Termin auf dem Server" "$(api "$URL/api/events" | python3 -c 'import json,sys; print([e["title"] for e in json.load(sys.stdin)])')" "['Zahnarzt']" check "Todo auf dem Server" "$(api "$URL/api/todos" | python3 -c 'import json,sys; print([t["title"] for t in json.load(sys.stdin)])')" "['Einkaufen']" check "Stand gemerkt" "$(val 'state().cursor > 0')" "true" +check "Nach eigener Änderung Takt 1 s" "$(val 'ext._sync.interval')" "1" "$E" "$PRE store.saveTodo({title: 'Sofort', date: null, time: null}); 'ok'" >/dev/null START=$(date +%s%N) for i in $(seq 1 40); do api "$URL/api/todos" | grep -q '"Sofort"' && break; sleep 0.1; done MS=$(( ($(date +%s%N) - START) / 1000000 )) -check "Eigene Änderung ist vor dem nächsten Takt (2 s) auf dem Server" "$([ $MS -lt 1500 ] && echo "schnell" || echo "langsam: ${MS}ms")" "schnell" +check "Eigene Änderung ist sofort auf dem Server" "$([ $MS -lt 1500 ] && echo "schnell" || echo "langsam: ${MS}ms")" "schnell" "$E" "$PRE store.removeTodo(store.todos.find(t => t.title === 'Sofort').id); 'ok'" >/dev/null sleep 2 @@ -73,6 +75,16 @@ echo "=== Server → lokal" push '{"todos": [{"id": "r1", "updated": "2026-09-19T10:00:00.000Z", "title": "Vom Handy", "date": null, "time": null, "done": false}], "notes": [{"id": "Remote/Idee.md", "updated": "2026-09-19T10:00:00.000Z", "content": "# Vom Server"}]}' sleep 3 check "Todo vom Server in der Liste" "$(val 'store.sortedTodos().map(t => t.title).sort().join(",")')" "Einkaufen,Vom Handy" +"$E" "$PRE ext._sync._lastActivity = GLib.get_monotonic_time() - 70e6; 'ok'" >/dev/null +check "Nach 70 s Ruhe Takt 5 s" "$(val 'ext._sync.interval')" "5" +push '{"todos": [{"id": "r2", "updated": "2026-09-19T10:00:00.000Z", "title": "Weckt auf", "date": null, "time": null, "done": false}]}' +sleep 7 +check "Änderung vom Server setzt Takt auf 1 s" "$(val 'store.todos.some(t => t.id === "r2") + " " + ext._sync.interval')" "true 1" +"$E" "$PRE ext._sync._lastActivity = GLib.get_monotonic_time() - 400e6; 'ok'" >/dev/null +check "Nach 5 min Ruhe Takt 10 s" "$(val 'ext._sync.interval')" "10" +# Aufräumen: Test-Todo weg, damit wieder schnell abgeglichen wird +"$E" "$PRE store.removeTodo('r2'); 'ok'" >/dev/null +sleep 2 check "Todo vom Server im Panel" "$(val 'labels(todos).includes("Vom Handy")')" "true" check "Notiz vom Server als Datei" "$(val 'exists(store.notesDir + "/Remote/Idee.md") + " " + JSON.stringify(read(store.notesDir + "/Remote/Idee.md"))')" 'true "# Vom Server"' check "Notiz vom Server im Baum" "$(val 'labels(notes).includes("▸ Remote")')" "true"