// --------------------------------------------------------------------------- // connectors/OdooConfig.tsx — WAVE 32 items 6/11, contract C2, rulings R9/R10/R11. // // ⛔ WHAT THE OWNER FOUND WHEN THEY CLICKED "Manage keys" ON ODOO: a list of // credentials. Not which server database this workspace reads, not which of the // ten mirrored grids it wants, not how often they sync, and no way to stop. This // panel is those four questions, on the card the question is asked from. // // ⛔⛔ THE PANEL IS A PURE COMPONENT AND THE FETCHING LIVES ABOVE IT, which is not // a style preference — it is what makes the ticket's `done-when` provable. The // page fetches on mount and `renderToStaticMarkup` does not run effects, so a // component that loads its own data is a spinner forever in a shot, and a ticket // whose done-when is "an admin SEES the server database and a checklist" cannot // be evidenced by a picture of a spinner. `ConnectorCard` owns the request; // `OdooConfigPanel` renders whatever it is handed. Same reason `ConnectorCard` // itself was lifted out of the page in wave 30. // // ⚠ NO CLIENT UNION OVER `syncEvery` OR THE GRID KEYS. Both are the server's // vocabulary (`odoo_relational.SYNC_PRESETS`, `odoo_relational.TABLES`) and // arrive as strings — a client union turns "the server added a preset" into "the // client drops the option" (the wave-9 law). // --------------------------------------------------------------------------- /** One materialisable grid, exactly as `GET /admin/connectors/odoo/config` sends it. */ export interface OdooGrid { key: string; label: string; enabled: boolean; } /** The config payload. Everything optional but `grids` is genuinely optional on the wire — * an older server, or a tenant with no Odoo at all, simply says less. */ export interface OdooConfig { applicable?: boolean; /** `keychain` | `env` | `none` — WHERE the credential lives, read as a string. */ source?: string; label?: string; /** The masked hint the keychain computed at write. Never the secret. */ preview?: string; serverDb?: string; serverUrl?: string; apiUser?: string; /** False for the deployment ENVIRONMENT: editing it here would be editing the container. */ serverDbEditable?: boolean; grids?: OdooGrid[]; syncEvery?: string; syncOptions?: string[]; syncFloorSeconds?: number; frozen?: boolean; frozenAt?: string; canDisconnect?: boolean; /** What the server did with what it was sent — a clamp, a refusal, a rewrite (W30/R6). */ notes?: string[]; } /** * The cadence words, in the two places a person reads them. ⚠ The MAP is presentation; the * OPTIONS come from the server (`syncOptions`), so a preset we have no phrase for still renders * — as its own key rather than as nothing. */ const EVERY_LABEL: Record = { "30m": "Every 30 minutes", "1h": "Every hour", "4h": "Every 4 hours", daily: "Once a day", manual: "Only when I ask", }; export const everyLabel = (k: string): string => EVERY_LABEL[k] || k; /** What the connection is, in one line, for the header of the panel. */ export function sourceLine(cfg: OdooConfig): string { if (cfg.frozen) return "Disconnected — these databases are frozen as static data"; if (cfg.source === "env") return "Connected using this deployment's own credentials"; if (cfg.source === "keychain") return "Connected using a key stored in this workspace"; return "Not connected"; } export default function OdooConfigPanel({ cfg, busy, onToggleGrid, onSyncEvery, onServerDb, onDisconnect, onReconnect, }: { cfg: OdooConfig; busy?: boolean; onToggleGrid: (key: string, enabled: boolean) => void; onSyncEvery: (every: string) => void; onServerDb: (db: string) => void; onDisconnect: () => void; onReconnect: () => void; }) { const grids = cfg.grids ?? []; const options = cfg.syncOptions ?? []; const on = grids.filter((g) => g.enabled).length; return (

{sourceLine(cfg)}

{/* ── the credential, as much of it as may be shown ────────────────────────────────── */}
Key {cfg.preview ? cfg.preview : "held on this deployment"} {cfg.apiUser ? " · " + cfg.apiUser : ""}
{cfg.serverUrl ? (
Server {cfg.serverUrl}
) : null}
Database {cfg.serverDbEditable ? ( // ⚠ A `defaultValue` + blur, not a controlled input: this component is PURE and holds // no state, and a controlled field with no owner is a box you cannot type in. { const next = e.target.value.trim(); if (next && next !== (cfg.serverDb ?? "")) onServerDb(next); }} /> ) : ( {cfg.serverDb || "—"} {/* R6's second sentence: a thing you cannot change says WHY, not nothing. */} — set on this deployment, not in this workspace )}
{/* ── R9's second reading: WHICH grids this workspace materialises ─────────────────── */}

Databases to build {on} of {grids.length}

{grids.map((g) => ( ))}
{/* ⛔ SAID BEFORE IT IS DONE, not after. Unticking stops the NEXT sync from rebuilding a database; it does not delete the one you already have. A checkbox that silently threw away rows would be the worst control on this page. */}

Unticking one stops it being rebuilt on the next sync. Nothing you already have is deleted.

{/* ── R11: the cadence, from the server's own preset list, floor included ──────────── */}

How often to sync

{/* ── R10: off, and back on ────────────────────────────────────────────────────────── */} {cfg.frozen ? (

Disconnected{cfg.frozenAt ? " on " + cfg.frozenAt.slice(0, 10) : ""}. Every row and every column you had is still here and still readable — nothing is being refreshed.

) : cfg.canDisconnect ? ( ) : null} {/* Whatever the server did with what it was sent — a clamp, a rewrite, a warning. */} {(cfg.notes ?? []).length ? ( ) : null}
); }