Soubor jako uloziste, kdyz neni databaze

Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor. Prezije
restart procesu i containeru, ale ne redeploy - filesystem containeru je
docasny. Je to mezistupen, ne nahrada databaze, a tak je to i napsane v portalu.

| Rezim    | Kdy                               | Restart | Redeploy |
| -------- | --------------------------------- | ------- | -------- |
| postgres | DATABASE_URL i SECRETS_KEY        | prezije | prezije  |
| file     | neni DB, ale je DATA_DIR          | prezije | ne       |
| memory   | ani jedno, nebo nejde zapsat      | ne      | ne       |

Rozhodnuti zustava na jednom miste (src/data/connectorStore.ts).

Pridano:
- src/data/snapshot.ts: atomicky zapis (.tmp a prejmenovani), slucovani zapisu
  a dokonceni rozepsaneho zapisu pri SIGTERM. Bez atomickeho zapisu by pad
  uprostred nechal polovicni JSON, ktery se pri startu nenacte. Rozbity soubor
  se prejmenuje na .broken a jede se dal - aplikace, ktera nenastartuje, je pro
  AppFactory nefunkcni sluzba
- src/data/connectors/local.ts: jeden kod pro pamet i soubor, lisi se jen tim,
  kam se zapisuje. Nahrazuje memory.ts, dve implementace by se casem rozesly
- klic k sifrovani se mimo databazi vygeneruje do DATA_DIR/secrets.key s pravy
  0600, takze sifrovani funguje bez nastaveni. Chrani to proti nahodnemu
  precteni JSONu, ne proti pristupu k disku - klic lezi vedle dat a je to tak
  napsane i v portalu. U databaze se negeneruje vubec: kdo ma zalohu tabulky,
  ma i klic ze stejneho stroje
- DATA_DIR v konfiguraci, data/ v .gitignore a .dockerignore
- hlaska v portalu rozlisuje tri nasledky: pamet, soubor a databaze

Overeno bez databaze: konektor s vyplnenymi udaji prezil restart, v JSONu jsou
hodnoty sifrovane a plaintext v nem neni. Pote s databazi: rezim postgres
funguje dal a klic vedle dat se nevygeneroval. Kontejner i data/ po overeni
smazany.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
JiriUhlir
2026-08-12 15:18:05 +02:00
co-authored by Claude Opus 5
parent 61cf29878f
commit 6e3d0640ff
12 changed files with 334 additions and 273 deletions
+43 -10
View File
@@ -2,6 +2,7 @@ import {
AlertTriangle,
CheckCircle2,
Eye,
Info,
EyeOff,
Pencil,
Plus,
@@ -118,17 +119,9 @@ export default function Connectors() {
{/*
Kam se uklada. Bez teto hlasky se clovek divi, kam se podely jeho
konektory po restartu, a hleda chybu v aplikaci.
konektory, a hleda chybu v aplikaci - presne to se stalo.
*/}
{storage?.ephemeral && (
<p className="flex items-start gap-2 rounded-card border border-warn-400/40 bg-warn-500/8 p-3 text-sm text-warn-400">
<AlertTriangle className="mt-0.5 size-4 shrink-0" />
<span>
Přístupové údaje se ukládají jen do paměti serveru a restart je smaže.
<span className="mt-0.5 block text-white/50">{storage.reason}</span>
</span>
</p>
)}
{storage && storage.mode !== 'postgres' && <StorageNotice storage={storage} />}
<DataState loading={loading} error={error} onRetry={load}>
{(connectors?.length ?? 0) === 0 ? (
@@ -165,6 +158,46 @@ export default function Connectors() {
);
}
/**
* Hlaska o ulozisti.
*
* Rozlisuje tri veci, protoze pro uzivatele znamenaji uplne jiny nasledek:
* pamet (ztrata pri restartu), soubor (ztrata pri redeployi) a databaze
* (bez ztraty, hlaska se nezobrazuje).
*/
function StorageNotice({ storage }: { storage: StorageStatus }) {
const severe = storage.mode === 'memory';
return (
<div
className={cn(
'flex items-start gap-2 rounded-card border p-3 text-sm',
severe ? 'border-warn-400/40 bg-warn-500/8' : 'border-ink-600/70 bg-ink-850/50',
)}
>
{severe ? (
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-warn-400" />
) : (
<Info className="mt-0.5 size-4 shrink-0 text-white/40" />
)}
<div>
<p className={severe ? 'text-warn-400' : 'text-white/70'}>
{severe
? 'Přístupové údaje se ukládají jen do paměti serveru, restart je smaže.'
: 'Data se ukládají do souboru na serveru. Restart přežijí, nové nasazení ne.'}
</p>
<p className="mt-0.5 text-xs text-white/45">{storage.reason}</p>
{storage.secrets === 'file' && (
<p className="mt-0.5 text-xs text-white/35">
Údaje jsou zašifrované klíčem, který leží ve stejné složce. Chrání to proti
náhodnému přečtení souboru, ne proti přístupu k disku serveru.
</p>
)}
</div>
</div>
);
}
// ----------------------------------------------------------------- mala karta
function ConnectorCard({
+10 -4
View File
@@ -292,13 +292,19 @@ export interface Connector {
ready: boolean;
}
/** Kam se uklada a jestli to prezije restart. */
/** Kam se uklada a co to prezije. */
export interface StorageStatus {
mode: 'postgres' | 'memory';
/** Proc se jede v pameti. null u databaze. */
reason: string | null;
mode: 'postgres' | 'file' | 'memory';
/** Kratke vysvetleni pro uzivatele. */
reason: string;
/** true = restart procesu data smaze. */
ephemeral: boolean;
/** true = redeploy containeru data smaze, i kdyz restart ne. */
lostOnRedeploy: boolean;
/** Odkud je klic k sifrovani tajnych hodnot. */
secrets: 'env' | 'file' | 'none';
/** Kde data lezi. null u pameti. */
location: string | null;
}
export interface ConnectorTestResult {