Playbook: Pinbox24 W4 — Deleted Records Cleanup

Typ: Maintenance / Data Cleanup Serwis: w4_db (regRecords + files), Wasabi S3 (3 regiony) Kontener: s3-v2-v42-prod @ bms-1 (94.23.26.113) Skrypt: scripts/cleanup-deleted-records.js GH tracking: (utwórz issue przed pierwszym wykonaniem)


Co robi ten cleanup

Usuwa z w4_db rekordy oznaczone deleted: true w kolekcji regRecords wraz z:

  1. Plikiem na Wasabi — primary bucket (files.bucket, endpoint EU eu-central-1) + każda kopia w storageInfo[] gdzie exist: true
  2. Dokumentem w files collection — 1 dokument per plik (repliki cross-region śledzone w storageInfo[], nie jako osobne dokumenty)
  3. Rekordem regRecords
regRecord (deleted=true)
  └─ recordData.recordMainDocument = files._id
       └─ files doc
            ├─ bucket: "test-replicated-to-us-bucket"   endpoint: s3.eu-central-1.wasabisys.com  ← primary
            ├─ storageInfo[0]: bucketName: "test-us-bucket-for-replication-testing"  endpoint: s3.us-east-2.wasabisys.com
            └─ storageInfo[1]: bucketName: "p24-was-us-east-1"                       endpoint: s3.us-east-1.wasabisys.com

Conservative policy: jeśli którykolwiek deleteObject zawiedzie — rekord zapisywany do CLEANUP_ERRORS.json, MongoDB (files + regRecord) nie jest usuwane dla tego rekordu, skrypt kontynuuje.


Kiedy uruchamiać

  • Scheduled: raz na miesiąc (np. w nocy, po godzinie ruchu)
  • On-demand: gdy files collection lub Wasabi bucket wykazuje wysokie użycie, po masowych operacjach usuwania rekordów (np. Stock cleanup)

Wstępny audyt (2026-07-04): ~10 427 rekordów deleted: true (szczyt maj 2025: 8 231, głównie status Stock).


Wymagania wstępne

  • SOPS split wykonany — secrets/pinbox24-w4.env.sops istnieje z kluczami Wasabi i MongoDB W4 (tracking issue)
  • pinbox24PublicAccessKeyId / pinbox24PublicSecretAccessKey mają uprawnienia do delete na bucketach: pinbox24, test-replicated-to-us-bucket, test-us-bucket-for-replication-testing, p24-was-us-east-1
  • Kontener s3-v2-v42-prod działa na bms-1 i ma dostęp do MongoDB w4_db

Kroki

Krok 1 — Skopiuj skrypt do kontenera

# Na maszynie dev (Windows):
scp C:\code_2026\p24-infra\scripts\cleanup-deleted-records.js root@94.23.26.113:/root/cleanup-deleted-records.js
 
# Na bms-1:
docker cp /root/cleanup-deleted-records.js s3-v2-v42-prod:/app/cleanup-deleted-records.js

Krok 2 — Dry run (OBOWIĄZKOWY)

docker exec s3-v2-v42-prod sh -c 'cd /app && node cleanup-deleted-records.js --dry-run 2>&1'

Sprawdź raport walidacyjny:

docker exec s3-v2-v42-prod cat /tmp/DELETED_RECORDS_CLEANUP_VALIDATION_REPORT.json

Zweryfikuj:

  • totals.regRecordsToDelete — oczekiwane ~10 427 (może być więcej bez age cutoff)
  • totals.wasabiEndpointsTotal > 0 — storageInfo lookup działa
  • sample_records[0].filesPreview — pokazuje bucket/endpoint dla każdej kopii
  • breakdowns.byStatus — czy są niespodziewane statusy (np. nie-deleted rekordy)?

Nie kontynuuj jeśli liczby wyglądają nieprawidłowo — zbadaj przed —execute.

Krok 3 — Execute

docker exec s3-v2-v42-prod sh -c 'cd /app && node cleanup-deleted-records.js --execute --confirmed 2>&1'

Skrypt loguje postęp co 20 rekordów. Dla ~10k rekordów z 3 endpointami oczekiwany czas: 60–90 minut.

Krok 4 — Natychmiast skopiuj audit log (PRZED restartem kontenera!)

Kontener jest ephemeral — przy restarcie /tmp/ jest kasowane. Pliki MUSZĄ być skopiowane.

# Na bms-1:
docker cp s3-v2-v42-prod:/tmp/DELETED_RECORDS_CLEANUP_LOG.jsonl /root/cleanup-log-$(date +%Y%m%d).jsonl
 
