From c345ee71cfcd78813e3f0d41f1b80a3747073338 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Thu, 20 Aug 2026 21:07:38 +0200 Subject: [PATCH] mining --- custom/apps/mining-checker/assets/js/app.js | 111 ++++++++++++- custom/apps/mining-checker/docs/README.md | 12 ++ docs/CONTENT.md | 1 + docs/POSTGRES_STAGING_RECOVERY.md | 171 ++++++++++++++++++++ docs/README.md | 4 + 5 files changed, 292 insertions(+), 7 deletions(-) create mode 100644 docs/POSTGRES_STAGING_RECOVERY.md diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 38421c1f..3198b952 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -543,6 +543,11 @@ daily_cost_currency: 'EUR', report_currency: 'EUR', crypto_currency: 'DOGE', + crypto_base_price_amount: '5.49', + crypto_base_price_currency: 'USD', + min_offer_runtime_months: 24, + target_offer_hashrate_kh: 50, + target_offer_runtime_months: 36, preferred_currencies: ['DOGE', 'USD', 'EUR'], cost_plans: [], currencies: [], @@ -1049,6 +1054,11 @@ baseline_coins_total: '', report_currency: 'EUR', crypto_currency: 'DOGE', + crypto_base_price_amount: '5.49', + crypto_base_price_currency: 'USD', + min_offer_runtime_months: '24', + target_offer_hashrate_kh: '50', + target_offer_runtime_months: '36', }); const [offerPreviewForm, setOfferPreviewForm] = useState({ crypto_base_price_amount: '', @@ -1173,7 +1183,11 @@ const currentWalletWithdrawals = walletWithdrawalRows.length ? walletWithdrawalRows : (Array.isArray(currentSettings.wallet_withdrawals) ? currentSettings.wallet_withdrawals : []); const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : []; const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : []; + const currentSummaryMinerOffers = Array.isArray(payload?.summary?.miner_offers) ? payload.summary.miner_offers : []; const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase(); + const minOfferRuntimeMonths = Number(currentSettings.min_offer_runtime_months) > 0 ? Number(currentSettings.min_offer_runtime_months) : 24; + const targetOfferHashrateKh = Number(currentSettings.target_offer_hashrate_kh) > 0 ? Number(currentSettings.target_offer_hashrate_kh) : 50; + const targetOfferRuntimeMonths = Number(currentSettings.target_offer_runtime_months) > 0 ? Number(currentSettings.target_offer_runtime_months) : 36; const overviewPerDayLabel = `${currentMiningCurrency} pro Tag`; const latestMeasurementDate = latest && latest.measured_at ? parseStoredUtcDate(latest.measured_at) : null; const latestMeasurementAgeMs = latestMeasurementDate ? (Date.now() - latestMeasurementDate.getTime()) : null; @@ -1273,13 +1287,47 @@ return false; } - if (Number.isFinite(runtimeMonths) && runtime !== runtimeMonths) { + if (Number.isFinite(runtimeMonths) && runtime < runtimeMonths) { return false; } return true; }); const scenarioMinerOffers = filteredMinerOffers; + const preferredMinerOffer = currentSummaryMinerOffers + .filter((offer) => String(offer.payment_type || '').toLowerCase() === 'crypto') + .map((offer) => { + const offerHashrateKh = Number(offer.offer_hashrate_mh) * 1000; + const runtime = Number(offer.runtime_months); + return { + ...offer, + __runtimeDelta: Math.abs(runtime - targetOfferRuntimeMonths), + __hashrateDelta: Math.abs(offerHashrateKh - targetOfferHashrateKh), + }; + }) + .sort((left, right) => left.__runtimeDelta - right.__runtimeDelta || left.__hashrateDelta - right.__hashrateDelta)[0] || null; + const preferredOfferRequiredCoins = preferredMinerOffer + ? convertCurrencyValue( + preferredMinerOffer.effective_price_amount ?? preferredMinerOffer.crypto_display_price_amount, + preferredMinerOffer.effective_price_currency || preferredMinerOffer.crypto_display_price_currency || currentMiningCurrency, + currentMiningCurrency + ) + : null; + const preferredOfferCurrentCoins = latest ? Number(latest.coins_total_visible ?? latest.coins_total) : null; + const preferredOfferRemainingCoins = Number.isFinite(preferredOfferRequiredCoins) && Number.isFinite(preferredOfferCurrentCoins) + ? Math.max(0, preferredOfferRequiredCoins - preferredOfferCurrentCoins) + : null; + const preferredOfferRemainingDays = preferredOfferRemainingCoins !== null + && Number(latest?.doge_per_day_interval) > 0 + ? (preferredOfferRemainingCoins / Number(latest.doge_per_day_interval)) + : null; + const preferredOfferEta = preferredOfferRemainingDays !== null && latest?.measured_at + ? ( + preferredOfferRemainingDays <= 0 + ? latest.measured_at + : new Date(parseStoredUtcDate(latest.measured_at).getTime() + (preferredOfferRemainingDays * 86400000)).toISOString() + ) + : null; const speedUnits = ['kH/s', 'MH/s']; const fiatCurrencies = currencies.filter((currency) => !currency.is_crypto); const cryptoCurrencies = currencies.filter((currency) => !!currency.is_crypto); @@ -1791,7 +1839,31 @@ baseline_coins_total: normalized.settings.baseline_coins_total || '', report_currency: normalized.settings.report_currency || 'EUR', crypto_currency: normalized.settings.crypto_currency || 'DOGE', + crypto_base_price_amount: normalized.settings.crypto_base_price_amount !== null && normalized.settings.crypto_base_price_amount !== undefined + ? String(normalized.settings.crypto_base_price_amount) + : '5.49', + crypto_base_price_currency: normalized.settings.crypto_base_price_currency || 'USD', + min_offer_runtime_months: normalized.settings.min_offer_runtime_months !== null && normalized.settings.min_offer_runtime_months !== undefined + ? String(normalized.settings.min_offer_runtime_months) + : '24', + target_offer_hashrate_kh: normalized.settings.target_offer_hashrate_kh !== null && normalized.settings.target_offer_hashrate_kh !== undefined + ? String(normalized.settings.target_offer_hashrate_kh) + : '50', + target_offer_runtime_months: normalized.settings.target_offer_runtime_months !== null && normalized.settings.target_offer_runtime_months !== undefined + ? String(normalized.settings.target_offer_runtime_months) + : '36', }); + setOfferPreviewForm((current) => ({ + crypto_base_price_amount: normalized.settings.crypto_base_price_amount !== null && normalized.settings.crypto_base_price_amount !== undefined + ? String(normalized.settings.crypto_base_price_amount) + : (current.crypto_base_price_amount || '5.49'), + crypto_base_price_currency: normalized.settings.crypto_base_price_currency || current.crypto_base_price_currency || 'USD', + fiat_base_price_amount: current.fiat_base_price_amount || '', + })); + setMinerOfferFilters((previous) => ({ + ...previous, + runtime_months: previous.runtime_months || String(normalized.settings.min_offer_runtime_months ?? 24), + })); setFxSelection(Array.isArray(normalized.settings.preferred_currencies) && normalized.settings.preferred_currencies.length ? normalized.settings.preferred_currencies : ['DOGE', 'USD', 'EUR']); @@ -2296,6 +2368,11 @@ daily_cost_currency: currentSettings.daily_cost_currency, report_currency: settingsForm.report_currency || 'EUR', crypto_currency: settingsForm.crypto_currency || 'DOGE', + crypto_base_price_amount: settingsForm.crypto_base_price_amount || '5.49', + crypto_base_price_currency: settingsForm.crypto_base_price_currency || 'USD', + min_offer_runtime_months: Number(settingsForm.min_offer_runtime_months) || 24, + target_offer_hashrate_kh: Number(settingsForm.target_offer_hashrate_kh) || 50, + target_offer_runtime_months: Number(settingsForm.target_offer_runtime_months) || 36, preferred_currencies: Array.isArray(currentSettings.preferred_currencies) ? currentSettings.preferred_currencies : fxSelection, @@ -3223,6 +3300,21 @@ yLabel: `Kurs ${reportCurrency}`, })), ]), + preferredMinerOffer ? h(StatCard, { + key: 'preferred-offer-target', + label: `Zielminer ${fmtNumber(targetOfferHashrateKh, 0)} kH/s · ${targetOfferRuntimeMonths} Monate`, + value: preferredOfferRequiredCoins !== null ? `${fmtNumber(preferredOfferRequiredCoins, 4)} ${currentMiningCurrency}` : 'n/a', + sub: [ + preferredMinerOffer.usd_display_price_amount !== null ? `≈ ${fmtMoney(preferredMinerOffer.usd_display_price_amount, 'USD')}` : null, + preferredOfferRemainingDays !== null + ? `Rest ${fmtNumber(preferredOfferRemainingDays, 2)} Tage` + : 'Resttage n/a', + preferredOfferRemainingCoins !== null + ? `Rest-${currentMiningCurrency} ${fmtNumber(preferredOfferRemainingCoins, 4)}` + : null, + preferredOfferEta ? `ETA ${fmtDate(preferredOfferEta)}` : null, + ].filter(Boolean).join(' · '), + }) : null, panel('Zielmonitor', `Rest-${currentCoinCurrency} und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.`, h('div', { className: 'mc-target-grid' }, currentTargets.map((target, index) => h('div', { @@ -3642,9 +3734,9 @@ ]), panel('Miner-Angebote', selectedOfferType === 'crypto' ? (currentWalletMiningBalance !== null - ? `Crypto-Angebote sind aktiv. Angezeigt werden nur Angebote, die dein NC-Wallet aktuell tragen kann (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency} Gegenwert).` - : 'Crypto-Angebote sind aktiv. Solange kein Wallet-Bestand erkannt wird, bleiben die Krypto-Angebote sichtbar.') - : 'Fiat-Angebote sind aktiv. Die Berechnung startet erst, wenn du eine Geschwindigkeit eingibst.', + ? `Crypto-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt und nur Angebote gezeigt, die dein NC-Wallet aktuell tragen kann (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency} Gegenwert).` + : `Crypto-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt. Solange kein Wallet-Bestand erkannt wird, bleiben die Krypto-Angebote sichtbar.`) + : `Fiat-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt. Die Berechnung startet erst, wenn du eine Geschwindigkeit eingibst.`, [ h('div', { key: 'filters', className: 'mc-filter-grid' }, [ selectedOfferType === 'crypto' @@ -3688,9 +3780,9 @@ ? inputField('Max. DOGE', 'number', minerOfferFilters.max_doge, (value) => setMinerOfferFilters({ ...minerOfferFilters, max_doge: value }), '0.0001') : null, inputField(`Max. Basispreis (${reportCurrency})`, 'number', minerOfferFilters.price_max, (value) => setMinerOfferFilters({ ...minerOfferFilters, price_max: value }), '0.0001'), - selectField('Laufzeit', minerOfferFilters.runtime_months, [{ value: '', label: 'Alle Laufzeiten' }].concat(Array.from(new Set(evaluatedMinerOffers.map((offer) => String(offer.runtime_months || '')).filter(Boolean))).sort((a, b) => Number(a) - Number(b)).map((value) => ({ + selectField('Min. Laufzeit', minerOfferFilters.runtime_months, [{ value: '', label: 'Alle Laufzeiten' }].concat(Array.from(new Set(evaluatedMinerOffers.map((offer) => String(offer.runtime_months || '')).filter(Boolean))).sort((a, b) => Number(a) - Number(b)).map((value) => ({ value, - label: `${value} Monate`, + label: `ab ${value} Monate`, }))), (value) => setMinerOfferFilters({ ...minerOfferFilters, runtime_months: value })), ]), visibleOfferBasisHistory.length ? h('div', { key: 'basis-history', className: 'mc-mini-grid' }, visibleOfferBasisHistory.map((entry) => h('div', { @@ -4138,7 +4230,7 @@ ]), ]), h('div', { className: 'mc-stack' }, [ - panel('Basis-Settings', 'Baseline bleibt als Referenzwert mit Datum und Uhrzeit bestehen.', h('form', { + panel('Basis-Settings', 'Baseline bleibt als Referenzwert mit Datum und Uhrzeit bestehen. Angebotsbasis und Zielminer steuern die automatische Miner-Kalkulation auf der Übersicht.', h('form', { className: 'mc-form', onSubmit: submitSettings, }, [ @@ -4146,6 +4238,11 @@ inputField('Baseline Coins', 'number', settingsForm.baseline_coins_total, (value) => setSettingsForm({ ...settingsForm, baseline_coins_total: value }), '0.000001'), selectField('Standard-FIAT-Währung', settingsForm.report_currency || 'EUR', selectableFiatCurrencies.map((currency) => currency.code), (value) => setSettingsForm({ ...settingsForm, report_currency: value })), selectField('Standard-Krypto-Währung', settingsForm.crypto_currency || 'DOGE', selectableCryptoCurrencies.map((currency) => currency.code), (value) => setSettingsForm({ ...settingsForm, crypto_currency: value })), + inputField('Krypto-Basispreis 50 kH/s · 3 Monate', 'number', settingsForm.crypto_base_price_amount, (value) => setSettingsForm({ ...settingsForm, crypto_base_price_amount: value }), '0.000001'), + selectField('Basiswaehrung Krypto', settingsForm.crypto_base_price_currency || 'USD', Array.from(new Set(['USD', String(settingsForm.crypto_currency || 'DOGE').toUpperCase()])), (value) => setSettingsForm({ ...settingsForm, crypto_base_price_currency: value || 'USD' })), + inputField('Min. Mietlaufzeit in Monaten', 'number', settingsForm.min_offer_runtime_months, (value) => setSettingsForm({ ...settingsForm, min_offer_runtime_months: value }), '1'), + inputField('Zielminer Hashrate in kH/s', 'number', settingsForm.target_offer_hashrate_kh, (value) => setSettingsForm({ ...settingsForm, target_offer_hashrate_kh: value }), '1'), + inputField('Zielminer Laufzeit in Monaten', 'number', settingsForm.target_offer_runtime_months, (value) => setSettingsForm({ ...settingsForm, target_offer_runtime_months: value }), '1'), h('button', { type: 'submit', className: 'mc-button mc-button--primary', diff --git a/custom/apps/mining-checker/docs/README.md b/custom/apps/mining-checker/docs/README.md index f2149bd0..c33e3261 100644 --- a/custom/apps/mining-checker/docs/README.md +++ b/custom/apps/mining-checker/docs/README.md @@ -112,3 +112,15 @@ Pro Abruf entsteht genau ein Datensatz in `fx-rates` mit Basiswaehrung, Provider Falls noch historische Mining-Checker-Fetches in `miningcheck_fx_fetches` und `miningcheck_fx_rates` liegen, kann `POST /api/mining-checker/v1/projects/{projectKey}/legacy-fx-migrate` diese nach `fx-rates` ueberfuehren. Danach werden bestehende Messpunkte soweit moeglich auf die passende `fx_fetch_id` aktualisiert. Fuer Auswertungen, Berichte und Listen speichert der Mining-Checker pro Messpunkt die damals passende `fx_fetch_id`. Historische Umrechnungen laufen damit gegen genau den zugeordneten `fx-rates`-Snapshot. + +## Miner-Basis und Zielminer + +Die Angebotsberechnung fuer Krypto-Miner nutzt eine konfigurierbare Basis fuer `50 kH/s` und `3 Monate`. Standard ist `5.49 USD`, kann aber in den Modul-Settings angepasst werden. + +Zusaetzlich koennen in den Settings diese Leitwerte hinterlegt werden: + +- minimale gewuenschte Mietlaufzeit in Monaten +- Zielminer-Hashrate in `kH/s` +- Zielminer-Laufzeit in Monaten + +Die Uebersicht berechnet daraus automatisch den theoretischen Preis des Zielminers, den benoetigten Krypto-Gegenwert und die Resttage bis zur theoretischen Anmietung auf Basis des letzten Uploads. diff --git a/docs/CONTENT.md b/docs/CONTENT.md index 08a842a8..270bb786 100644 --- a/docs/CONTENT.md +++ b/docs/CONTENT.md @@ -121,6 +121,7 @@ Aus [README.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/docs/READ - Keycloak bleibt das Auth-System - das globale Desktop-Debug ist als Shell-Werkzeug unter `system/shell/desktop-debug/` definiert - der Gitea-Deploy-Status liegt als optionales Addon unter `system/addons/gitea-deploy-status/` +- fuer die Staging-PostgreSQL-Instanz mit Portainer-Stack und Bind-Mount gibt es ein zentrales Recovery-Runbook unter `docs/POSTGRES_STAGING_RECOVERY.md` ### Module diff --git a/docs/POSTGRES_STAGING_RECOVERY.md b/docs/POSTGRES_STAGING_RECOVERY.md new file mode 100644 index 00000000..357e0c49 --- /dev/null +++ b/docs/POSTGRES_STAGING_RECOVERY.md @@ -0,0 +1,171 @@ +# PostgreSQL Staging Recovery + +Runbook fuer den Fall, dass die Staging-PostgreSQL-Instanz per Portainer-Stack nicht mehr sauber startet und WAL- oder Checkpoint-Schaeden meldet. + +Stand dieses Runbooks: + +- Datum: `2026-08-20` +- Zielsystem: `staging.desktop.kusche.berlin` +- PostgreSQL-Image: `postgres:15-alpine` +- Datenpfad als Bind-Mount: `/mnt/db/postgres/nexus/staging:/var/lib/postgresql/data` +- Init-SQL als Bind-Mount: `/mnt/docker/containerdata/stackfiles/int/nexus/init-staging.sql:/docker-entrypoint-initdb.d/init.sql:ro` + +## Typische Fehlersymptome + +Beispielhafte Logmeldungen: + +- `invalid resource manager ID in primary checkpoint record` +- `PANIC: could not locate a valid checkpoint record` +- `database system was interrupted` + +Die Meldung `chmod: /var/run/postgresql: Operation not permitted` kann daneben auftreten, war in diesem Fall aber nicht die eigentliche Ursache des Startfehlers. + +## Wichtige Grundregeln + +- die bestehende Datenbasis nicht ungesichert loeschen +- in Portainer kein Volume-Reset oder `down -v` ausloesen +- vor jeder Reparatur ein Dateisystem-Backup des kompletten Bind-Mounts anlegen +- `pg_resetwal` nur als Rettungsmassnahme verwenden +- nach erfolgreichem Start immer sofort einen logischen Dump erzeugen +- eine mit `pg_resetwal` gerettete Datenbank nicht dauerhaft als Zielzustand weiterbetreiben + +## Verwendete Sicherungen + +Im dokumentierten Staging-Fall wurden diese Sicherungen erzeugt: + +- Roh-Backup des Datenverzeichnisses: `~/pg-backups/postgres-staging-backup-2026-08-20.tar.gz` +- logischer Dump: `~/pg-backups/staging-rettung.dump` + +## Sofortmassnahmen bei WAL- oder Checkpoint-Schaeden + +### 1. Stack stoppen + +In Portainer den betroffenen Staging-Stack vollstaendig stoppen, damit PostgreSQL nicht parallel auf das Datenverzeichnis schreibt. + +### 2. Bind-Mount sichern + +Auf dem Docker-Host: + +```bash +mkdir -p ~/pg-backups +tar czf ~/pg-backups/postgres-staging-backup-2026-08-20.tar.gz -C /mnt/db/postgres/nexus staging +ls -lh ~/pg-backups/postgres-staging-backup-2026-08-20.tar.gz +``` + +### 3. Diagnosecontainer mit passender PostgreSQL-Major starten + +```bash +docker run --rm -it \ + -v /mnt/db/postgres/nexus/staging:/var/lib/postgresql/data \ + postgres:15-alpine \ + sh +``` + +Im Container: + +```bash +pg_controldata /var/lib/postgresql/data +``` + +### 4. `pg_resetwal` erst als Dry-Run + +`pg_resetwal` muss als PostgreSQL-Superuser ausgefuehrt werden. Deshalb Diagnosecontainer als User `postgres` starten: + +```bash +docker run --rm -it \ + --user postgres \ + -v /mnt/db/postgres/nexus/staging:/var/lib/postgresql/data \ + postgres:15-alpine \ + sh +``` + +Im Container: + +```bash +pg_resetwal -n /var/lib/postgresql/data +``` + +Wenn der Dry-Run plausibel aussieht, kann die eigentliche WAL-Ruecksetzung ausgefuehrt werden: + +```bash +pg_resetwal -f /var/lib/postgresql/data +``` + +Danach den Container verlassen: + +```bash +exit +``` + +### 5. Stack wieder starten + +In Portainer den Staging-Stack wieder starten und pruefen, ob PostgreSQL ohne neue Fehler hochkommt. + +### 6. Sofort einen logischen Dump erzeugen + +Containernamen und Umgebungsvariablen pruefen: + +```bash +docker ps +docker exec -it env | grep POSTGRES +``` + +Im dokumentierten Fall waren die Werte: + +- Datenbank: `nexus_staging` +- User: `rootNexusStaging` + +In den laufenden Container wechseln: + +```bash +docker exec -it sh +``` + +Im Container: + +```bash +pg_dump -Fc -U rootNexusStaging nexus_staging > /tmp/staging-rettung.dump +ls -lh /tmp/staging-rettung.dump +``` + +Zurueck auf dem Host den Dump herauskopieren: + +```bash +docker cp :/tmp/staging-rettung.dump ~/pg-backups/staging-rettung.dump +``` + +## Zielzustand nach erfolgreicher Rettung + +Nach einem erfolgreichen `pg_resetwal` ist die Instanz nur wieder startbar gemacht. Der saubere Zielzustand ist: + +1. frisches leeres PostgreSQL-Datenverzeichnis erzeugen +2. neue saubere Staging-Instanz initialisieren +3. `staging-rettung.dump` in die neue Instanz zurueckspielen +4. erst danach die alte reparierte Datenbasis ersetzen oder archivieren + +## Restore in eine frische Staging-Instanz + +Der empfohlene Ablauf ist: + +1. aktuelle gerettete Datenbasis zusaetzlich archivieren +2. neues leeres Datenverzeichnis anlegen, z. B. `/mnt/db/postgres/nexus/staging-fresh` +3. Portainer-Stack oder Compose temporaer auf das neue Datenverzeichnis zeigen lassen +4. Stack starten, damit PostgreSQL sauber initialisiert +5. Dump `~/pg-backups/staging-rettung.dump` in die neue Instanz kopieren +6. mit `pg_restore` zurueckspielen +7. Anwendung gegen die frische Instanz testen +8. erst nach erfolgreichem Test das alte Datenverzeichnis abloesen oder umbenennen + +Beispiel fuer den Restore in einen laufenden Container: + +```bash +docker cp ~/pg-backups/staging-rettung.dump :/tmp/staging-rettung.dump +docker exec -it sh +pg_restore --clean --if-exists -U rootNexusStaging -d nexus_staging /tmp/staging-rettung.dump +``` + +Je nach Zielzustand kann statt `--clean --if-exists` auch ein Restore in eine frisch erzeugte leere Datenbank sinnvoller sein. + +## Ablage im Projekt + +Dieses Runbook dokumentiert den echten Staging-Vorfall vom `2026-08-20` und gilt als Referenz fuer weitere PostgreSQL-Wiederherstellungen auf diesem Projektstand. diff --git a/docs/README.md b/docs/README.md index 9d82d72a..6ab299ea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,9 @@ Zentraler Dokumentationsindex fuer das Projekt. - [Umsetzungsanweisung/START_HIER.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/docs/Umsetzungsanweisung/START_HIER.md) Einstieg in die fachlichen Umsetzungsanweisungen und deren Reihenfolge. +- [POSTGRES_STAGING_RECOVERY.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berlin/docs/POSTGRES_STAGING_RECOVERY.md) + Runbook fuer WAL-/Checkpoint-Schaeden und Wiederherstellung der Staging-PostgreSQL-Instanz. + ## Technische Projektstruktur - `public/` Web-Root mit Desktop-Shell auf `/` @@ -70,3 +73,4 @@ Aktuell wichtig: - `Systemtools` und installierbare `Module` werden getrennt behandelt; Systemtools sind nicht Teil der Benutzer-Installationsauswahl - das `Cron Tool` ist als globales Systemtool vorhanden und sammelt Modul-Cronjobs automatisch ueber Manifest-Metadaten - Widgets und Cron-Endpunkte sollen von Modulen bevorzugt direkt in `module.json` beschrieben werden +- das PostgreSQL-Staging-Recovery-Runbook fuer Portainer-Stacks mit Bind-Mount liegt zentral unter `docs/POSTGRES_STAGING_RECOVERY.md`