# Opcjonalnie — skopiuj na maszynę dev:
# scp root@94.23.26.113:/root/cleanup-log-*.jsonl C:\backup\

Jeśli istnieje CLEANUP_ERRORS.json — skopiuj też:

docker cp s3-v2-v42-prod:/tmp/CLEANUP_ERRORS.json /root/cleanup-errors-$(date +%Y%m%d).json 2>/dev/null || true

Krok 5 — Weryfikacja

# Sprawdź czy coś zostało
docker exec s3-v2-v42-prod sh -c 'cd /app && node -e "
  var m = require(\"mongoose\");
  m.connect(process.env.DB_URI || process.env.MONGODB_URI || process.env.NEW_MONGODB_URI).then(async function(c) {
    var n = await c.connection.db.collection(\"regRecords\").countDocuments({ deleted: true });
    console.log(\"deleted=true remaining: \" + n);
    process.exit(0);
  });
" 2>&1'

Oczekiwane: deleted=true remaining: 0 (lub nowe rekordy dodane podczas cleanup).


Obsługa błędów

CLEANUP_ERRORS.json istnieje

Rekordy w tym pliku miały Wasabi delete failure — MongoDB pozostało nienaruszone.

[
  { "recordId": "...", "fileId": "...", "endpoint": "s3.us-east-2.wasabisys.com", "bucket": "test-us-bucket-for-replication-testing", "error": "..." }
]

Retry: Uruchom skrypt ponownie — przy --execute --confirmed ponowi próbę dla tych rekordów (query deleted: true trafi na te same rekordy, które nie zostały usunięte). Jeśli błąd Wasabi jest trwały (np. brak uprawnień), napraw klucze API i powtórz.

Skrypt przerwany w połowie

Conservative policy działa per-rekord. Bezpiecznie uruchomić --execute --confirmed ponownie — rekordy już usunięte z DB nie pojawią się w query (query szuka deleted: true). Wasabi double-delete jest idempotentny (S3 nie zwraca błędu dla nieistniejącego obiektu).


Rollback / Recovery

Usunięcia z Wasabi i MongoDB są NIEODWRACALNE.

  • Nie ma rollback dla usuniętych danych.
  • Jedynym źródłem prawdy o tym co zostało usunięte jest audit log z Kroku 4.
  • Jeśli audit log nie został skopiowany przed restartem kontenera — informacja o usuniętych plikach jest bezpowrotnie utracona.

Minimalny recovery plan:

  1. Sprawdź /root/cleanup-log-YYYYMMDD.jsonl na bms-1 (skopiowany w Kroku 4)
  2. Każdy wpis zawiera: recordId, officeId, regId, recordMainDocument, filesDeleted[] z pełnymi lokalizacjami Wasabi
  3. Backup Wasabi: jeśli bucket replication policy to umożliwia — sprawdź Wasabi Console → Bucket → Replication Policy → czy usunięcie propaguje się do repliki

Struktura kopii Wasabi

Pinbox24 przechowuje jeden dokument w files collection per plik. Repliki cross-region śledzone są w tablicy storageInfo[] wewnątrz tego samego dokumentu:

PoleOpis
files.bucketPrimary bucket (np. test-replicated-to-us-bucket)
endpoint primaryZawsze s3.eu-central-1.wasabisys.com (hardcoded w skrypcie)
storageInfo[n].bucketNameNazwa bucketu repliki
storageInfo[n].endpointURL endpointu repliki (strip https:// przed użyciem)
storageInfo[n].existtrue = replika istnieje, false = pominąć

Skrypt deduplikuje listę (bucket, endpoint) przed wywołaniem deleteObject, żeby uniknąć podwójnego delete jeśli primary i storageInfo[0] wskazują na ten sam bucket.


Znane ograniczenia

  • Node.js w kontenerze jest stary (10.x/12.x) — brak optional chaining ?.. Skrypt używa (obj || {}).prop.
  • Skrypt jest sekwencyjny — ~10k rekordów z 3 endpointami = 30k API calls = ok. 60–90 minut.
  • Brak Error Notification Standard dla CLEANUP_ERRORS.json jeśli P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL nie jest ustawione w kontenerze. Sprawdź env vars kontenera po SOPS split.

Historia

DataDziałanie
2026-07-04Audyt: 10 427 rekordów deleted: true w w4_db (spike maj 2025: 8 231 Stock)
2026-07-05Skrypt napisany, playbook utworzony. Blocked: SOPS split prerequisite