diff --git a/docs/de/cloud/data-residency.md b/docs/de/cloud/data-residency.md index b977c7ba86..70d2e14682 100644 --- a/docs/de/cloud/data-residency.md +++ b/docs/de/cloud/data-residency.md @@ -9,7 +9,7 @@ Die Default-Region für neue Cloud-Orgs ist die Schweiz. Die Region nach der Anm ## Ein durchgespieltes Beispiel — ein Chat-Roundtrip -Der User in Zürich öffnet Chat und sendet „fass den letzten Kundenanruf zusammen". Die Anfrage trifft Tales Edge in der gewählten Region, landet auf `tale-platform`, ruft in `tale-convex` (das Backend), liest Wissen aus der Datenbank des Wissens-Korpus, sobald das Wissens-Tool des Agents danach fragt, und emittiert einen ausgehenden Anruf an den Provider hinter dem Modell, das die sendende Person gewählt hat. Der Wissens-Abruf läuft im Convex-Backend — es fragt die Korpus-Datenbank direkt ab, ohne separaten Retrieval-Dienst im Pfad. Der Modell-Provider gibt Tokens zurück; Tale streamt sie auf demselben Pfad zurück. Die Antwort und die Zitate landen in der operativen Datenbank, der Korpus bleibt in der Wissensdatenbank, und beide werden innerhalb der Region repliziert. +Der User in Zürich öffnet Chat und sendet „fass den letzten Kundenanruf zusammen". Die Anfrage trifft Tales Edge in der gewählten Region, die die Seite aus der Web-Schicht ausliefert und die Nachricht selbst an das Anwendungs-Backend routet. Das Backend liest Wissen aus der Korpus-Datenbank, sobald das Wissens-Tool des Agents danach fragt, und schickt einen ausgehenden Aufruf an den Provider hinter dem Modell, das die sendende Person gewählt hat. Der Wissens-Abruf läuft im selben Backend-Prozess — er fragt die Korpus-Datenbank direkt ab, ohne separaten Retrieval-Dienst im Pfad. Der Modell-Provider gibt Tokens zurück; das Backend streamt sie an den Browser. Die Antwort und die Zitate landen in der operativen Datenbank, der Korpus bleibt in der Wissensdatenbank, jede Datei, die der Turn erzeugt hat, landet im Objektspeicher der Region, und alle drei werden innerhalb der Region repliziert. Zwei Pfeile überqueren in diesem Trip die regionale Grenze: der Anruf an den Modell-Provider (immer extern) und jeder Sub-Prozessor, den die Tools des Agents ausgelöst haben (Web-Fetch, OneDrive-Lese, MCP-Server in einer anderen Region). Alles andere bleibt in der Region. diff --git a/docs/de/develop/api-reference.md b/docs/de/develop/api-reference.md index 0c861bb7b6..6a35e271b0 100644 --- a/docs/de/develop/api-reference.md +++ b/docs/de/develop/api-reference.md @@ -240,7 +240,7 @@ curl -sS "https://your-host.example.com/api/v1/tasks/" \ # → 200 { "task": { "id": "", "title": "...", "status": "in_progress", "externalId": "case-991", "labels": [], ... } } ``` -Und hol die Ergebnisse. Was die Automatisierung zurückgemeldet hat, steht in der Diskussion der Aufgabe; was sie abgelegt hat, liegt als Dateien im Quartalsordner — beides liest du durch denselben Zugang. Der Content-Endpoint streamt einen Convex-Blob direkt; bei einer Organisation mit eigenem Objektspeicher antwortet er mit **302** auf eine kurzlebige präsignierte URL, folge also Redirects: +Und hol die Ergebnisse. Was die Automatisierung zurückgemeldet hat, steht in der Diskussion der Aufgabe; was sie abgelegt hat, liegt als Dateien im Quartalsordner — beides liest du durch denselben Zugang. Der Content-Endpoint streamt selbst keine Bytes: Jede Datei liegt im Objektspeicher, er antwortet also immer mit **302** auf eine kurzlebige präsignierte URL. Folge Redirects und behandle diese URL wie ein Credential — sie gibt die Bytes an jeden heraus, der sie hat, bis sie abläuft: ```bash curl -sS "https://your-host.example.com/api/v1/tasks//comments" \ diff --git a/docs/de/develop/status-page.md b/docs/de/develop/status-page.md index 8041daec72..40a8eadfb8 100644 --- a/docs/de/develop/status-page.md +++ b/docs/de/develop/status-page.md @@ -21,11 +21,11 @@ Der RSS-Feed trägt jeden Status-Wechsel — offen, Update, gelöst — für jed | Service | Was er abdeckt | Wann er rot wird | | ---------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------- | -| `platform` | Die TanStack-Start-+-Convex-Anwendung — Agents, Workflows, Connectors, UI. | UI nicht erreichbar; API gibt 5xx; Auth defekt. | +| `platform` | Der TanStack-Start-UI-Server und das Node-Backend dahinter — Agents, Workflows, Connectors, UI. | UI nicht erreichbar; API gibt 5xx; Auth defekt. | | `rag` | Der Python-FastAPI-Dokumentdienst — Indexierung, Retrieval. | Dokument-Uploads stocken; Retrieval ist leer. | | `crawler` | Der Crawl4AI-Web-Extraktionsdienst — verwendet von Document-Ingest und Tavily-Fallback. | Web-gezogene Dokumente scheitern; Deep Research stockt. | | `proxy` | Der Caddy-Edge — TLS-Terminierung, HTTP-Routing. | Gesamter Tale-Cloud-Verkehr betroffen. | -| `db` | TimescaleDB — dauerhafter Zustand für die Convex-Schicht und Plattform-Metadaten. | Schreiben abgelehnt; die platform-Zeile wird ebenfalls rot. | +| `db` | Postgres — dauerhafter Anwendungszustand und die Job-Warteschlange. | Schreiben abgelehnt; die platform-Zeile wird ebenfalls rot. | Jede Zeile trägt die letzten 90 Tage Uptime als Sparkline. Ein Incident liest sich als farbiges Band auf der Zeile; ein Klick aufs Band öffnet den Verlauf — erstes Update, Folge-Updates, Auflösung, Post-Mortem, wenn eines ansteht. @@ -37,7 +37,9 @@ Die Seite gehört der Bereitschafts-Rotation. Updates werden vom Engineer gescho ## Self-hosted: was sich ändert -Selbst gehostete Instanzen erscheinen nicht auf `status.tale.dev` — die Seite deckt Tale Cloud ab. Jedes Deployment bringt stattdessen seine eigene Status-Page mit, von der Plattform ausgeliefert und ohne Anmeldung erreichbar unter `https:///status`. Sie rendert serverseitig eine Gesundheits-Zusammenfassung — operational, degraded oder outage — aus einem Liveness-Probe gegen das Convex-Backend, sodass ein Betreiber (oder ein Endnutzer, der prüft, ob es nur bei ihm hakt) die Verfügbarkeit ohne Login lesen kann. Die maschinenlesbare Form ist `https:///status.json`, die dasselbe Ergebnis als JSON zurückgibt, das ein Uptime-Monitor pollen kann. +Selbst gehostete Instanzen erscheinen nicht auf `status.tale.dev` — die Seite deckt Tale Cloud ab. Jedes Deployment bringt stattdessen seine eigene Status-Page mit, von der Plattform ausgeliefert und ohne Anmeldung erreichbar unter `https:///status`. Sie rendert serverseitig eine Gesundheits-Zusammenfassung — operational, degraded oder outage — aus einem Liveness-Probe gegen die `/ping`-Route der Backend-Schicht, dieselbe Route, die auch der Healthcheck des `backend-api`-Containers nutzt. Ein Betreiber (oder ein Endnutzer, der prüft, ob es nur bei ihm hakt) liest die Verfügbarkeit damit ohne Login. Die maschinenlesbare Form ist `https:///status.json`, die dasselbe Ergebnis als JSON zurückgibt, das ein Uptime-Monitor pollen kann. + +Der Probe meldet genau eine Komponente, `backend`, denn diese Schicht bedient jede Anfrage der App: Antwortet sie, fließen Daten. Die Liveness des Plattform-Containers steckt implizit drin — sonst hätte die Status-Page nicht gerendert. Ergebnisse sind fünf Sekunden gecacht und jeder Probe läuft nach zwei Sekunden ab, ein Uptime-Monitor auf `/status.json` kostet das Backend also selbst im kurzen Intervall fast nichts. Diese Seite meldet die Verfügbarkeit des Deployments selbst. Für tieferes Betriebssignal — Container-Gesundheit von `tale status`, Anfrage-Metriken aus den Caddy-Logs und Control-Plane-Events im In-Product-Audit-Log — bildet die [Observability-Troubleshooting-Seite](/de/self-hosted/operate/observability/troubleshooting) Symptome auf Logs ab. diff --git a/docs/de/develop/webdav-api.md b/docs/de/develop/webdav-api.md index 51e9840005..d21a5b1eb1 100644 --- a/docs/de/develop/webdav-api.md +++ b/docs/de/develop/webdav-api.md @@ -39,11 +39,11 @@ Jede authentifizierte Anfrage prüft zusätzlich, dass der anfragende Benutzer a | PROPFIND | Eine Ressource auflisten (Depth 0) oder die direkten Kinder einer Sammlung (Depth 1). Die emittierte Eigenschaftsliste ist unten dokumentiert. **Depth: infinity wird mit 403 abgelehnt**, um unbegrenzte Antworten zu verhindern. | Erforderlich | | PROPPATCH | Gibt 207-Erfolg pro Eigenschaft zurück, ohne Werte zu speichern. Dead Properties werden in v1 nicht persistiert; PROPPATCH gelingt optimistisch zur Client-Kompatibilität. | Erforderlich | | GET / HEAD | Den Dokument-Blob streamen. Setzt `Content-Type`, `Content-Length`, `ETag` und `Last-Modified`. GET auf eine Sammlung gibt 405 zurück. | Erforderlich | -| PUT | Ein Dokument erstellen oder ersetzen. Neuer Blob im Convex-Speicher mit Content-Hash-Dedup; die Dokument-Zeile erhält `sourceProvider: "webdav"`. Gibt 201 beim Erstellen, 204 beim Überschreiben zurück. | Erforderlich | +| PUT | Ein Dokument erstellen oder ersetzen. Der Body streamt unter einem frischen Key in den Objektspeicher der Organisation; die Dokument-Zeile erhält `sourceProvider: "webdav"`. Gibt 201 beim Erstellen, 204 beim Überschreiben zurück. Eine Anfrage ohne `Content-Length` (Chunked Transfer Encoding) wird abgelehnt — die präsignierte URL braucht die Länge vorab. | Erforderlich | | DELETE | Ein Dokument soft-löschen (`lifecycleStatus: "trashed"`) oder einen Ordner (kaskadiert Trash auf enthaltene Dokumente, hard-löscht die Ordner-Zeilen). Gibt 204 zurück. | Erforderlich | | MKCOL | Einen Ordner unter einem bestehenden Eltern erstellen. Nur leerer Body. Gibt 201 zurück, 405 wenn das Ziel existiert oder 409 wenn der Eltern fehlt. | Erforderlich | | MOVE | Umbenennen oder verschieben. Atomar für Dokumente. Für Ordner wird die `parentId` des verschobenen Ordners aktualisiert. Beachtet `Overwrite: T/F` und `If`. Gibt 201 (neues Ziel) oder 204 (Überschreiben) zurück. | Erforderlich | -| COPY | Serverseitige Kopie. Dokumentkopien wiederverwenden die Convex-Storage-ID (Dedup). Ordnerkopien rekursiv. Beachtet `Overwrite` und `If`. | Erforderlich | +| COPY | Serverseitige Kopie. Eine Dokumentkopie ist eine zweite Zeile auf dasselbe gespeicherte Objekt — keine Bytes wandern, und das Objekt bleibt, bis die letzte Zeile darauf verschwindet. Ordnerkopien rekursiv. Beachtet `Overwrite` und `If`. | Erforderlich | | LOCK | Class-2-exklusive oder geteilte Schreibsperre. Timeout aus `Timeout: Second-N`-Header, gedeckelt auf 3600. Refresh durch erneutes LOCK mit `If: ()` und leerem Body. | Erforderlich | | UNLOCK | Eine Sperre per Token freigeben. Nur der Sperr-Besitzer kann freigeben. Gibt 204 zurück. | Erforderlich | @@ -67,7 +67,7 @@ Dead Properties werden nicht gespeichert. PROPPATCH gibt für eine allein gesetz ## Sperrsemantik -Sperren leben in ihrer eigenen Convex-Tabelle, gekeyt mit `(organizationId, resourcePath)`. Wire-Form ist `opaquelocktoken:`. Der Server: +Sperren leben in ihrer eigenen Postgres-Tabelle, indiziert über `(organizationId, resourcePath)`. Wire-Form ist `opaquelocktoken:`. Der Server: - Deckelt Timeout auf 3600 Sekunden. Anfragen für längere Fenster werden still gekappt. - Behandelt `LOCK` mit `If: ()`-Header und leerem Body als Refresh — der Ablauf der bestehenden Sperre wird verlängert. @@ -111,16 +111,16 @@ Der Server bewirbt `DAV: 1, 2` in der OPTIONS-Antwort. - `Depth: infinity` auf PROPFIND wird mit `403` abgelehnt. - `Timeout: Second-N` auf LOCK wird auf `[1, 3600]` begrenzt. -- Die PUT-Body-Größe ist standardmäßig auf **5 GB** begrenzt (`413` bei Überschreitung), erzwungen sowohl am Reverse-Proxy als auch im Plattform-Server. Betreiber können das Limit über die Umgebungsvariable `WEBDAV_MAX_PUT_BYTES` anpassen. Der Body wird an eine Convex-Presigned-URL gestreamt, ohne dass ein großer Upload im Plattform-Speicher gepuffert wird. +- Die PUT-Body-Größe ist standardmäßig auf **5 GB** begrenzt (`413` bei Überschreitung), erzwungen sowohl am Reverse-Proxy als auch im Backend. Betreiber passen das Limit über die Umgebungsvariable `WEBDAV_MAX_PUT_BYTES` an — setz sie auch auf dem Proxy-Container, sonst bleibt der Proxy die bindende Grenze. Der Body streamt an eine präsignierte Objektspeicher-URL, ohne dass ein großer Upload im Speicher des Backends gepuffert wird. - XML-Request-Bodys (PROPFIND / PROPPATCH / MKCOL / LOCK) sind auf **64 KB** begrenzt (`413` bei Überschreitung) — diese Envelopes sind per Design winzig. - App-Passwörter werden mit HMAC-SHA256 gehasht; das Geheimnis taucht nach dem Create-Call in keiner Antwort mehr auf. - `lastUsedAt` wird höchstens einmal pro Minute pro App-Passwort gepatcht, um Write-Storms auf belebten Mounts zu vermeiden. ## Netzwerk-Voraussetzungen -Der WebDAV-Endpunkt läuft im Plattform-Hono-Server (`platform:3000` in Compose). Caddy routet `/dav/*` über den Default-Fallback dorthin — keine Extra-Konfiguration erforderlich. Der Pfad erfordert, dass der Plattform-Server `ADMIN_KEY` in seiner Umgebung gesetzt hat, damit er interne Convex-Abfragen mit Admin-Auth aufrufen kann. +Den WebDAV-Endpunkt bedient die Backend-Schicht (`backend-api:3005` in Compose). Caddy hat dafür einen eigenen `handle /dav/*`-Block, der dorthin weiterleitet und das Body-Limit anwendet — keine Extra-Konfiguration erforderlich. Der Endpunkt braucht kein Deployment-Credential: Jede Anfrage authentifiziert sich mit ihrem eigenen App-Passwort, und die Handler lesen und schreiben Postgres im selben Prozess. -Für Dev (`bun dev`) wird derselbe Dispatch als Vite-Middleware gemountet (`vite-plugins/serve-webdav.ts`) — `curl` und Clients können `http://localhost:3000/dav//...` gegen einen laufenden Dev-Server ohne Rebuild treffen. +Für Dev (`bun run dev`) proxyt Vite `/dav` an dasselbe Backend, `curl` und gemountete Clients treffen also `http://localhost:3000/dav//...` gegen einen laufenden Dev-Server. ## Sicherheit diff --git a/docs/de/self-hosted/configuration/data-residency.md b/docs/de/self-hosted/configuration/data-residency.md index 4bcd81b288..cfdb29c7f2 100644 --- a/docs/de/self-hosted/configuration/data-residency.md +++ b/docs/de/self-hosted/configuration/data-residency.md @@ -3,29 +3,35 @@ title: Datenresidenz description: Richte die Wissensdatenbank, die Anwendungsdatenbank und den Speicher für hochgeladene Dateien einer selbst gehosteten Tale-Installation auf Infrastruktur aus, die du selbst kontrollierst — von Administratoren unter Einstellungen > Datenresidenz konfiguriert und beim Neustart angewendet. --- -Eine selbst gehostete Tale-Installation läuft auf Infrastruktur, die du ohnehin schon kontrollierst, also liegen ihre Daten standardmäßig auf deinen Hosts. **Datenresidenz** ist für den Fall gedacht, dass du einzelne Datenspeicher auf dein eigenes verwaltetes Postgres oder deinen Objektspeicher ausrichten willst statt auf die mitgelieferten Container — etwa um Dokumenttext in einer Datenbank zu halten, die dein Team betreibt, oder hochgeladene Dateien in deinem eigenen S3-Bucket. Der Wissens-Korpus läuft genau deshalb als eigener Container (`knowledge-db`), damit er sich unabhängig von der operativen Datenbank verlagern oder ersetzen lässt — er ist der Speicher, um den sich die meisten Residenz-Anforderungen drehen. Administratoren konfigurieren das unter **Einstellungen > Datenresidenz**; die Änderung wird in eine einzige Konfigurationsdatei auf Deployment-Ebene geschrieben und **greift, sobald die betroffenen Container neu starten**. +Eine selbst gehostete Tale-Installation läuft auf Infrastruktur, die du ohnehin schon kontrollierst, also liegen ihre Daten standardmäßig auf deinen Hosts. **Datenresidenz** ist für den Fall gedacht, dass du einzelne Datenspeicher auf dein eigenes verwaltetes Postgres oder deinen Objektspeicher ausrichten willst statt auf die mitgelieferten Container — etwa um Dokumenttext in einer Datenbank zu halten, die dein Team betreibt, oder hochgeladene Dateien in deinem eigenen S3-Bucket. Der Wissens-Korpus ist genau deshalb eine eigene Datenbank mit eigenem Connection-String, damit er sich unabhängig von der operativen Datenbank verlagern oder ersetzen lässt — er ist der Speicher, um den sich die meisten Residenz-Anforderungen drehen. -Diese Seite behandelt, was sich verlagern lässt, die eine Voraussetzung, die zubeißt (ParadeDB), wie die Konfiguration abgelegt und angewendet wird, und wie du sicher neu startest. +Dahinter stehen zwei Mechanismen. Einen **deployment-weiten** Speicher lenkst du auf dem Host um, in `.env` und im Config-Baum, und die Änderung greift, sobald die Backend-Container neu starten. Einen **organisationseigenen** Speicher konfiguriert ein Owner oder Admin der Organisation unter **Einstellungen > Datenresidenz**; er landet im Config-Verzeichnis dieser Organisation und greift bei der nächsten Anfrage. Diese Seite behandelt beides, die eine Voraussetzung, die zubeißt (ParadeDB), wie die Konfiguration abgelegt wird, und wie du sicher neu startest. ## Bearbeitung aktivieren -**Einstellungen > Datenresidenz** ist eine einzige Seite mit zwei Arten von Abschnitten: den deployment-weiten Speichern, die sich alle Organisationen teilen, und den Speichern, die eine einzelne Organisation selbst mitbringt. Jeder Abschnitt erscheint lesend oder bearbeitbar, je nachdem, was die lesende Person ändern darf, und die Seite benennt den Zustand. Ansehen darf jeder Owner oder Admin einer Organisation; die **deployment-weiten Speicher bearbeiten** — einen Datenspeicher umlenken, Secrets speichern, einen Verbindungstest laufen lassen oder einen Neustart auslösen — darf nur eine benannte Allowlist von Operatoren. Trage deren Anmelde-E-Mails (kommagetrennt) in `.env` ein und starte neu: +**Einstellungen > Datenresidenz** ist eine einzige Seite mit zwei Arten von Abschnitten: den deployment-weiten Speichern, die sich alle Organisationen teilen, und den Speichern, die eine einzelne Organisation selbst mitbringt. Jeder Abschnitt erscheint lesend oder bearbeitbar, je nachdem, was die lesende Person ändern darf, und die Seite benennt den Zustand. Ansehen darf jeder Owner oder Admin einer Organisation; die **deployment-weiten Speicher bearbeiten** — einen Datenspeicher umlenken, Secrets speichern, einen Verbindungstest laufen lassen — darf nur eine benannte Allowlist von Operatoren. Trage deren Anmelde-E-Mails (kommagetrennt) in `.env` ein und starte neu: ```bash TALE_DEPLOYMENT_CONFIG_ADMINS=alice@example.com,bob@example.com ``` -Ist die Allowlist leer oder nicht gesetzt, zeigen die Deployment-Abschnitte Administratoren die aktuelle Konfiguration weiterhin an, aber nur lesend — die Kopfzeilen-Aktionen **Deployment speichern** und **Anwenden & neu starten** erscheinen nur für Operatoren auf der Allowlist. Nur ein angemeldeter Admin, dessen E-Mail auf der Liste steht, bekommt diese Abschnitte bearbeitbar; die Seite nennt dir, welche E-Mail einzutragen ist. Die Entrypoints lesen die Konfigurationsdatei unabhängig von der Allowlist, also kann ein Operator, der die Datei lieber direkt auf der Platte bearbeitet, das tun, ohne UI-Bearbeiter zu benennen. +Ist die Allowlist leer oder nicht gesetzt, zeigen die Deployment-Abschnitte Administratoren die aktuelle Konfiguration weiterhin an, aber nur lesend — die Kopfzeilen-Aktion **Deployment speichern** erscheint nur für Operatoren auf der Allowlist. Nur ein angemeldeter Admin, dessen E-Mail auf der Liste steht, bekommt diese Abschnitte bearbeitbar; die Seite nennt dir, welche E-Mail einzutragen ist. Es gibt keine Neustart-Schaltfläche: Ein Speichern zeigt die zwei Befehle, die die Änderung anwenden, und der Abschnitt weiter unten wiederholt sie. Ein Operator, der lieber auf dem Host arbeitet, überspringt die Allowlist ganz und bearbeitet `.env` und die Config-Dateien direkt. ## Was du verlagern kannst Drei Speicher, jeder unabhängig und optional. Eine fehlende Einstellung bedeutet „nimm den mitgelieferten Default" — eine frische Installation ohne Konfiguration bleibt also unverändert. -- **Wissensdatenbank** — der Wissens-Korpus: Dokumentmetadaten, der extrahierte Chunk-Text, Embeddings, der BM25-Index, der semantische Cache und die gecrawlten Webseiten. Sie kommt als mitgelieferter `knowledge-db`-Container (`tale_knowledge`, mit den Schemata `private_knowledge` und `public_web`) und ist der Speicher, um den sich die meisten Residenz-Anforderungen drehen, weil er deinen Dokumentinhalt hält. Richte ihn auf dein eigenes verwaltetes Postgres aus, um den Korpus auf Infrastruktur zu halten, die dein Team betreibt. -- **Dateispeicher** — wo hochgeladene Dateien (die ursprünglichen Blobs) liegen. Standardmäßig liegen sie im mitgelieferten Objektspeicher des Stacks (Dienst `object-store`, auf einem eigenen Volume); du kannst sie auf einen externen S3-kompatiblen Bucket ausrichten. -- **Anwendungsdatenbank** (erweitert) — die operative Convex-Datenbank (der mitgelieferte `db`-Container). Das Convex-Backend leitet den Namen dieser Datenbank aus `INSTANCE_NAME` (`tale_platform`) ab und verbindet sich nur über Host:Port, daher muss das externe Postgres eine Datenbank mit genau dem Namen `tale_platform` enthalten. Ihr TLS-Modus wird vom Convex-Treiber vorgegeben und ist nicht konfigurierbar. + -> Hinweis: Die Wissensdatenbank und die Anwendungsdatenbank sind zwei separate Postgres-Instanzen — die eine zu verschieben rührt die andere nicht an. Die Wissensdatenbank zu verlagern verschiebt den extrahierten Text und die Embeddings; die ursprünglich hochgeladenen Dateien wandern erst mit, wenn du auch den **Dateispeicher** auf S3 ausrichtest. +**Die deployment-weiten Abschnitte zu speichern lenkt keinen Speicher um.** Das Backend öffnet die Anwendungsdatenbank aus `DATABASE_URL`, den Wissens-Korpus aus `KNOWLEDGE_DATABASE_URL` und den Blob-Store aus der `object-storage/connection.json` im `default`-Config-Baum. Nichts liest beim Boot den `dataStores`-Block, den diese Abschnitte in `deployment.yml` schreiben. Verlagere einen deployment-weiten Speicher über die Umgebungsvariable oder die Datei, die unten bei ihm steht, und lies die Deployment-Abschnitte als Notiz der beabsichtigten Topologie, nicht als den Schalter, der sie anwendet. Die **organisationseigenen** Abschnitte weiter unten sind ein anderer Mechanismus und greifen tatsächlich. + + + +- **Wissensdatenbank** — der Wissens-Korpus: Dokumentmetadaten, der extrahierte Chunk-Text, Embeddings, der BM25-Index, der semantische Cache und die gecrawlten Webseiten. Er kommt als `tale_knowledge`-Datenbank mit den Schemata `private_knowledge` und `public_web`, erreichbar unter dem Host `knowledge-db`, und ist der Speicher, um den sich die meisten Residenz-Anforderungen drehen, weil er deinen Dokumentinhalt hält. Richte ihn mit `KNOWLEDGE_DATABASE_URL` in `.env` auf dein eigenes verwaltetes Postgres aus, um den Korpus auf Infrastruktur zu halten, die dein Team betreibt. +- **Dateispeicher** — wo hochgeladene Dateien (die ursprünglichen Blobs) liegen. Standardmäßig liegen sie im mitgelieferten Objektspeicher des Stacks (Dienst `object-store`, auf einem eigenen Volume). Richte sie auf einen externen S3-kompatiblen Bucket aus, indem du `$TALE_CONFIG_DIR/default/object-storage/connection.json` und das Sidecar `connection.secrets.json` bearbeitest; das Backend seedet diese Datei beim ersten Boot gegen den mitgelieferten Store und überschreibt eine vorhandene nie. +- **Anwendungsdatenbank** (erweitert) — der operative Speicher: Chats, Aufgaben, Automation-Runs, das Audit-Log, die Job-Warteschlange. Sie kommt als `tale_app`-Datenbank auf dem mitgelieferten `db`-Container, und das Backend erreicht sie über einen Connection-String, `DATABASE_URL`. Richte den auf dein eigenes verwaltetes Postgres aus, um sie zu verlagern; das Backend legt seine Schema-Migrationen beim Boot auf das an, was es dort findet, innerhalb eines Advisory Locks. + +> Hinweis: Die Wissensdatenbank und die Anwendungsdatenbank sind zwei separate Datenbanken — die eine zu verschieben rührt die andere nicht an. Auf einem Single-Host-`tale deploy`-Stack teilen sie sich einen Postgres-Container, eine Residenz-Anforderung, die sie trennt, ist also ein Grund, mindestens eine zu verlagern. Die Wissensdatenbank zu verlagern verschiebt den extrahierten Text und die Embeddings; die ursprünglich hochgeladenen Dateien wandern erst mit, wenn du auch den **Dateispeicher** verlagerst. ## Die ParadeDB-Voraussetzung @@ -64,7 +70,7 @@ Die Verbindung liegt neben der Wissens-Verbindung im Konfigurationsverzeichnis d - `$TALE_CONFIG_DIR//object-storage/connection.json` — Region, optionaler Endpoint (für MinIO/R2), Path-Style-Flag, Bucket und ein optionales Key-Präfix. - `$TALE_CONFIG_DIR//object-storage/connection.secrets.json` — das Schlüsselpaar, SOPS-verschlüsselt, sobald ein SOPS-Age-Schlüssel konfiguriert ist (siehe [Secrets mit SOPS](/de/self-hosted/configuration/secrets-with-sops)). -Anders als der deployment-weite S3-Schalter oben ist dieser Weg **nicht** nur für Neuinstallationen: Sobald die Konfiguration existiert, landen neue Uploads im Bucket der Org, während zuvor gespeicherte Dateien lesbar bleiben, wo sie sind — gemischte Referenzen werden unterstützt, du kannst also jederzeit umschalten. Früher gespeicherte Dateien bleiben im Convex-Speicher, bis du sie mit dem Blob-Backfill unten verlagerst. Entfernst du die Konfiguration, landen neue Uploads wieder im Deployment-Default; bereits in den Bucket geschriebene Dateien bleiben dort, Tale kann sie aber erst wieder lesen, wenn die Verbindung erneut eingerichtet ist. Ein Neustart ist in keine Richtung nötig. +Dieser Weg ist **nicht** nur für Neuinstallationen: Sobald die Konfiguration existiert, landen neue Uploads im Bucket der Org, während zuvor gespeicherte Dateien im Deployment-Default-Store lesbar bleiben — du kannst also jederzeit umschalten und die älteren Dateien danach mit dem Blob-Backfill unten verlagern. Entfernst du die Konfiguration, landen neue Uploads wieder im Deployment-Default; bereits in den Bucket geschriebene Dateien bleiben dort, Tale kann sie aber erst wieder lesen, wenn die Verbindung erneut eingerichtet ist. Ein Neustart ist in keine Richtung nötig: Der Resolver cacht eine Verbindung fünfzehn Sekunden, eine Änderung ist also praktisch sofort live. Org-Admins verwalten auch diese Verbindung in denselben Organisations-Abschnitten von **Einstellungen > Datenresidenz**; der dortige Verbindungstest führt einen echten Hochladen-Lesen-Löschen-Durchlauf gegen den Bucket aus, bevor du dich festlegst. Wie bei der Wissens-Verbindung bleiben die JSON-Dateien die Quelle der Wahrheit. @@ -72,39 +78,25 @@ Org-Admins verwalten auch diese Verbindung in denselben Organisations-Abschnitte ### Vorhandene Dateien in den Bucket verschieben -Den Bucket zu verbinden leitet nur **neue** Uploads um; die Blobs, die vor der Verbindung geschrieben wurden, bleiben in Convex' `_storage` und funktionieren weiter über die gemischten Referenzen oben. Um auch diese Historie auf deine eigene Infrastruktur zu holen — der eigentliche Sinn der Datenresidenz — führe den **Blob-Backfill** aus: Er kopiert jeden vorhandenen Blob in den Bucket der Org, prüft, dass er Byte für Byte identisch zurückkommt, schreibt jede referenzierende Zeile um und löscht die Convex-Kopie. - -Ein Org-Admin startet ihn in der UI: Ist die Bucket-Verbindung gespeichert, zeigt der Objektspeicher-Abschnitt von **Einstellungen > Datenresidenz** die Schaltfläche **Bestehende Dateien verschieben** — bestätige, und der Umzug läuft im Hintergrund, während Uploads weiter funktionieren; eine Statuszeile im selben Abschnitt meldet Fortschritt und Ausgang des letzten Laufs. - -Ein Operator mit Convex-CLI-Zugriff kann dieselbe Engine stattdessen aus einer Shell starten und die ID der Organisation übergeben. Mach zuerst einen Probelauf, um zu sehen, was verschoben würde, dann den echten Lauf: - -```bash -# Probelauf — zählt und sampelt, was verschoben würde, schreibt nichts: -bunx convex run object_storage/backfill_actions:migrateOrgBlobsToObjectStorage '{"organizationId":"","dryRun":true}' - -# Der echte Lauf — lass dryRun weg, sobald die Zahlen stimmen: -bunx convex run object_storage/backfill_actions:migrateOrgBlobsToObjectStorage '{"organizationId":""}' -``` - -Der Backfill ist **idempotent** und **org-gebunden**: Er verschiebt nur die Blobs dieser Organisation, überspringt alles, was schon im Bucket liegt, und lässt jede Convex-Quelle stehen, bis ihre Kopie verifiziert ist — ein erneuter Lauf nach einer Unterbrechung setzt also sicher fort. Ein echter Lauf braucht die zuvor konfigurierte Bucket-Verbindung; ein Probelauf nicht. Das ist bewusst **keine** versionierte Framework-Migration — er läuft auf Abruf, pro Organisation, wenn du die Historie eines Mandanten verlagern willst, nicht an einer Release-Grenze. +Den Bucket zu verbinden leitet nur **neue** Uploads um; die Blobs, die vor der Verbindung geschrieben wurden, bleiben im Deployment-Default-Store und funktionieren weiter, denn eine gespeicherte Referenz benennt den Objekt-Key, und der Resolver entscheidet, aus welchem Store er ihn liest. Um auch diese Historie auf deine eigene Infrastruktur zu holen — der eigentliche Sinn der Datenresidenz — führe den **Blob-Backfill** aus: Er geht die Dokumente der Organisation durch (die aktuellen Dateien und jede Version in ihrer Historie) sowie deren Datei-Metadaten und kopiert jedes Objekt unter demselben Key aus dem Deployment-Default-Store in den Bucket der Org. -## Dateispeicher auf S3 +Ein Org-Admin startet ihn in der UI: Ist die Bucket-Verbindung gespeichert, zeigt der Objektspeicher-Abschnitt von **Einstellungen > Datenresidenz** die Schaltfläche **Bestehende Dateien verschieben** — bestätige, und der Umzug läuft als Hintergrund-Job, während Uploads weiter funktionieren; eine Statuszeile im selben Abschnitt meldet Fortschritt und Ausgang des letzten Laufs. -Externer Dateispeicher ist alles-oder-nichts über die Speicher-Use-Cases von Convex hinweg, also gibst du **fünf Buckets** an — files, exports, snapshot-imports, modules und search — plus Region und Anmeldedaten. Für S3-kompatible Dienste (MinIO, Cloudflare R2) setzt du den Endpunkt und aktivierst die Path-Style-Adressierung. +Zwei Eigenschaften machen einen erneuten Lauf sicher. Keys ändern sich nie, es wird also keine Zeile umgeschrieben und keine Referenz kann mitten im Lauf schal werden: Ein Objekt kippt in dem Moment vom Lesen aus dem Default-Store auf das Lesen aus dem Bucket, in dem seine Kopie landet. Und jedes Objekt, das im Bucket schon liegt, wird übersprungen, ein unterbrochener Lauf setzt also fort statt neu zu kopieren. Der Lauf ist org-gebunden und braucht die zuvor gespeicherte Bucket-Verbindung. -> **Nur Greenfield.** Den Dateispeicher von lokal auf S3 umzustellen migriert die bereits auf dem lokalen Volume liegenden Blobs **nicht** — Convex sucht sie im Bucket und findet sie nicht. Setze S3 bei der ersten Installation, oder kopiere den vorhandenen lokalen Speicher vorab in den Bucket, bevor du umstellst. +Was er nicht tut, ist löschen. Das Quell-Objekt bleibt im Deployment-Default-Store, ein Backfill verlagert also eine Kopie statt die Bytes zu verschieben — plane einen separaten Aufräum-Durchlauf, wenn die Residenz-Anforderung ist, dass die alte Kopie zu existieren aufhört. Das ist bewusst **keine** versionierte Framework-Migration: Er läuft auf Abruf, pro Organisation, wenn du die Historie eines Mandanten verlagern willst, nicht an einer Release-Grenze. ## Wie die Konfiguration abgelegt wird -Speichern schreibt zwei Dateien im Konfigurations-Root (nicht unter einem Org-Verzeichnis): +Die Deployment-Abschnitte zu speichern schreibt zwei Dateien im Konfigurations-Root (nicht unter einem Org-Verzeichnis): -- `deployment.json` — die nicht geheime Konfiguration (Hosts, Ports, Buckets, Modi). +- `deployment.yml` — die nicht geheime Konfiguration (Hosts, Ports, Buckets, Modi). Ein Deployment, das noch die abgelöste `deployment.json` trägt, wird gelesen wie sie ist und beim nächsten Speichern konvertiert. - `deployment.secrets.json` — die Datenbank-Passwörter und S3-Schlüssel, SOPS-verschlüsselt (siehe [Secrets mit SOPS](/de/self-hosted/configuration/secrets-with-sops)). -Beim Boot liest der `convex`-Entrypoint diese und leitet seine Verbindungen ab, bevor er startet. Wissens-Ingestion und Retrieval laufen im Convex-Backend, also ist es der einzige Container, der die Verbindung zur Wissensdatenbank öffnet — es gibt keinen separaten Retrieval-Dienst zu konfigurieren. Der Vertrag ist **fail-closed**: ein vorhandenes, aber unparsbares `deployment.json`, ein nicht entschlüsselbares Secret oder eine Konfiguration ohne Pflichtfelder **bricht den Start ab**, statt still auf die mitgelieferte Datenbank zurückzufallen — regulierte Daten fehlzuleiten ist schlimmer, als nicht zu starten. Eine fehlende Datei ist der normale Default-Pfad. +Die organisationseigenen Abschnitte schreiben stattdessen ins Verzeichnis der Organisation, unter die oben aufgeführten Pfade. Das sind die Dateien, aus denen das Backend tatsächlich eine Verbindung auflöst, und der Lesevorgang ist **fail-closed**: Eine Org-Konfiguration, die vorhanden aber unparsbar ist oder deren Secret nicht entschlüsselt, verweigert die Lesezugriffe dieser Organisation, statt still auf den mitgelieferten Speicher zurückzufallen — regulierte Daten fehlzuleiten ist schlimmer, als laut zu scheitern. Eine fehlende Datei ist der normale Default-Pfad. ## Eine Änderung anwenden: Neustart -Die Konfiguration wird beim Boot gelesen, also greift ein Speichern erst, wenn die Backend-Container (`backend-api` und `backend-worker`) neu starten. Führe `docker compose restart backend-api backend-worker` aus, oder `tale deploy` für einen Zero-Downtime-Blue-Green-Roll — die Einstellungsseite zeigt nach dem Speichern dieselben Befehle an. +Eine deployment-weite Verbindung wird beim Boot gelesen, also greift eine Änderung an `.env` oder am `default`-Config-Baum erst, wenn die Backend-Container (`backend-api` und `backend-worker`) neu starten. Führe `docker compose restart backend-api backend-worker` aus, oder `tale deploy` für einen Zero-Downtime-Blue-Green-Roll — die Einstellungsseite zeigt nach dem Speichern dieselben Befehle an. Eine organisationseigene Verbindung braucht keinen Neustart. Die relevante Umgebungsvariable ist `TALE_DEPLOYMENT_CONFIG_ADMINS` (die kommagetrennte E-Mail-Allowlist der bearbeitungsberechtigten Operatoren). Setze sie in `.env`. Siehe auch [Umgebungsvariablen-Referenz](/de/self-hosted/configuration/environment-reference) und [Secrets mit SOPS](/de/self-hosted/configuration/secrets-with-sops). diff --git a/docs/de/self-hosted/configuration/environment-reference.md b/docs/de/self-hosted/configuration/environment-reference.md index 5bc31cf1ec..dcf6981651 100644 --- a/docs/de/self-hosted/configuration/environment-reference.md +++ b/docs/de/self-hosted/configuration/environment-reference.md @@ -9,7 +9,7 @@ i18nLintExclude: Tale liest seine Konfiguration aus einer einzigen `.env`-Datei im Repo-Stammverzeichnis. Etwa ein Dutzend Variablen sind beim ersten Boot Pflicht; der Rest stimmt das Verhalten ab. Diese Seite listet jede Variable, die [`.env.example`](https://github.com/tale-project/tale/blob/main/.env.example) mitbringt, was sie als Default hat und welche Oberfläche im Produkt sie konsumiert. -Gruppen sind danach geordnet, wann du sie zuerst brauchst: Domain-Identität, TLS, Secrets, Datenbank, Instanz, Observability, Provider-Verschlüsselung. Ändert sich der Wert einer Variable, starte den Plattform-Container neu (`docker compose restart tale-platform tale-convex`), damit sie wirkt. +Gruppen sind danach geordnet, wann du sie zuerst brauchst: Domain-Identität, TLS, Secrets, Datenbank, Instanz, Observability, Provider-Verschlüsselung. Ändert sich der Wert einer Variable, starte die Container neu, die sie lesen. Die meisten liest das Backend, `docker compose restart backend-api backend-worker` ist also der übliche Befehl; die wenigen, die die Web-Schicht liest, brauchen zusätzlich `platform`, und `tale deploy` rollt alles. ## Wie du diese Seite liest @@ -42,22 +42,23 @@ Die `SITE_URL` muss exakt mit dem übereinstimmen, was der Benutzer im Browser e | ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `BETTER_AUTH_SECRET` | Beispielwert in der Datei | **Pflicht.** Base64-Secret für den Better-Auth-Session-Signer. Generier mit `openssl rand -base64 32`. Rotieren invalidiert jede Session. | | `ENCRYPTION_SECRET_HEX` | Beispielwert in der Datei | **Pflicht.** 32-Byte-Hex-Schlüssel. AES-256-Schlüssel für OAuth- und Connector-Credentials und HKDF-Input für die Guardrails-Secret-Box. Generier mit `openssl rand -hex 32`. Rotieren invalidiert jeden DB-Ciphertext; Operator müssen betroffene Secrets neu eingeben. | -| `INSTANCE_SECRET` | Beispielwert in der Datei | **Pflicht.** Wird genutzt, um den Convex-Admin-Schlüssel für `tale deploy` abzuleiten. Deploy schlägt fehl, wenn unset. | +| `INSTANCE_SECRET` | Beispielwert in der Datei | **Pflicht.** 64-stelliger Hex-String. Leitet den HMAC-Schlüssel der WebDAV-App-Passwörter und das Sandbox-Stage-Token ab, wenn diese nicht explizit gesetzt sind. Deploy schlägt fehl, wenn unset oder fehlerhaft; ihn zu rotieren invalidiert jedes ausgegebene WebDAV-App-Passwort. | Ersetze die Werte, die in `.env.example` mitkommen, bevor du die Instanz exponierst — sie sind absichtlich unsichere Platzhalter. ## Datenbank -Tale betreibt zwei Postgres-Datenbanken: den operativen Speicher (`db`, Port 5432) hinter dem Convex-Backend und den Wissens-Korpus (`knowledge-db`, Port 5433), der Dokument-Chunks, Embeddings und gecrawlte Seiten hält. Beide sind ParadeDB und teilen sich `DB_PASSWORD`, aber sie sind unabhängig — zeig jede für sich auf externe Infrastruktur. +Tale betreibt zwei Postgres-Datenbanken: den operativen Speicher (`tale_app` auf `db`, Port 5432), den das Backend für Anwendungs-State, Sessions und die Job-Warteschlange nutzt, und den Wissens-Korpus (`tale_knowledge`, erreichbar unter dem Host `knowledge-db`), der Dokument-Chunks, Embeddings und gecrawlte Seiten hält. Beide sind ParadeDB und teilen sich `DB_PASSWORD`, aber es sind unabhängige Datenbanken — zeig jede für sich auf externe Infrastruktur. Auf einem Single-Host-`tale deploy`-Stack liegen sie im selben Postgres-Container, der den Netzwerk-Alias `knowledge-db` trägt. | Name | Default | Beschreibung | | ------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DB_PASSWORD` | `tale_password_change_me` | **Pflicht.** Passwort für den selbst gehosteten Postgres-Benutzer. Vor der Produktion ändern. Von beiden Datenbank-Containern genutzt. | -| `POSTGRES_URL` | aus `DB_PASSWORD` konstruiert | **Optional.** Überschreibt die automatisch konstruierte URL der operativen Datenbank. Nutze das, wenn du auf einen externen Postgres oder einen Nicht-Standard-Host/Port zeigst. | -| `KNOWLEDGE_DATABASE_URL` | `postgresql://tale:${DB_PASSWORD}@knowledge-db:5432/tale_knowledge` | **Optional.** Verbindungs-URL, die das Convex-Backend für den Wissens-Korpus nutzt. Überschreib sie, um den Korpus auf dein eigenes verwaltetes ParadeDB zu verlagern — der datenresidenz-sensible Speicher wandert unabhängig. | +| `DATABASE_URL` | von Compose aus `DB_PASSWORD` und `APP_DB_NAME` gesetzt | **Pflicht** für das Backend, und von Compose und `tale deploy` für dich gesetzt. Die vollständige Verbindungs-URL der Anwendungsdatenbank, Datenbankname inklusive. Überschreib sie, um das Backend auf ein externes Postgres zu zeigen. | +| `APP_DB_NAME` | `tale_app` | **Optional.** Name der Anwendungsdatenbank auf dem mitgelieferten `db`-Container. | +| `KNOWLEDGE_DATABASE_URL` | `postgresql://tale:${DB_PASSWORD}@knowledge-db:5432/tale_knowledge` | **Optional.** Verbindungs-URL, die das Backend für den Wissens-Korpus nutzt. Überschreib sie, um den Korpus auf dein eigenes verwaltetes ParadeDB zu verlagern — der datenresidenz-sensible Speicher wandert unabhängig. | | `KNOWLEDGE_DB_NAME` | `tale_knowledge` | **Optional.** Name der Wissensdatenbank. Der mitgelieferte `knowledge-db`-Container erstellt diese Datenbank beim ersten Boot. | -Die auto-konstruierte operative Form ist `postgresql://tale:${DB_PASSWORD}@db:5432`. Convex erwartet diese URL ohne Datenbanknamen; der Name wird aus der Instanz-Konfiguration abgeleitet. Der Wissens-Korpus lebt in `tale_knowledge` mit den Schemata `private_knowledge` und `public_web`; die UI unter **Einstellungen > Datenresidenz** schreibt eine reichere Per-Store-Konfiguration als diese rohen Variablen, behandelt in [Datenresidenz](/de/self-hosted/configuration/data-residency). +Das Backend legt seine eigenen Schema-Migrationen auf das an, worauf `DATABASE_URL` zeigt, beim Boot, innerhalb eines Advisory Locks — ein rollender Deploy migriert also genau einmal, egal wie viele Container gemeinsam starten. Der Wissens-Korpus lebt in `tale_knowledge` mit den Schemata `private_knowledge` und `public_web`. Eine einzelne Organisation kann die Korpus-Verbindung überschreiben, ohne diese Variablen anzufassen — behandelt in [Datenresidenz](/de/self-hosted/configuration/data-residency). ## Observability @@ -67,7 +68,7 @@ Die auto-konstruierte operative Form ist `postgresql://tale:${DB_PASSWORD}@db:54 | `SENTRY_TRACES_SAMPLE_RATE` | unset | Optionale Sample-Rate für Performance-Traces im Browser (`0.0`–`1.0`). Nur Browser — das Backend meldet Fehler, nie Traces. | | `METRICS_BEARER_TOKEN` | unset | Bearer-Token, das für den Zugriff auf die Prometheus-`/metrics/*`-Endpoints nötig ist. Unset hält Metrics-Endpoints von aussen unerreichbar. | -`METRICS_BEARER_TOKEN` zu setzen exponiert zwei Endpoints hinter dem Token: `/metrics/platform` und `/metrics/convex` (Convex' 261 eingebaute Metriken, die jetzt auch die RAG- und Crawl-Timings tragen). Siehe [Observability-Konfig](/de/self-hosted/configuration/observability-config) für die Scrape-Konfiguration. +`METRICS_BEARER_TOKEN` zu setzen exponiert drei Endpoints hinter dem Token: `/metrics/backend`, `/metrics/platform` und `/metrics/sla-rules`. Scrape `/metrics/backend` — das ist die Schicht, die jede Anfrage bedient und die Job-Warteschlange abarbeitet. Siehe [Observability-Konfig](/de/self-hosted/configuration/observability-config) für die Scrape-Konfiguration. ## Provider-Secrets-Verschlüsselung @@ -78,7 +79,7 @@ Die auto-konstruierte operative Form ist `postgresql://tale:${DB_PASSWORD}@db:54 Wenn beide age-Vars unset sind, speichert Tale `providers/*.secrets.json` als Klartext-JSON mit Modus 0600. Erreich diesen Modus nur, wenn der Host-Storage at-rest verschlüsselt ist oder die Dateien von externem Tooling erzeugt werden (ein Kubernetes-Secret-Mount, ein Vault-Template). Einen age-Key zu rotieren bedeutet, den neuen Key anzuhängen, jeden Provider in der UI neu zu speichern, dann den alten Key zu entfernen. Siehe [Secrets mit SOPS](/de/self-hosted/configuration/secrets-with-sops) für den vollen Rotations-Walkthrough. -Die Umgebungsvariablen-Schlüsselquelle braucht keinen Deployment-Schalter: Zugangsdaten können statt eines gespeicherten Schlüssels nur den _Namen_ einer Umgebungsvariable halten, solange dieser Name das reservierte Präfix `TALE_PROVIDER_KEY_` trägt. Die Schranke ist fail-closed — jeder andere Name wird abgelehnt, das Feld kann also nie auf ein fremdes Deployment-Geheimnis zeigen — und Namen sind auf 40 Zeichen begrenzt. Definier die Variable hier oder in deinem Secret-Manager, damit sowohl die Plattform als auch das Convex-Backend sie lesen können; den vollen Mechanismus beschreibt [Anbieter](/de/self-hosted/configuration/providers). Zugangsdaten mit Subscription-Broker haben einen zweiten, getrennten Namensraum für das Geheimnis, das Tale **dem Broker** präsentiert: Dieses Feld nimmt einen Umgebungsvariablen-Namen unter dem reservierten Präfix `TALE_TOKEN_SOURCE_`, begrenzt auf 60 Zeichen. Die zwei Präfixe bleiben mit Absicht getrennt — ein Broker-Geheimnis ist kein Anbieter-API-Schlüssel, und keines der Felder kann eine Variable außerhalb seines eigenen Namensraums benennen. +Die Umgebungsvariablen-Schlüsselquelle braucht keinen Deployment-Schalter: Zugangsdaten können statt eines gespeicherten Schlüssels nur den _Namen_ einer Umgebungsvariable halten, solange dieser Name das reservierte Präfix `TALE_PROVIDER_KEY_` trägt. Die Schranke ist fail-closed — jeder andere Name wird abgelehnt, das Feld kann also nie auf ein fremdes Deployment-Geheimnis zeigen — und Namen sind auf 40 Zeichen begrenzt. Definier die Variable hier oder in deinem Secret-Manager, damit beide Backend-Rollen sie lesen können; den vollen Mechanismus beschreibt [Anbieter](/de/self-hosted/configuration/providers). Zugangsdaten mit Subscription-Broker haben einen zweiten, getrennten Namensraum für das Geheimnis, das Tale **dem Broker** präsentiert: Dieses Feld nimmt einen Umgebungsvariablen-Namen unter dem reservierten Präfix `TALE_TOKEN_SOURCE_`, begrenzt auf 60 Zeichen. Die zwei Präfixe bleiben mit Absicht getrennt — ein Broker-Geheimnis ist kein Anbieter-API-Schlüssel, und keines der Felder kann eine Variable außerhalb seines eigenen Namensraums benennen. ## Connector-OAuth-Apps @@ -130,7 +131,7 @@ Optionale Schalter für Features, die standardmässig nicht aktiviert sind. Jede ## RAG-Retrieval-Tuning -Optionale Stellschrauben für die Wissensdatenbank-Suche. Der In-Process-RAG-Pfad (Convex-Node-Actions) bewertet Ergebnisse mit einem Cross-Encoder neu, wenn Re-Ranking an ist. Alle tragen das `RAG_`-Präfix und werden von den Containern `platform` und `convex` beim Boot gelesen; nach einer Änderung führe `docker compose restart platform convex` aus, damit sie wirkt. +Optionale Stellschrauben für die Wissensdatenbank-Suche. Der Abruf läuft im Prozess im Backend und bewertet Ergebnisse mit einem Cross-Encoder neu, wenn Re-Ranking an ist. Alle tragen das `RAG_`-Präfix und werden beim Boot gelesen; nach einer Änderung führe `docker compose restart backend-api backend-worker` aus, damit sie wirkt. | Name | Default | Beschreibung | | ---------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -154,7 +155,7 @@ Lass es unset, um die Standard-Sitzungsdauer zu behalten. Wenn gesetzt, läuft e ## Video-Link-Ingestion (yt-dlp) -Liest Tale einen Video-Link ein, holt es dessen Transkript für den Agenten. YouTube blockiert automatisierten Zugriff von Rechenzentrums-/Server-IPs, sodass dies bei einer Cloud-Bereitstellung fehlschlagen kann. Die Bereitstellung bringt standardmäßig einen PO-Token-Provider verdrahtet mit (das vollständige Bild liefert [Video-Ingestion](/de/self-hosted/configuration/video-ingestion)); die Optionen unten sind optionale Überschreibungen und Eskalationen. Keine garantiert eine Umgehung — eine saubere Ausgangs-IP ist der wirksamste Hebel. Vom `convex`-Container gelesen und bei jeder Ingestion neu ausgewertet, sodass eine Änderung ohne Neustart greift. +Liest Tale einen Video-Link ein, holt es dessen Transkript für den Agenten. YouTube blockiert automatisierten Zugriff von Rechenzentrums-/Server-IPs, sodass dies bei einer Cloud-Bereitstellung fehlschlagen kann. Die Bereitstellung bringt standardmäßig einen PO-Token-Provider verdrahtet mit (das vollständige Bild liefert [Video-Ingestion](/de/self-hosted/configuration/video-ingestion)); die Optionen unten sind optionale Überschreibungen und Eskalationen. Keine garantiert eine Umgehung — eine saubere Ausgangs-IP ist der wirksamste Hebel. Von `backend-worker` gelesen, der die Ingestion fährt, und bei jeder Ingestion neu ausgewertet, sodass eine Änderung ohne Neustart greift. | Name | Standard | Beschreibung | | -------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -166,7 +167,7 @@ Liest Tale einen Video-Link ein, holt es dessen Transkript für den Agenten. You | `VIDEO_INGEST_PLAYER_CLIENT` | `default,tv_simply` | Kommagetrennte Fallback-Liste der YouTube-Player-Clients. Mit angebundenem PO-Token-Provider erweitert sich der Standard auf `default,mweb,tv_simply` (mweb benötigt ein GVS-Token); explizit setzen, um eine Liste zu erzwingen. | | `VIDEO_INGEST_PO_TOKEN` | nicht gesetzt | Manuell gesetztes PO-Token (`CLIENT.CONTEXT+TOKEN`). Vor allem zum Testen — Tokens sind an die Video-ID gebunden und kurzlebig; den Provider bevorzugen. | | `VIDEO_INGEST_IMPERSONATE` | nicht gesetzt | Ziel für Browser-TLS/JA3-Imitation (z. B. `safari`). Erfordert `curl_cffi` im Image; nicht setzen, sofern nicht verfügbar. | -| `VIDEO_INGEST_BIN_DIR` | nicht gesetzt | Verzeichnis, das dem `PATH` des yt-dlp/ffmpeg-Kindprozesses vorangestellt wird, damit ein selbst bereitgestelltes `yt-dlp` (samt Deno-Runtime) außerhalb der eingebackenen Bin-Verzeichnisse zuerst gefunden wird. Das `convex`-Image backt yt-dlp in den `PATH` ein, dort also nicht gesetzt lassen; auf einem Host- oder Dev-Rechner mit eigener Toolchain setzen. | +| `VIDEO_INGEST_BIN_DIR` | nicht gesetzt | Verzeichnis, das dem `PATH` des yt-dlp/ffmpeg-Kindprozesses vorangestellt wird, damit ein selbst bereitgestelltes `yt-dlp` (samt Deno-Runtime) außerhalb der eingebackenen Bin-Verzeichnisse zuerst gefunden wird. Das Plattform-Image backt yt-dlp in den `PATH` ein, in einer Container-Bereitstellung also nicht gesetzt lassen; auf einem Host- oder Dev-Rechner mit eigener Toolchain setzen. | | `VIDEO_INGEST_FFMPEG_LOCATION` | `/usr/bin/ffmpeg` | Absoluter Pfad zu dem ffmpeg, das yt-dlp für die Nachbearbeitung nutzt (Untertitel-Konvertierung, Audio-Extraktion). Überschreiben, wenn ffmpeg woanders liegt — z. B. Homebrews `/opt/homebrew/bin/ffmpeg` auf einem macOS-Dev-Rechner. | Keine dieser Optionen garantiert Erfolg gegen YouTubes adaptive Erkennung. Gewöhnliche öffentliche Videos, weniger aggressive Plattformen oder eine Bereitstellung mit Residential-IP bzw. Selbst-Hosting funktionieren üblicherweise auch ohne sie. diff --git a/docs/de/self-hosted/configuration/observability-config.md b/docs/de/self-hosted/configuration/observability-config.md index 4cc5027817..2d6579cbcb 100644 --- a/docs/de/self-hosted/configuration/observability-config.md +++ b/docs/de/self-hosted/configuration/observability-config.md @@ -19,26 +19,25 @@ Tale bringt keinen Log-Shipper mit. Der Driver-Tausch ist der unterstützte Conn ## Metriken -Der Caddy-Proxy exponiert bis zu vier Metric-Pfade, gegated von einem einzigen Bearer-Token: +Der Caddy-Proxy exponiert drei Metric-Pfade, gegated von einem einzigen Bearer-Token: -| Pfad | Quelle | Was drinsteckt | -| -------------------- | --------------- | ----------------------------------------------------------------------------- | -| `/metrics/platform` | `tale-platform` | HTTP-Latenz, Route-Counter, Node-Prozessmetriken, Antwortzeit-SLA-Ziel-Gauges | -| `/metrics/convex` | `tale-convex` | 261 eingebaute Convex-Metriken, plus die RAG- und Crawl-Timings | -| `/metrics/sla-rules` | `tale-platform` | Generierte Prometheus-Recording- + Alerting-Rules für die Antwortzeit-SLAs | -| `/metrics/backend` | `tale-backend-api` | Prozess-Metriken, HTTP-Counter und Latenz pro Routen-Klasse, Queue-Tiefe je Job-Status, laufende Chat-Generierungen, offene Hint-Streams, Drain-Zustand und dieselben SLA-Ziel-Gauges | +| Pfad | Quelle | Was drinsteckt | +| -------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/metrics/backend` | `backend-api` | Prozess-Metriken, HTTP-Counter und Latenz pro Routen-Klasse, Queue-Tiefe je Job-Status, laufende Chat-Generierungen, offene Hint-Streams, Drain-Zustand und die SLA-Ziel-Gauges | +| `/metrics/platform` | `tale-platform` | Node-Prozessmetriken (CPU, Speicher, Event-Loop-Lag, GC) und die Antwortzeit-SLA-Ziel-Gauges. Die Web-Schicht liefert statische Dateien aus, sie emittiert also keine HTTP-Request-Reihe | +| `/metrics/sla-rules` | `tale-platform` | Generierte Prometheus-Recording- + Alerting-Rules für die Antwortzeit-SLAs | -Wissens-Arbeit (RAG-Suche, Dokument-Ingestion, Web-Crawling) läuft jetzt im Convex-Backend, also reiten ihre Timings auf der `/metrics/convex`-Reihe statt auf einem separaten Endpoint. Setze `METRICS_BEARER_TOKEN` in `.env`, um diese Endpoints zu aktivieren; lass es unset, damit sie jeder Anfrage 401 zurückgeben. Der `/metrics/sla-rules`-Pfad ist eine schreibgeschützte YAML-Rules-Datei, die du in Prometheus lädst, kein Scrape-Target — die Schwellen darin sind in [Operations](/de/self-hosted/operate/observability/operations) dokumentiert. Alles ausser den gelisteten Pfaden gibt ebenfalls 401 zurück, damit ein fehlgerouteter Scraper die internen Health-Endpoints der Plattform nicht versehentlich sieht. +`/metrics/backend` ist der Pfad, auf den es ankommt: Diese Schicht bedient jede Anfrage, fährt die Wissens-Suche und arbeitet die Job-Warteschlange ab. Setze `METRICS_BEARER_TOKEN` in `.env`, um diese Endpoints zu aktivieren; lass es unset, damit sie jeder Anfrage 401 zurückgeben. Der `/metrics/sla-rules`-Pfad ist eine schreibgeschützte YAML-Rules-Datei, die du in Prometheus lädst, kein Scrape-Target — die Schwellen darin sind in [Operations](/de/self-hosted/operate/observability/operations) dokumentiert. Alles ausser den gelisteten Pfaden gibt innerhalb der Schranke 404 und ausserhalb 401 zurück, ein fehlgerouteter Scraper sieht also nie die Zahlen eines anderen Dienstes unter dem falschen Namen. -`/metrics/backend` gibt es erst, wenn ein Deployment auf das Postgres-Backend umgestellt ist (`BACKEND_UPSTREAM` in der `.env` gesetzt). Vorher antwortet der Pfad mit 404, statt still die Zahlen eines anderen Dienstes unter dem Namen des Backends auszuliefern — ein zu früh eingetragenes Scrape-Target scheitert also sichtbar, statt den falschen Prozess zu plotten. +Auf `backend-worker` gibt es nichts zu scrapen: Die Worker-Rolle bedient kein HTTP. Ihr Verhalten ist stattdessen auf `/metrics/backend` sichtbar, denn das Queue-Gauge liest die gemeinsame Job-Tabelle — `tale_backend_jobs{state="created"}`, das steigt und nie abfliesst, ist genau das Bild eines stehenden Workers. Eine funktionierende Prometheus-Scrape-Stanza: ```yaml scrape_configs: - - job_name: tale-platform + - job_name: tale-backend scheme: https - metrics_path: /metrics/platform + metrics_path: /metrics/backend authorization: credentials: static_configs: @@ -61,7 +60,9 @@ Die Sample-Rate begrenzt Performance-Traces im Browser und gilt nur dort — das ## Was noch nicht mitkommt -OpenTelemetry-Traces sind nicht in die Container eingebaut. Die Daten sind indirekt erreichbar — Convex-Action-Dauern und HTTP-Route-Timings kommen durch die Prometheus-Metriken — aber es gibt heute keinen OTLP-Exporter auf der Box. Brauchst du vollen Trace-Export, betreib einen OpenTelemetry Collector neben Tale und scrape die Prometheus-Endpoints aus ihm. +OpenTelemetry-Traces sind nicht in die Container eingebaut. Die Daten sind indirekt erreichbar — Request-Dauern pro Routen-Klasse kommen durch die Prometheus-Metriken — aber es gibt heute keinen OTLP-Exporter auf der Box. Brauchst du vollen Trace-Export, betreib einen OpenTelemetry Collector neben Tale und scrape die Prometheus-Endpoints aus ihm. + +Ein Request-Log gibt es auch nicht. Das Backend erfasst jede Anfrage als Metrik, nicht als Zeile, in `docker compose logs backend-api` steht also kein Audit pro Anfrage — der Access-Log des Proxys kommt dem am nächsten, und für Control-Plane-Aktionen ist das Audit-Log im Produkt zuständig. ## Wo das hingehört diff --git a/docs/de/self-hosted/configuration/providers.md b/docs/de/self-hosted/configuration/providers.md index 86f789b0d5..fe2b4dc6ea 100644 --- a/docs/de/self-hosted/configuration/providers.md +++ b/docs/de/self-hosted/configuration/providers.md @@ -66,11 +66,11 @@ TALE_PROVIDER_KEY_OPENAI_PROD=sk-... -Die Schranke ist fail-closed: Jeder Name ausserhalb des reservierten Präfixes wird abgelehnt. Genau das verhindert, dass Zugangsdaten ein fremdes Deployment-Geheimnis wie `SOPS_AGE_KEY` oder `BETTER_AUTH_SECRET` benennen und es als Bearer-Token an einen Anbieter-Endpunkt geschickt wird. Namen sind auf 40 Zeichen begrenzt — das Limit der Env-Synchronisierung von der Plattform zu Convex, denn ein längerer Name würde die Backend-Laufzeit nie erreichen. +Die Schranke ist fail-closed: Jeder Name ausserhalb des reservierten Präfixes wird abgelehnt. Genau das verhindert, dass Zugangsdaten ein fremdes Deployment-Geheimnis wie `SOPS_AGE_KEY` oder `BETTER_AUTH_SECRET` benennen und es als Bearer-Token an einen Anbieter-Endpunkt geschickt wird. Namen sind auf 40 Zeichen begrenzt. -Definier die Variable so, dass sowohl der Plattform-Container als auch das Convex-Backend sie lesen können. Die Plattform synchronisiert ihre Umgebung beim Boot zu Convex, damit die dortigen Actions denselben Wert auflösen; eine nach dem Boot hinzugefügte oder geänderte Variable braucht einen Neustart des Plattform-Containers, bevor sie sichtbar wird. Werte werden getrimmt, was dir den Zeilenumbruch am Ende einer gemounteten Secret-Datei und den daraus folgenden `401` erspart. +Definier die Variable dort, wo die Backend-Container sie lesen — in `.env` oder über das, was dein Secret-Store in `backend-api` und `backend-worker` injiziert. Beide Rollen lösen Zugangsdaten auf, beide brauchen den Wert also: Der Worker macht für Hintergrundarbeit dieselben Provider-Aufrufe, die die API für einen Live-Turn macht. Eine nach dem Boot hinzugefügte oder geänderte Variable braucht `docker compose restart backend-api backend-worker`, bevor sie sichtbar wird. Werte werden getrimmt, was dir den Zeilenumbruch am Ende einer gemounteten Secret-Datei und den daraus folgenden `401` erspart. ## Broker-Secrets aus der Umgebung diff --git a/docs/de/self-hosted/configuration/retention.md b/docs/de/self-hosted/configuration/retention.md index 4c144edd14..990ab72f7f 100644 --- a/docs/de/self-hosted/configuration/retention.md +++ b/docs/de/self-hosted/configuration/retention.md @@ -41,7 +41,7 @@ Die vom Admin gewählten Aufbewahrungsfenster liegen in einer separaten Datei, ` ## Der Retention-Sweep -Ein geplanter Cron in `tale-convex` läuft die tatsächliche Löschung. Jede Kategorie wird unabhängig gesweept — ein langsamer Lauf einer blockiert die anderen nicht. Löschungen sind audited (jede Kategorie hat ihr eigenes `*.retention_deleted`-Event), und eine Entität in ihrem Gnaden-Fenster wiederherzustellen ist von **Papierkorb** möglich, bevor der finale Sweep läuft. +Ein täglich geplanter Job auf `backend-worker` löscht tatsächlich, um 04:00 UTC. Jede Kategorie wird unabhängig gesweept — ein langsamer Lauf einer blockiert die anderen nicht, und ein gescheiterter Sweep wird von der Warteschlange erneut versucht statt bis morgen übersprungen. Löschungen sind audited (jede Kategorie hat ihr eigenes `*.retention_deleted`-Event), und eine Entität in ihrem Gnaden-Fenster wiederherzustellen ist von **Papierkorb** möglich, bevor der finale Sweep läuft. Audit-Log-Einträge unterliegen selbst der Retention, aber ihre Untergrenze wird pro Deployment durchgesetzt, nicht pro Org: Die strengste (kürzeste) Audit-Log-Retention über alle Orgs ist das, was tatsächlich läuft. Eine strengere Mandantin zieht alle enger — denk daran auf Multi-Tenant-Instanzen. diff --git a/docs/de/self-hosted/configuration/secrets-with-sops.md b/docs/de/self-hosted/configuration/secrets-with-sops.md index 5451d49d6b..8322b2c001 100644 --- a/docs/de/self-hosted/configuration/secrets-with-sops.md +++ b/docs/de/self-hosted/configuration/secrets-with-sops.md @@ -15,7 +15,7 @@ Die Env-Vars, die die Modi steuern, sind `SOPS_AGE_KEY` und `SOPS_AGE_KEY_FILE` | Schlüssel-Datei | `SOPS_AGE_KEY_FILE=/path/to/keys` | Pflicht für Rotation. Ein age-Schlüssel pro Zeile, `#`-Kommentare. | | Klartext bei 0600 | Beide unset | Platte at-rest verschlüsselt, oder externe Tooling schreibt die Dateien. | -Der Plattform-Container wählt den Modus beim Boot. Die Inline-Form ist die einfachste; die Datei-Form ist die einzige, die mehrere Leser unterstützt (was Rotation ohne Downtime möglich macht); die Klartext-Form überspringt SOPS ganz und vertraut dem Dateisystem. +Die Backend-Container wählen den Modus beim Boot — sie besitzen jeden Schreibzugriff auf den Config-Store und sind die einzigen Prozesse, die ihn entschlüsseln; die Web-Schicht mountet dasselbe Volume read-only und fasst den age-Schlüssel nie an. Die Inline-Form ist die einfachste; die Datei-Form ist die einzige, die mehrere Leser unterstützt (was Rotation ohne Downtime möglich macht); die Klartext-Form überspringt SOPS ganz und vertraut dem Dateisystem. ## Verschlüsselter Modus beim ersten Boot @@ -30,7 +30,7 @@ cat providers/openai.secrets.json # } ``` -Entschlüsselung passiert in-process, wenn der Plattform-Container die Datei liest. Der age-Schlüssel verlässt den Speicher des Plattform-Containers nie. +Entschlüsselung passiert in-process, wenn ein Backend-Container die Datei liest. Der age-Schlüssel verlässt den Speicher dieses Containers nie. ## Den age-Schlüssel rotieren @@ -43,10 +43,10 @@ age-keygen -o /etc/tale/age-keys.txt # 2. Häng den neuen Schlüssel als zweite Zeile in der Datei an echo "AGE-SECRET-KEY-1NEW..." >> /etc/tale/age-keys.txt -# 3. Richte .env auf die Datei und starte den Plattform-Container neu +# 3. Richte .env auf die Datei und starte die Backend-Container neu sed -i 's|^SOPS_AGE_KEY=.*|# SOPS_AGE_KEY=|' .env sed -i 's|^# SOPS_AGE_KEY_FILE=.*|SOPS_AGE_KEY_FILE=/etc/tale/age-keys.txt|' .env -docker compose restart tale-platform tale-convex +docker compose restart backend-api backend-worker ``` Jetzt können sowohl der alte als auch der neue Schlüssel bestehende Dateien entschlüsseln. Speichere den API-Schlüssel jedes Anbieters unter **Einstellungen > Anbieter** neu — jedes Speichern erzeugt Ciphertext, der von beiden Schlüsseln lesbar ist. Sobald jeder Anbieter neu gespeichert wurde (die Spalte **Zuletzt rotiert** in der Anbieter-Tabelle sagt dir, welche noch alten Ciphertext halten), entferne den alten Schlüssel aus der Datei: @@ -54,10 +54,10 @@ Jetzt können sowohl der alte als auch der neue Schlüssel bestehende Dateien en ```bash # 4. Lass die alte Schlüssel-Zeile fallen und starte erneut neu sed -i '/^AGE-SECRET-KEY-1OLD/d' /etc/tale/age-keys.txt -docker compose restart tale-platform tale-convex +docker compose restart backend-api backend-worker ``` -Die Reihenfolge ist tragend: Entfern den alten Schlüssel nie, bevor jede Datei neu verschlüsselt ist, oder der Plattform-Container scheitert beim Lesen der noch-alten Dateien bei der nächsten Entschlüsselung. +Die Reihenfolge ist tragend: Entfern den alten Schlüssel nie, bevor jede Datei neu verschlüsselt ist, oder das Backend scheitert beim Lesen der noch-alten Dateien bei der nächsten Entschlüsselung. ## Auf Klartext umsteigen diff --git a/docs/de/self-hosted/contributing-docker.md b/docs/de/self-hosted/contributing-docker.md index 823759b265..ea7c2ecb8c 100644 --- a/docs/de/self-hosted/contributing-docker.md +++ b/docs/de/self-hosted/contributing-docker.md @@ -14,15 +14,14 @@ Der Stack ist vollständig TypeScript — kein Python-Image. Jedes Image hat ein | Image | Quell-Pfad | Basis | | ------------------------ | ----------------------------- | ---------------------------- | | `tale-proxy` | `services/proxy/` | Caddy | -| `tale-platform` | `services/platform/` | Bun + Debian slim | -| `tale-convex` | `services/convex/` | Convex local-backend | +| `tale-platform` | `services/platform/` | Debian slim + Bun + Node | | `tale-db` | `services/db/` | ParadeDB (Postgres) | | `tale-sandbox` | `services/sandbox/` | Bun + Docker-CLI | | `tale-sandbox-egress` | `services/sandbox-egress/` | Alpine + tinyproxy | | `tale-sandbox-runtime` | `services/sandbox-runtime/` | Bun + Chromium + Playwright | | `tale-sandbox-buildkitd` | `services/sandbox-buildkitd/` | Debian + BuildKit + redsocks | -Beide Datenbank-Container — `db` und `knowledge-db` — bauen aus demselben `tale-db`-ParadeDB-Image; der Unterschied ist die Datenbank, die jeder bedient. Das LLM-Gateway `tale-sandbox-llm-gateway` ist ein gepinntes Upstream-Image (`maximhq/bifrost`), hat also kein Dockerfile im Repo. Die Compose-Dateien im Repo-Root (`compose.yml` für Development, die CLI-generierte Produktions-Compose) referenzieren diese über `ghcr.io/tale-project/tale/:`. Ein lokaler Build ersetzt den Registry-Pull mit einem `build:`-Block in Compose. +Beide Datenbank-Container — `db` und `knowledge-db` — bauen aus demselben `tale-db`-ParadeDB-Image; der Unterschied ist die Datenbank, die jeder bedient. Dasselbe Image bedient auch beide Backend-Rollen: `backend-api` und `backend-worker` sind `tale-platform`, gestartet mit einem anderen `TALE_ROLE` — deshalb können sie gegenüber der Web-Schicht nie in einen Versions-Skew laufen. Zwei Container haben kein eigenes Dockerfile: Der Blob-Store ist ein Upstream-MinIO-Image, das Compose direkt referenziert, und `tale-sandbox-llm-gateway` ist ein dünnes Re-Tag des gepinnten Upstream-Gateways `maximhq/bifrost`, das zur Laufzeit nichts ändert. Die Compose-Dateien im Repo-Root (`compose.yml` für Development, die CLI-generierte Produktions-Compose) referenzieren diese über `ghcr.io/tale-project/tale/:`. Ein lokaler Build ersetzt den Registry-Pull mit einem `build:`-Block in Compose. ## Lokal bauen @@ -47,7 +46,7 @@ Die unterstützten Erweiterungs-Punkte für Forks sind auf der Dockerfile-Ebene. - **Sandbox-Runtime-Image** — `services/sandbox-runtime/Dockerfile` ist die Ausführungsumgebung für **Code-ausführen**, Web-Render und Dokumentgenerierung; es trägt bereits Chromium und Playwright. Ein Fork, der ein zusätzliches System-Paket oder einen anderen Browser-Build braucht, patcht hier. - **Sandbox-Egress-Proxy** — `services/sandbox-egress/tinyproxy.conf.template` ist die Proxy-Konfiguration, die der Entrypoint beim Start rendert: standardmäßig offenes Egress, oder ein Default-Deny-Hostname-Filter, wenn `SANDBOX_EGRESS_ALLOWLIST` gesetzt ist. Ein Fork, der anderes Proxy-Verhalten braucht, patcht hier. -Was keine unterstützte Naht ist: der Anwendungscode des Convex-Backends, inklusive der Dokument-Extraktion und der RAG- und Crawler-Logik, die jetzt im Prozess leben (`services/platform/convex/`), und der Runtime-Code des Plattform-Containers (`services/platform/app/`). Diese Dateien sind Anwendungscode, keine Konfiguration — einen Dokumentformat-Extraktor hinzuzufügen oder das Retrieval-Verhalten zu ändern ist ein echter Fork und trägt die Upgrade-Steuer. +Was keine unterstützte Naht ist: der Anwendungscode des Backends (`services/platform/backend/`), inklusive der Dokument-Extraktion und der RAG- und Crawler-Logik, die dort im Prozess laufen, und der Runtime-Code der Web-Schicht (`services/platform/app/`). Diese Dateien sind Anwendungscode, keine Konfiguration — einen Dokumentformat-Extraktor hinzuzufügen oder das Retrieval-Verhalten zu ändern ist ein echter Fork und trägt die Upgrade-Steuer. ## Taggen und in eigene Registry pushen diff --git a/docs/de/self-hosted/install/docker-compose-reference.md b/docs/de/self-hosted/install/docker-compose-reference.md index 86c806f275..8ca2146e3a 100644 --- a/docs/de/self-hosted/install/docker-compose-reference.md +++ b/docs/de/self-hosted/install/docker-compose-reference.md @@ -38,15 +38,19 @@ Die linkeste Datei ist die Basis; jede nachfolgende Datei merged ihre Schlüssel ## Services und ihre Rollen -Der Basis-Graph fährt acht Container hoch: - -- `tale-proxy` — Caddy. TLS, Reverse-Proxy, 301s. -- `tale-platform` — die TanStack-Start-App. Die User-zugewandte UI und API. -- `tale-convex` — das Convex-Backend. WebSocket, Queries, Mutationen, Actions — und die In-Process-RAG-Suche, Dokument-Ingestion, das Web-Crawling und die Dokumentgenerierung, die früher separate Services waren. -- `tale-db` — operatives Postgres (ParadeDB). Der persistente Speicher des Convex-Backends. +Der Basis-Graph fährt zehn Container hoch: + +- `tale-proxy` — Caddy. TLS, Reverse-Proxy, 301s. Er veröffentlicht ausserdem den Bucket-Pfad des Blob-Stores, damit präsignierte URLs im Browser funktionieren. +- `tale-platform` — die TanStack-Start-App. Die User-zugewandte UI, die statischen Assets und die öffentliche `/status`-Seite. +- `backend-api` — das Anwendungs-Backend: ein Node-Prozess, der jede Tür unter `/api/` bedient, plus `/events`, `/dav` und die Maschinen-API. Die Wissens-Suche läuft in diesem Prozess. +- `backend-worker` — dasselbe Image in der Worker-Rolle, das die pg-boss-Job-Warteschlange abarbeitet: Dokument-Ingestion und Embedding, Web-Crawling, Automation-Runs, Retention-Sweeps. Er bedient kein HTTP. Beide Backend-Services nehmen `--scale`, und deshalb hat keiner einen festen Container-Namen. +- `tale-db` — operatives Postgres (ParadeDB). Die `tale_app`-Datenbank: Anwendungs-State, Sessions und die Job-Warteschlange. +- `tale-object-store` — der Blob-Store (MinIO). Jedes hochgeladene Dokument, jeder Chat-Anhang, jede Audiodatei und jedes generierte Medium. Nur intern erreichbar. - `tale-knowledge-db` — Postgres des Wissens-Korpus (ParadeDB). Die `tale_knowledge`-Datenbank mit Dokument-Chunks, Embeddings und gecrawlten Seiten, auf Port 5433, damit sie nie mit `tale-db` auf 5432 kollidiert. - `tale-sandbox-llm-gateway` — das LLM-Gateway für Harness-Züge (gepinntes externes Image). -- `tale-sandbox-egress` und `tale-sandbox` — die Sandbox-Ebene. Run-Code-Container hinter einem Egress-Proxy (standardmäßig offen; sperrbar mit `SANDBOX_EGRESS_ALLOWLIST`), zugleich die Headless-Browser-Laufzeit, die das Convex-Backend für Web-Render und Dokumentgenerierung aufruft. +- `tale-sandbox-egress` und `tale-sandbox` — die Sandbox-Ebene. Run-Code-Container hinter einem Egress-Proxy (standardmäßig offen; sperrbar mit `SANDBOX_EGRESS_ALLOWLIST`), zugleich die Headless-Browser-Laufzeit, die das Backend für Web-Render und Dokumentgenerierung aufruft. + +Dazu kommt ein `bgutil-provider`-Sidecar für die YouTube-Ingestion; er ist best-effort, und der Stack funktioniert ohne ihn. Ein Single-Host-`tale deploy`-Stack lässt `tale-knowledge-db` weg und faltet den Korpus in `tale-db` unter dem Netzwerk-Alias `knowledge-db`. Der Stack ist jetzt vollständig TypeScript — es gibt keinen Python-Service im Graph. [Container-Architektur](/de/self-hosted/operate/container-architecture) vertieft, was was besitzt. diff --git a/docs/de/self-hosted/install/quickstart.md b/docs/de/self-hosted/install/quickstart.md index 2b30bb45e1..9e11a951cf 100644 --- a/docs/de/self-hosted/install/quickstart.md +++ b/docs/de/self-hosted/install/quickstart.md @@ -83,7 +83,7 @@ Auf einer leeren Instanz gibt es keine Sign-up-Seite zu suchen: Der erste Besuch -[Erster Admin](/de/self-hosted/install/first-admin) behandelt den Wizard im Detail, wie Teammitglieder dazukommen und den Convex-Dashboard-Admin-Key — ein Backend-Inspektionswerkzeug, das mit der Anmeldung nichts zu tun hat. +[Erster Admin](/de/self-hosted/install/first-admin) behandelt den Wizard im Detail und wie Teammitglieder dazukommen. diff --git a/docs/de/self-hosted/operate/backups-and-restore.md b/docs/de/self-hosted/operate/backups-and-restore.md index aecdc1de92..d66adf262f 100644 --- a/docs/de/self-hosted/operate/backups-and-restore.md +++ b/docs/de/self-hosted/operate/backups-and-restore.md @@ -3,21 +3,31 @@ title: Backups und Restore description: Volume-Snapshots über `tale backup`, der automatische Pre-Migrations-Snapshot, Retention, die Off-Host-Kopie und der `tale restore`-Drill. --- -Tales Backup-Einheit ist der Volume-Snapshot: ein pausiertes, checksummengesichertes Tar jedes Daten-Volumes der Instanz, geschrieben in ein dediziertes `backups`-Volume, das neben den Daten lebt, die es schützt. Das CLI nimmt automatisch einen vor jedem Deploy-Schritt, der Daten migrieren kann, und `tale backup` nimmt einen auf Zuruf. Recovery ist `tale restore ` plus ein Redeploy der passenden Version — dieses Paar ist die Antwort auf ein gescheitertes Upgrade und der Grund, warum `tale rollback` sich alles jenseits eines Patch-Schritts verweigern kann. +Tales Backup-Einheit ist der Volume-Snapshot: ein pausiertes, checksummengesichertes Tar der Datenbank, des Org-Config-Baums und des Proxy-States, geschrieben in ein dediziertes `backups`-Volume, das neben den Daten lebt, die es schützt. Das CLI nimmt automatisch einen vor jedem Deploy-Schritt, der Daten migrieren kann, und `tale backup` nimmt einen auf Zuruf. Recovery ist `tale restore ` plus ein Redeploy der passenden Version — dieses Paar ist die Antwort auf ein gescheitertes Upgrade und der Grund, warum `tale rollback` sich alles jenseits eines Patch-Schritts verweigern kann. -Der Architektur-Kontext lebt in [Container-Architektur](/de/self-hosted/operate/container-architecture); diese Seite deckt ab, was ein Snapshot enthält, wann einer genommen wird, wie die Kopie vom Host runterkommt und den Restore-Walk. +Ein Snapshot ist nicht die ganze Instanz. Hochgeladene Datei-Blobs liegen außerhalb, deshalb ist der Off-Host-Job weiter unten das, was einen vollständigen Wiederaufbau überhaupt möglich macht — lies diesen Abschnitt auch dann, wenn du nie manuell snapshotest. + +Der Architektur-Kontext lebt in [Container-Architektur](/de/self-hosted/operate/container-architecture); diese Seite deckt ab, was ein Snapshot enthält, was er dir überlässt, wann einer genommen wird, wie die Kopie vom Host runterkommt und den Restore-Walk. ## Was ein Snapshot enthält -| Volume | Enthält | -| ---------------------------- | ---------------------------------------------------- | -| `db-data` | Postgres — Agents, Runs, das Audit-Log | -| `convex-data` | Org-Config, Anbieter-Secrets, hochgeladenes Branding | -| `rag-data` | Der Vektor-Index aus deinen Dokumenten | -| `crawler-data` | Gecrawltes Website-Wissen | -| `caddy-data`, `caddy-config` | TLS-Zertifikate und Proxy-State | +| Volume | Enthält | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `db-data` | Postgres — die Anwendungsdatenbank (Chats, Aufgaben, Automation-Runs, das Audit-Log) und, auf einem Single-Host-`tale deploy`-Stack mit einem gemeinsamen Postgres, das Wissens-Korpus | +| `convex-data` | Der Org-Config-Baum — Agents, Automations, Connectors, Anbieter, Skills, Governance-Policies, SSO-Verbindungen, Branding | +| `caddy-data`, `caddy-config` | TLS-Zertifikate und Proxy-State | + +`convex-data` ist der historische Name des Config-Volumes. Er bleibt bewusst, damit die Abschaltung des Convex-Backends niemanden zwingt, ein Volume nur für eine Umbenennung zu migrieren; Convex läuft darin nichts mehr. + +Jeder Snapshot ist ein Verzeichnis mit einem Namen wie `20260611-142530-deploy` im `backups`-Volume des Projekts: ein `.tar.gz` pro Volume, je ein `.sha256`-Sidecar und ein zuletzt geschriebenes `manifest.json`. Ein Verzeichnis ohne Manifest ist ein unvollständiger Snapshot — er taucht nie in Listings auf und lässt sich nie wiederherstellen. + + + +**Hochgeladene Dateien sind nicht im Snapshot.** Dokument-Blobs, Chat-Anhänge, Audio und generierte Medien liegen im Blob-Store auf dem Volume `object-store-data`, und `tale backup` erfasst es nicht. Ein Restore bringt damit Zeilen zurück, die auf Blobs zeigen, die der Store nicht mehr hat — die App rendert die Dokumentliste und scheitert beim Öffnen. Erfasse `object-store-data` im selben Job, der das `backups`-Volume vom Host kopiert, oder richte das Deployment auf einen Object-Store, der seine eigenen Backups mitbringt. + + -Jeder Snapshot ist ein Verzeichnis mit einem Namen wie `20260611-142530-deploy` im `backups`-Volume des Projekts: ein `.tar.gz` pro Volume, je ein `.sha256`-Sidecar und ein zuletzt geschriebenes `manifest.json`. Ein Verzeichnis ohne Manifest ist ein unvollständiger Snapshot — er taucht nie in Listings auf und lässt sich nie wiederherstellen. Zwei Dinge leben außerhalb der Volumes und brauchen separate Erfassung: der Projekt-Workspace (das Verzeichnis mit `tale.json`) und `.env`. +Drei weitere Dinge leben außerhalb der gesnapshotteten Volumes und brauchen separate Erfassung: der Blob-Store oben, der Projekt-Workspace (das Verzeichnis mit `tale.json`) und `.env`. ## Wann Snapshots genommen werden @@ -36,19 +46,20 @@ Die Rotation behält die neuesten fünf Snapshots und alles aus den letzten 14 T ## Off-Host-Kopie -Die Snapshots leben auf demselben Host wie die Daten, die sie schützen — eine tote Platte nimmt beides mit. Richte dein bestehendes Backup-Tooling (Restic, Borg, Velero, Cloud-Provider-Snapshots) auf das `backups`-Volume und erfasse den Projekt-Workspace und `.env` im selben Job. Tale bringt keinen Upload-Schritt mit — die Off-Host-Kopie unter deinem bestehenden Backup-Vertrag zu halten ist Absicht. +Die Snapshots leben auf demselben Host wie die Daten, die sie schützen — eine tote Platte nimmt beides mit. Richte dein bestehendes Backup-Tooling (Restic, Borg, Velero, Cloud-Provider-Snapshots) auf das `backups`-Volume **und** auf `object-store-data` und erfasse den Projekt-Workspace und `.env` im selben Job. Tale bringt keinen Upload-Schritt mit — die Off-Host-Kopie unter deinem bestehenden Backup-Vertrag zu halten ist Absicht. ```bash -# crontab auf dem Host — stündliche Restic-Kopie des backups-Volumes nach S3 +# crontab auf dem Host — stündliche Restic-Kopie der Snapshots und des Blob-Stores 0 * * * * restic -r s3:s3.amazonaws.com/bucket/tale backup \ - /var/lib/docker/volumes/_backups/_data + /var/lib/docker/volumes/_backups/_data \ + /var/lib/docker/volumes/_object-store-data/_data ``` -Den Host-Pfad des Volumes findest du mit `docker volume inspect _backups`; die Projekt-ID steht in `tale.json`. +Den Host-Pfad eines Volumes findest du mit `docker volume inspect _backups`; die Projekt-ID steht in `tale.json`. ## Einen Snapshot wiederherstellen -`tale restore` ohne Argumente listet, was verfügbar ist; mit einer ID verifiziert es die Checksummen, leert die Daten-Volumes und entpackt den Snapshot. Es verweigert, solange irgendein Projekt-Container läuft — `--stop` stoppt sie — und fragt nach Bestätigung, bevor es irgendetwas anfasst. +`tale restore` ohne Argumente listet, was verfügbar ist; mit einer ID verifiziert es die Checksummen, leert die Volumes, die der Snapshot abdeckt, und entpackt ihn. Es verweigert, solange irgendein Projekt-Container läuft — `--stop` stoppt sie — und fragt nach Bestätigung, bevor es irgendetwas anfasst. Wiederhergestellt werden nur die Volumes aus der Tabelle oben; den Blob-Store spielst du selbst aus der Off-Host-Kopie zurück, bevor du den Stack wieder hochfährst. ```bash # Sehen, was verfügbar ist @@ -66,7 +77,7 @@ Das Redeploy der passenden Version ist Teil des Restores, kein optionales Extra: ## Restore-Drill -Lauf den Drill vierteljährlich auf einem Nicht-Produktions-Host. Der Drill ist nicht „existiert ein Snapshot" — er ist „kann ein frischer Host aus der Off-Host-Kopie des `backups`-Volumes, dem Projekt-Workspace und `.env` in unter einer Stunde wiederaufgebaut werden". Die Fehler-Modi, die der Drill fängt: ein Off-Host-Job, der den Workspace nie erfasst hat, und eine veraltete `.env`, die nicht mehr zu den Anforderungen des aktuellen Binarys passt. +Lauf den Drill vierteljährlich auf einem Nicht-Produktions-Host. Der Drill ist nicht „existiert ein Snapshot" — er ist „kann ein frischer Host aus der Off-Host-Kopie des `backups`-Volumes, dem Blob-Store, dem Projekt-Workspace und `.env` in unter einer Stunde wiederaufgebaut werden". Schließe damit ab, ein Dokument zu öffnen, das vor dem Snapshot hochgeladen wurde: Das ist der eine Schritt, der beweist, dass der Blob-Store zusammen mit der Datenbank zurückgekommen ist, und genau den überspringt ein Drill, der nur den Snapshot prüft. Die weiteren Fehler-Modi, die der Drill fängt: ein Off-Host-Job, der den Workspace nie erfasst hat, und eine veraltete `.env`, die nicht mehr zu den Anforderungen des aktuellen Binarys passt. ## Wo das hingehört diff --git a/docs/de/self-hosted/operate/container-architecture.md b/docs/de/self-hosted/operate/container-architecture.md index ae0ff945b3..5c59a4a4bb 100644 --- a/docs/de/self-hosted/operate/container-architecture.md +++ b/docs/de/self-hosted/operate/container-architecture.md @@ -3,59 +3,71 @@ title: Container-Architektur description: Welcher Container welchen Job in einer laufenden Tale-Instanz hat, der Request-Pfad einer Chat-Nachricht und wie ein Ausfall jedes Containers aussieht. --- -Eine Tale-Instanz besteht aus acht Containern, verdrahtet durch docker compose. Die Architektur-Seite hat behandelt, wofür jeder Container da ist; diese Seite ist die Operator-Version — welcher Container welchen Job besitzt, wie eine Chat-Nachricht durch sie fliesst und wie der Fehlermodus aussieht, wenn einer von ihnen stirbt. +Eine Tale-Instanz besteht aus zehn Containern, verdrahtet durch docker compose. Die Architektur-Seite hat behandelt, wofür jeder Container da ist; diese Seite ist die Operator-Version — welcher Container welchen Job besitzt, wie eine Chat-Nachricht durch sie fliesst und wie der Fehlermodus aussieht, wenn einer von ihnen stirbt. Lies das, wenn du Bereitschaft hast. Komm zurück, wenn du entscheidest, welchen Container du während eines Upgrades zuerst rollst. -## Die acht Container, mit ihren Jobs +## Die zehn Container, mit ihren Jobs -| Container | Job | Ausfälle betreffen | -| -------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `tale-proxy` | TLS-Terminierung + Edge-Routing | Jeden Ingress — kein Client erreicht die UI | -| `tale-platform` | UI-Server, statische Asset-Auslieferung | Browser sieht 502; die API ist erreichbar | -| `tale-convex` | Backend Actions/Queries/Mutations + WebSocket, plus In-Process-RAG, Crawling und Dokumentgen | UI lädt, aber ohne Daten; laufende Chats stocken; Ingestion stockt | -| `tale-db` | Operatives Postgres für Convex | Convex fällt in Read-only; Writes blockieren | -| `tale-knowledge-db` | Postgres des Wissens-Korpus (Dokument-Chunks, Embeddings, gecrawlte Seiten) | Wissens-Suche liefert leer; Ingestion scheitert | -| `tale-sandbox-llm-gateway` | LLM-Gateway für Harness-Züge | Harness-Züge erreichen kein Modell; Chat ist unbetroffen | -| `tale-sandbox-egress` | Netzwerk-Egress für sandboxierten Code | **Code-ausführen**-Tool scheitert mit „Egress denied"; Web-Render scheitert | -| `tale-sandbox` | Sandbox-Laufzeit + Headless-Browser für Web-Render und Dokumentgenerierung | **Code-ausführen**, Web-Crawl-Render und Dokumentgenerierung scheitern alle | +| Container | Job | Ausfälle betreffen | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `tale-proxy` | TLS-Terminierung + Edge-Routing | Jeden Ingress — kein Client erreicht die UI | +| `tale-platform` | UI-Server, statische Asset-Auslieferung, die öffentliche `/status`-Seite | Browser sieht 502; die API ist erreichbar | +| `backend-api` | Jede Anwendungs-Anfrage: Auth, App-API, Maschinen-API, WebDAV, der Live-Update-Stream — und die Wissens-Suche im selben Prozess | UI lädt, aber ohne Daten; laufende Chats stocken | +| `backend-worker` | Hintergrund-Jobs: Dokument-Ingestion und Embedding, Web-Crawling, Automation-Runs, Retention-Sweeps, der Cron-Plan | UI arbeitet weiter; Uploads bleiben auf „Indexieren", Automations feuern nicht | +| `tale-db` | Postgres — die Anwendungsdatenbank, die Job-Warteschlange und das Wissens-Korpus | Writes werden abgelehnt; die App degradiert auf das, was schon geladen ist | +| `tale-knowledge-db` | Postgres des Wissens-Korpus (Dokument-Chunks, Embeddings, gecrawlte Seiten) | Wissens-Suche liefert leer; Ingestion scheitert | +| `tale-object-store` | Der Blob-Store — hochgeladene Dokumente, Chat-Anhänge, Audio, generierte Medien | Jeder Upload und jeder Download scheitert; der Rest der App arbeitet weiter | +| `tale-sandbox-llm-gateway` | LLM-Gateway für Harness-Züge | Harness-Züge erreichen kein Modell; Chat ist unbetroffen | +| `tale-sandbox-egress` | Netzwerk-Egress für sandboxierten Code | **Code-ausführen**-Tool scheitert mit „Egress denied"; Web-Render scheitert | +| `tale-sandbox` | Sandbox-Laufzeit + Headless-Browser für Web-Render und Dokumentgenerierung | **Code-ausführen**, Web-Crawl-Render und Dokumentgenerierung scheitern alle | -Ein Container ist dem öffentlichen Netz exponiert (`tale-proxy` für HTTPS, optional `tale-sandbox-egress` ausgehend für die Sandbox); der Rest nur intern. +`backend-api` und `backend-worker` sind dasselbe Image wie `tale-platform`, nur in einer anderen Rolle gestartet, und beide skalieren unabhängig — `docker compose up -d --scale backend-worker=3` ist eine unterstützte Topologie, und genau deshalb gibt die ausgelieferte Compose-Datei ihnen keinen festen Container-Namen. Sprich sie über den Service-Namen an. Ein `tale deploy`-Stack nennt sie `-backend-api` und `-backend-worker`. + +`tale-knowledge-db` ist in der ausgelieferten Compose-Datei ein eigener Container. Ein Single-Host-`tale deploy`-Stack faltet das Korpus stattdessen in `tale-db` und gibt diesem Container den Netzwerk-Alias `knowledge-db`, sodass derselbe Connection-String in beiden Fällen auflöst — wenn `tale status` keine Wissensdatenbank zeigt, liegt es daran, und `tale-db` ist der Container, in den du schauen musst. + +Ein Container ist dem öffentlichen Netz exponiert (`tale-proxy` für HTTPS, optional `tale-sandbox-egress` ausgehend für die Sandbox); der Rest nur intern, der Blob-Store eingeschlossen — Blobs erreichen den Browser über präsignierte URLs, die der Proxy unter dem Bucket-Pfad weiterleitet. ## Der Request-Pfad Eine Chat-Nachricht macht einen Durchlauf durch die Container: 1. Browser → `tale-proxy` (TLS terminiert). -2. `tale-proxy` → `tale-platform` für HTML/JS, → `tale-convex` für API + WebSocket. -3. `tale-convex` liest die Provider-Config der Organisation, wählt das Modell, öffnet einen Stream zum Upstream-Provider. -4. Holt der Agent Wissen: `tale-convex` fährt die RAG-Suche im Prozess und fragt `tale-knowledge-db` direkt ab — kein separater Retrieval-Dienst im Pfad. -5. Führt der Agent Code aus: `tale-convex` → `tale-sandbox` → `tale-sandbox-egress` für ausgehende Netzwerk-Aufrufe. -6. Der Provider-Stream gibt Tokens durch `tale-convex` zurück an den Browser über den WebSocket. +2. `tale-proxy` → `tale-platform` für HTML, JS und die statischen Assets → `backend-api` für alles unter `/api/`, plus `/events`, `/dav` und die Maschinen-API. +3. `backend-api` liest die Provider-Config der Organisation, wählt das Modell, öffnet einen Stream zum Upstream-Provider und streamt die Tokens über Server-Sent Events zurück an den Browser. +4. Holt der Agent Wissen: `backend-api` fährt die Suche im Prozess und fragt die Korpus-Datenbank direkt ab — kein separater Retrieval-Dienst im Pfad. +5. Führt der Agent Code aus: `backend-api` → `tale-sandbox` → `tale-sandbox-egress` für ausgehende Netzwerk-Aufrufe. +6. Alles, was der Turn aufgeschoben hat — das Indexieren eines neuen Uploads, eine Folge-Automation — committet in derselben Transaktion wie der Write in die Job-Warteschlange, und `backend-worker` nimmt es auf. + +Neben dem Token-Stream hält der Browser eine langlebige `GET /events`-Verbindung zu `backend-api`. Sie trägt keine Daten, nur Invalidierungs-Hinweise: Kommt einer an, lädt die App die betroffene Query neu. Ein toter Hinweis-Stream sieht deshalb aus wie eine UI, die von selbst nichts mehr aktualisiert — nicht wie ein Ausfall. -Der heisse Pfad ist kurz. Fühlt sich die Chat-Latenz falsch an, ist der Container, der schuld ist, fast immer der Upstream-Provider, nicht Tale; die Metric-Endpoints auf `tale-convex` (das jetzt auch die RAG- und Crawl-Timings trägt) zeigen die Zeit in jedem Sprung. +Der heisse Pfad ist kurz. Fühlt sich die Chat-Latenz falsch an, ist der Container, der schuld ist, fast immer der Upstream-Provider, nicht Tale; die Request-Histogramme des Backends auf `/metrics/backend` zeigen die Zeit in jedem Sprung. ## Die Sandbox-Ebene Sandboxierte Code-Ausführung läuft in `tale-sandbox`, mit `tale-sandbox-egress` als der einzigen Netzwerk-Naht. Die Zwei-Container-Trennung ist Absicht: `tale-sandbox` selbst hat kein ausgehendes Netz; jeder Request, den der sandboxierte Code macht, geht durch `tale-sandbox-egress`, der Cloud-Metadaten und private Adressbereiche auf IP-Ebene blockiert und — wenn der Operator `SANDBOX_EGRESS_ALLOWLIST` setzt — zusätzlich eine Default-Deny-Hostname-Allowlist durchsetzt. Ist der Egress-Container down, scheitert sandboxierter Code, der das Netz braucht, geschlossen mit „Egress denied" — nicht stiller Timeout. -Die Sandbox-Laufzeit trägt Chromium und Playwright, also nutzt das Convex-Backend sie für die Headless-Arbeit, die es im Prozess nicht erledigen kann, erneut: das Rendern einer JavaScript-Seite während eines Web-Crawls und das Verwandeln von generiertem HTML in ein PDF oder Bild. Diese Jobs laufen als ephemere Sandbox-Ausführungen statt als User-Code, reiten aber dieselbe Egress- und Isolations-Naht. Die Sandbox ist der einzige Container, der eher-nicht-vertrauenswürdigen Code läuft (User-gelieferte Fähigkeits-Skripte, Agent-**Code-ausführen**-Aufrufe); der Rest des Stacks läuft den eigenen Code der Plattform. +Die Sandbox-Laufzeit trägt Chromium und Playwright, also nutzt das Backend sie für die Headless-Arbeit, die es im Prozess nicht erledigen kann, erneut: das Rendern einer JavaScript-Seite während eines Web-Crawls und das Verwandeln von generiertem HTML in ein PDF oder Bild. Diese Jobs laufen als ephemere Sandbox-Ausführungen statt als User-Code, reiten aber dieselbe Egress- und Isolations-Naht. Die Sandbox ist der einzige Container, der eher-nicht-vertrauenswürdigen Code läuft (User-gelieferte Fähigkeits-Skripte, Agent-**Code-ausführen**-Aufrufe); der Rest des Stacks läuft den eigenen Code der Plattform. ## Fehler-Modi — wie der Ausfall jedes Containers aussieht -**`tale-proxy` down.** TLS-Handshake scheitert; jeder Client sieht einen Verbindungsfehler. Im Host sind die Plattform- und Convex-Container weiter up — starte Proxy zuerst neu. +**`tale-proxy` down.** TLS-Handshake scheitert; jeder Client sieht einen Verbindungsfehler. Im Host sind die Plattform- und Backend-Container weiter up — starte Proxy zuerst neu. + +**`tale-platform` down.** Browser bekommt 502 vom Proxy; die API arbeitet weiter. Bestehende Browser-Tabs mit gecachten Assets sprechen weiter mit dem Backend und merken es vielleicht erst beim Reload. + +**`backend-api` down.** Browser lädt die UI-Shell, aber nichts wird befüllt, und die öffentliche `/status`-Seite liest `outage` — ihr einziger Probe ist das `/ping` dieser Schicht. Neu zu starten ist sicher: Sessions liegen in Postgres, und der Browser baut seinen Hinweis-Stream wieder auf und lädt beim Reconnect neu. -**`tale-platform` down.** Browser bekommt 502 vom Proxy; die API arbeitet weiter. Bestehende Browser-Tabs mit gecachten Assets sprechen weiter mit Convex über den WebSocket und merken es vielleicht erst beim Reload. +**`backend-worker` down.** Vor dem User bricht nichts — und genau deshalb übersieht man diesen Ausfall leicht. Anfragen werden weiter bedient, aber nichts Aufgeschobenes läuft: Uploads bleiben auf „Indexieren", Automations feuern nicht, geplante Sweeps stehen. Die Arbeit ist nicht verloren — pg-boss hält die Jobs in Postgres, und der Worker arbeitet den Rückstand ab, sobald er zurück ist. Achte auf `tale_backend_jobs{state="created"}`, das auf `/metrics/backend` steigt, denn der Container selbst hat keinen Healthcheck (er bedient kein HTTP), `tale status` wird also immer nur `running` sagen. -**`tale-convex` down.** Browser lädt die UI-Shell, aber nichts wird befüllt. WebSocket-Reconnect schleift. Convex neu zu starten ist sicher — Sessions sind serverseitig; Clients reabonnieren beim Reconnect. +**`tale-db` down.** Jeder Write wird abgelehnt und die meisten Reads mit ihm; die Anmeldung scheitert, und die Job-Warteschlange nimmt keine Arbeit mehr an. Hier degradiert nichts sanft — die Datenbank ist der Speicher der Wahrheit für die Anwendung, die Warteschlange und die Sessions. -**`tale-db` down.** Convex tritt in seinen degradierten Modus: Reads aus dem Cache, Writes werden gepuffert. Lange Ausfälle zeigen sich irgendwann als „Speichern fehlgeschlagen"-Toasts. +**`tale-knowledge-db` down.** Dokument-Ingestion scheitert und die Wissens-Suche liefert leer — Agents, die Wissen abrufen, bekommen eine leere Ergebnismenge und eine Warnung im Ausführungs-Log. Der Rest der App arbeitet weiter; Chats ohne Wissen sind unbetroffen. Den Container neu zu starten räumt das, und laufende Uploads versuchen es beim nächsten Durchlauf erneut. Auf einem Stack, der das Korpus in `tale-db` gefaltet hat, sind dieser und der Ausfall darüber derselbe Ausfall. -**`tale-knowledge-db` down.** Dokument-Ingestion scheitert und die Wissens-Suche liefert leer — Agents, die Wissen abrufen, bekommen eine leere Ergebnismenge und eine Warnung im Ausführungs-Log. Der Rest der App arbeitet weiter; Chats ohne Wissen sind unbetroffen. Den Container neu zu starten räumt das, und laufende Uploads versuchen es beim nächsten Durchlauf erneut. +**`tale-object-store` down.** Eine Datei hochzuladen scheitert, und eine bereits hochgeladene zu öffnen auch — eine Dokumentliste rendert weiter aus der Datenbank, aber jeder Download antwortet 5xx. Chat, Aufgaben und Automations, die keine Dateien anfassen, sind unbetroffen. Eine Organisation mit eigenem S3-Bucket läuft weiter, während der gebündelte Store down ist. -**`tale-sandbox` / `tale-sandbox-egress` down.** **Code-ausführen**-Tool-Aufrufe geben einen Fehler zurück und Fähigkeits-Skripte scheitern. Weil das Convex-Backend Webseiten rendert und Dokumente über die Sandbox-Laufzeit generiert, scheitern auch ein Web-Crawl, der JavaScript-Rendering braucht, und die Dokumentgenerierung geschlossen, solange die Sandbox down ist. Agents, die keines davon nutzen, arbeiten weiter. +**`tale-sandbox` / `tale-sandbox-egress` down.** **Code-ausführen**-Tool-Aufrufe geben einen Fehler zurück und Fähigkeits-Skripte scheitern. Weil das Backend Webseiten rendert und Dokumente über die Sandbox-Laufzeit generiert, scheitern auch ein Web-Crawl, der JavaScript-Rendering braucht, und die Dokumentgenerierung geschlossen, solange die Sandbox down ist. Agents, die keines davon nutzen, arbeiten weiter. -**`tale-sandbox-llm-gateway` down.** Harness-Züge verlieren ihren Pfad zu einem Modell-Provider. Regulärer Chat — der Provider direkt aus Convex aufruft, nicht über das LLM-Gateway — ist unbetroffen. +**`tale-sandbox-llm-gateway` down.** Harness-Züge verlieren ihren Pfad zu einem Modell-Provider. Regulärer Chat — der Provider direkt aus dem Backend aufruft, nicht über das LLM-Gateway — ist unbetroffen. ## Wo das hingehört diff --git a/docs/de/self-hosted/operate/observability/operations.md b/docs/de/self-hosted/operate/observability/operations.md index 57fcbda5b5..3fe5840a78 100644 --- a/docs/de/self-hosted/operate/observability/operations.md +++ b/docs/de/self-hosted/operate/observability/operations.md @@ -12,29 +12,47 @@ Der symptomorientierte Index ist in [Troubleshooting](/de/self-hosted/operate/ob | Signal | Schweregrad | Warum es zählt | | ------------------------------------------- | ----------- | ---------------------------------------------------------- | | `tale-proxy`-Health-Probe scheitert > 1 Min | page | Jeder Benutzer sieht einen Verbindungsfehler | -| `tale-platform` HTTP-5xx-Rate > 5 % | page | Die UI ist für einen relevanten Anteil der Anfragen kaputt | -| `tale-convex` WebSocket-Reconnect-Storm | page | UI lädt, aber keine Daten fliessen | +| `tale-platform`-Health-Probe scheitert | page | Die UI lädt nicht mehr; der Proxy antwortet 502 | +| `backend-api` HTTP-5xx-Rate > 5 % | page | Jede Anfrage der App geht durch diese Schicht | | Postgres-Verbindungen > 80 % des Pools | warn | Die nächste Spitze fängt an zu blockieren | | `db-data`-Volume > 80 % voll | warn | Das operative Postgres geht bei voll auf read-only | | `knowledge-db-data`-Volume > 80 % voll | warn | Ingestion scheitert, wenn die Korpus-Datenbank voll ist | -| `tale-knowledge-db` von convex unerreichbar | warn | Wissens-Suche liefert leer; Ingestion stockt | +| `tale-knowledge-db` unerreichbar | warn | Wissens-Suche liefert leer; Ingestion stockt | +| `tale_backend_jobs{state="created"}` steigt | warn | Der Worker steht; nichts Aufgeschobenes läuft | +| `tale_backend_jobs{state="failed"}` wächst | warn | Jobs brauchen ihre Retries auf | +| `tale-object-store`-Health-Probe scheitert | page | Keine Datei lässt sich hochladen oder öffnen | | Anbieter-Anfrage-Fehlerrate > 20 % | warn | Der Upstream-LLM-Anbieter hat einen schlechten Tag | | Tägliches Backup nicht geschrieben | page | Restore-Drill scheitert zum schlimmsten Zeitpunkt | | TLS-Cert-Erneuerung gescheitert | warn | Erneuert 30 T vor Ablauf — du hast Zeit | -Die ersten zwei Pages sind die wirklich kundenwirksamen. Die warns fangen Trends, bevor sie ins Page-Gebiet kippen. +Die Pages sind die wirklich kundenwirksamen. Die warns fangen Trends, bevor sie ins Page-Gebiet kippen. + +Die 5xx-Rate kommt aus `tale_backend_http_requests_total{status="5xx"}` auf `/metrics/backend`. Die Web-Schicht emittiert keine eigene Request-Reihe — sie liefert statische Dateien aus —, ihre Ausfälle sind also als scheiternder Container-Health-Probe und als 502 am Proxy sichtbar, nicht als Tale-Metrik. ## Log-Signale, nach denen man greppen sollte -Logs kommen über stdout pro Container, aufgefangen vom `json-file`-Driver von Docker. Die vier Phrasen, die konsistent Ärger bedeuten: +Logs kommen über stdout pro Container, aufgefangen vom `json-file`-Driver von Docker. Das Backend stellt seinen eigenen Zeilen `[backend]` voran und loggt überhaupt keine Zeile pro Anfrage — Anfragen sind Metriken, keine Log-Einträge —, ein stilles `backend-api`-Log ist also normal. Die Phrasen, die konsistent Ärger bedeuten: -- `panic` oder `unexpected error` in `tale-convex`-Logs — Convex-Action-Crash. -- `decryption failed` in `tale-platform`-Logs — SOPS-age-Schlüssel-Mismatch mit der Datei auf Platte. +- `[backend] fatal startup error` in `backend-api` oder `backend-worker` — der Prozess kam nicht hoch. Meist eine falsche `DATABASE_URL` oder eine Migration, die nicht anwendbar ist. +- `[backend] task (job ) failed` in `backend-worker` — ein Hintergrund-Job hat geworfen. Wiederholt für denselben Task-Namen ist das Zeichen, dass er seine Retries aufbraucht. +- `[backend] pg-boss error` in `backend-worker` — die Queue-Engine selbst ist unglücklich, was meist heisst: Postgres ist es. +- `decryption failed` in einem Backend-Log — SOPS-age-Schlüssel-Mismatch mit der Datei auf Platte. - `429 Too Many Requests` wiederholt von einem Anbieter — Rate-Limit getroffen, Agents fangen an zu scheitern. -- `connection refused` oder `ECONNREFUSED` zu `knowledge-db` in `tale-convex`-Logs — das Backend erreicht die Korpus-Datenbank nicht; Ingestion und Wissens-Suche scheitern. +- `connection refused` oder `ECONNREFUSED` zu `knowledge-db` in einem Backend-Log — die Korpus-Datenbank ist unerreichbar; Ingestion und Wissens-Suche scheitern. Leite diese als abgeleitete Alerts an deinen Aggregator weiter; die Metric-Endpoints zeigen sie nicht als Gauges. +## Die Job-Warteschlange inspizieren + +Es gibt keine Queue-UI und kein CLI-Subkommando für Jobs. Zwei Türen existieren, und beide genügen. Das Gauge `tale_backend_jobs{state}` auf `/metrics/backend` ist das, worauf du alarmierst. Brauchst du das Detail — welcher Task, welcher Payload — frag die Queue-Tabelle direkt in der Anwendungsdatenbank ab: + +```bash +docker compose exec db psql -U tale -d tale_app \ + -c "SELECT name, state, count(*) FROM pgboss.job GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 20;" +``` + +`name` ist der Task-Identifier, eine Warteschlange pro Identifier. Ein Rückstand, der sich auf einen Namen konzentriert, ist ein hängender Task; ein Rückstand über alle hinweg ist ein gestoppter Worker. + ## Oncall-Checkliste Wenn eine Page landet, folgen die ersten fünf Minuten jedes Mal derselben Form. @@ -58,7 +76,7 @@ Zwei Antwortzeit-Budgets werden als erstklassige Signale verfolgt: interaktive D | Dialog-Eingabe | Mittelwert | ~1 s | 30 Min | `tale_dialog_ttft_seconds` | | Lange Operation | Mittelwert | ~40 s | 6 Std | `tale_long_operation_seconds` | -Jedes Ziel reitet zudem auf dem Plattform-Metrik-Endpoint als `tale_sla_target_seconds{sla,statistic}`, sodass ein Grafana-Panel die Budget-Linie direkt aus Prometheus zeichnet, statt sie fest zu verdrahten. Die zugrundeliegenden Latenz-Serien sind die Convex-Funktions-Ausführungs-Histogramme auf `/metrics/convex`; relabel oder record sie auf die Namen oben, damit die Rules auflösen. Die Plattform liefert die fertigen Recording- und Alerting-Rules unter `/metrics/sla-rules` (hinter demselben Bearer-Token wie die anderen Metrik-Pfade) — hole sie einmal und referenziere die Datei unter `rule_files:`, oder füge das Äquivalent ein: +Jedes Ziel reitet zudem auf den Metrik-Endpoints als `tale_sla_target_seconds{sla,statistic}`, sodass ein Grafana-Panel die Budget-Linie direkt aus Prometheus zeichnet, statt sie fest zu verdrahten. Die Namen in der Spalte `Zugrundeliegende Serie` werden nicht direkt emittiert — leite sie mit einer Recording-Rule aus dem Request-Histogramm des Backends `tale_backend_http_request_duration_seconds` ab, damit die SLA-Aggregation korrekt bleibt, welche Routen-Klasse die Operation auch trägt. Die Plattform liefert die fertigen Recording- und Alerting-Rules unter `/metrics/sla-rules` (hinter demselben Bearer-Token wie die anderen Metrik-Pfade) — hole sie einmal und referenziere die Datei unter `rule_files:`, oder füge das Äquivalent ein: ```yaml groups: diff --git a/docs/de/self-hosted/operate/observability/prometheus-grafana.md b/docs/de/self-hosted/operate/observability/prometheus-grafana.md index 8de007437a..f2f62ef73f 100644 --- a/docs/de/self-hosted/operate/observability/prometheus-grafana.md +++ b/docs/de/self-hosted/operate/observability/prometheus-grafana.md @@ -1,15 +1,15 @@ --- title: Prometheus und Grafana -description: Ein Copy-paste-Stack aus Prometheus und Grafana, der Tales zwei Metrics-Endpoints scrapt — plus ein Starter-Dashboard und eine erste Alert-Regel. +description: Ein Copy-paste-Stack aus Prometheus und Grafana, der Tales Metrics-Endpoints scrapt — plus ein Starter-Dashboard und eine erste Alert-Regel. --- -Das ist das durchgespielte Beispiel hinter [Observability-Konfiguration](/de/self-hosted/configuration/observability-config): ein Paar aus Prometheus und Grafana, das du neben Tale stellst, auf die zwei Bearer-Token-Metrics-Endpoints gerichtet, mit einem Starter-Dashboard und einer Alert-Regel zum Ausbauen. Es ist für selbst hostende Betreiber, die `METRICS_BEARER_TOKEN` bereits gesetzt haben und jetzt Live-Graphen statt eines `curl` gegen `/metrics` wollen. +Das ist das durchgespielte Beispiel hinter [Observability-Konfiguration](/de/self-hosted/configuration/observability-config): ein Paar aus Prometheus und Grafana, das du neben Tale stellst, auf die Bearer-Token-Metrics-Endpoints gerichtet, mit einem Starter-Dashboard und einer Alert-Regel zum Ausbauen. Es ist für selbst hostende Betreiber, die `METRICS_BEARER_TOKEN` bereits gesetzt haben und jetzt Live-Graphen statt eines `curl` gegen `/metrics` wollen. Die Konfigurations-Referenzseite listet die Endpoints und die einzelne Scrape-Stanza; diese Seite stellt den ganzen Stack von Anfang bis Ende auf. Alles hier läuft auf demselben Host wie Tale, also verlässt keine Metrik die Maschine. ## Bevor du startest -Setz `METRICS_BEARER_TOKEN` in deiner `.env` und starte den Proxy neu — ohne ihn geben die zwei Endpoints auf jede Anfrage 401 zurück, und Prometheus zeigt jedes Target als down. Die Endpoints, und was jeder trägt, sind die Tabelle in [Observability-Konfiguration](/de/self-hosted/configuration/observability-config#metrics): `/metrics/platform` und `/metrics/convex` (Letzterer trägt jetzt die In-Process-RAG- und Crawl-Timings), beide von `tale-proxy` über denselben Hostnamen wie die App ausgeliefert. +Setz `METRICS_BEARER_TOKEN` in deiner `.env` und starte den Proxy neu — ohne ihn gibt jeder Endpoint 401 zurück, und Prometheus zeigt jedes Target als down. Die Endpoints, und was jeder trägt, sind die Tabelle in [Observability-Konfiguration](/de/self-hosted/configuration/observability-config#metrics): `/metrics/backend` und `/metrics/platform`, beide von `tale-proxy` über denselben Hostnamen wie die App ausgeliefert. Scrape `/metrics/backend` zuerst — das ist die Schicht, die jede Anfrage bedient. ## Prometheus und Grafana zu deinem Stack hinzufügen @@ -45,22 +45,22 @@ volumes: ## Scrape-Konfiguration -Tales zwei Endpoints teilen sich ein Bearer-Token, also ist die Scrape-Konfiguration die veröffentlichte Stanza, einmal pro Pfad wiederholt. Speicher das als `prometheus.yml` neben dem Override oben und setz deinen Host und dein Token ein — Prometheus liest das Token aus der Datei, also halt sie `chmod 600` und aus der Versionskontrolle raus. +Tales Endpoints teilen sich ein Bearer-Token, also ist die Scrape-Konfiguration die veröffentlichte Stanza, einmal pro Pfad wiederholt. Speicher das als `prometheus.yml` neben dem Override oben und setz deinen Host und dein Token ein — Prometheus liest das Token aus der Datei, also halt sie `chmod 600` und aus der Versionskontrolle raus. ```yaml global: scrape_interval: 30s scrape_configs: - - job_name: tale-platform + - job_name: tale-backend scheme: https - metrics_path: /metrics/platform + metrics_path: /metrics/backend authorization: { credentials: '${METRICS_BEARER_TOKEN}' } static_configs: - targets: ['tale.example.com'] - - job_name: tale-convex + - job_name: tale-platform scheme: https - metrics_path: /metrics/convex + metrics_path: /metrics/platform authorization: { credentials: '${METRICS_BEARER_TOKEN}' } static_configs: - targets: ['tale.example.com'] @@ -73,13 +73,17 @@ scrape_configs: Richte Grafana zuerst auf Prometheus — füg eine Prometheus-Datenquelle unter `http://prometheus:9090` hinzu (Grafana erreicht sie über den Compose-Servicenamen). Bau dann ein Dashboard aus diesen Panels; die ersten drei nutzen Metriken, die immer vorhanden sind, und der Rest bildet die Signale in [Operations](/de/self-hosted/operate/observability/operations) ab. | Panel | Query | Liest sich als | -| --------------- | ---------------------------------------------------- | ------------------------------------------------------------ | -| Targets up | `up{job=~"tale-.*"}` | `1` pro gesundem Endpoint, `0` wenn das Scraping fehlschlägt | -| Platform-Memory | `process_resident_memory_bytes{job="tale-platform"}` | Resident-Memory des platform-Containers | -| Event-Loop-Lag | `nodejs_eventloop_lag_seconds{job="tale-platform"}` | Springt, wenn die Plattform gesättigt ist | -| Convex up | `up{job="tale-convex"}` | Backend-Erreichbarkeit — `0` ist ein Page | - -Der platform-Endpoint trägt Nodes Default-Prozessmetriken (CPU, Memory, Event-Loop-Lag, GC), darum zielen die konkreten Queries oben auf ihn. Der Convex-Endpoint exponiert seine eigene reichere Reihe, inklusive der In-Process-RAG- und Crawl-Timings — öffne ihn einmal (`curl -H "Authorization: Bearer $TOKEN" https://tale.example.com/metrics/convex`), um die exakten Metriknamen deiner Version zu lesen, und füg dann Panels für den Wissens-Ingestion-Durchsatz und die Provider-Fehlerrate aus Operations hinzu. +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Targets up | `up{job=~"tale-.*"}` | `1` pro gesundem Endpoint, `0` wenn das Scraping fehlschlägt | +| Backend-5xx | `sum(rate(tale_backend_http_requests_total{status="5xx"}[5m])) / sum(rate(tale_backend_http_requests_total[5m]))` | Anteil fehlschlagender Anfragen — das kundenwirksame Signal | +| Request-Latenz | `histogram_quantile(0.95, sum by (le) (rate(tale_backend_http_request_duration_seconds_bucket[5m])))` | p95 über die Routen-Klassen; für Details nach `route` aufbrechen | +| Queue-Rückstand | `tale_backend_jobs{state="created"}` | Arbeit, die auf einen Worker wartet — ein steigender Boden heisst: er steht | +| Fehlerhafte Jobs| `tale_backend_jobs{state="failed"}` | Jobs, die ihre Retries aufgebraucht haben | +| Laufende Turns | `tale_backend_generations_inflight` | Chat-Generierungen, die genau jetzt laufen | +| Hint-Streams | `tale_backend_hint_streams_open` | Verbundene Browser; `0` bei Usern online heisst, SSE ist kaputt | +| Event-Loop-Lag | `nodejs_eventloop_lag_seconds{job="tale-platform"}` | Springt, wenn die Web-Schicht gesättigt ist | + +Beide Endpoints tragen Nodes Default-Prozessmetriken (CPU, Memory, Event-Loop-Lag, GC). Die Anwendungs-Reihen oben sind die des Backends, und das `route`-Label ist eine begrenzte Klasse (`/api/app/`, `/api/v1`, `/dav`, `/events`, …) statt des rohen Pfads — ein Panel nach `route` aufzubrechen lässt die Reihen-Anzahl also nie explodieren. Öffne den Endpoint einmal (`curl -H "Authorization: Bearer $TOKEN" https://tale.example.com/metrics/backend`), um die exakten Namen deiner Version zu lesen. ## Eine erste Alert-Regel @@ -97,10 +101,10 @@ groups: summary: 'Tale metrics target {{ $labels.job }} is down' ``` -Die volle Liste, was ein Page wert ist gegenüber was warten kann — platform-5xx-Rate, Postgres-Pool-Sättigung, Erreichbarkeit der Wissensdatenbank, tägliches-Backup-nicht-geschrieben — ist die Signaltabelle in [Operations](/de/self-hosted/operate/observability/operations); übersetz jede Zeile in eine Regel, sobald die passende Reihe auf deinem Dashboard ist. +Die volle Liste, was ein Page wert ist gegenüber was warten kann — Backend-5xx-Rate, Postgres-Pool-Sättigung, Queue-Rückstand, Erreichbarkeit der Wissensdatenbank, tägliches-Backup-nicht-geschrieben — ist die Signaltabelle in [Operations](/de/self-hosted/operate/observability/operations); übersetz jede Zeile in eine Regel, sobald die passende Reihe auf deinem Dashboard ist. ## Wo das hingehört -Diese Seite verwandelt die zwei dokumentierten Metrics-Endpoints in einen laufenden Prometheus-und-Grafana-Stack: ein Compose-Override, eine Zwei-Job-Scrape-Konfiguration, ein Starter-Dashboard und einen Target-down-Alert, den du mit den Operations-Schwellen ausbaust. Halt beide Services an localhost gebunden und das Bearer-Token nicht im Klartext auf der Festplatte, und die ganze Monitoring-Oberfläche bleibt mit Tale auf dem Host. +Diese Seite verwandelt die dokumentierten Metrics-Endpoints in einen laufenden Prometheus-und-Grafana-Stack: ein Compose-Override, eine Zwei-Job-Scrape-Konfiguration, ein Starter-Dashboard und einen Target-down-Alert, den du mit den Operations-Schwellen ausbaust. Halt beide Services an localhost gebunden und das Bearer-Token nicht im Klartext auf der Festplatte, und die ganze Monitoring-Oberfläche bleibt mit Tale auf dem Host. Die Endpoints und das Token, das sie absichert, gehören [Observability-Konfiguration](/de/self-hosted/configuration/observability-config); die Schwellen und die Oncall-Checkliste sind [Operations](/de/self-hosted/operate/observability/operations). Wenn ein Panel rot wird, ist die Symptom-zu-Fix-Suche [Troubleshooting](/de/self-hosted/operate/observability/troubleshooting). diff --git a/docs/de/self-hosted/operate/observability/troubleshooting.md b/docs/de/self-hosted/operate/observability/troubleshooting.md index e288109004..9f244e3db4 100644 --- a/docs/de/self-hosted/operate/observability/troubleshooting.md +++ b/docs/de/self-hosted/operate/observability/troubleshooting.md @@ -26,24 +26,55 @@ Ist der Modus bereits `letsencrypt`, prüf die Proxy-Logs auf ACME-Fehlschläge ## UI lädt, aber keine Daten erscheinen -Die UI-Shell sind statische Assets, von `tale-platform` serviert; alles andere fliesst durch `tale-convex` über einen WebSocket. Wenn der WebSocket sich nicht verbinden kann, lädt die Shell und bleibt leer. Symptome: Spinner, die nie auflösen, „reconnecting"-Toasts, der Chat-Input, der nie eine Nachricht annimmt. +Die UI-Shell sind statische Assets, von `tale-platform` serviert; jede Anfrage dahinter geht an die Backend-Schicht. Kann das Backend nicht antworten, lädt die Shell und bleibt leer. Symptome: Spinner, die nie auflösen, ein Offline-Banner, der Chat-Input, der nie eine Nachricht annimmt. + +Bestätige es mit einer Anfrage — die öffentliche Status-Seite probet genau diese Schicht und braucht kein Login: + +```bash +curl -sS https://dein-host.example.com/status.json +# → {"status":"outage","checkedAt":"...","components":[{"id":"backend","status":"outage"}]} +docker compose logs --tail=200 backend-api +``` + +Das Backend startet wahrscheinlich neu (such nach `[backend] fatal startup error`) oder ist vom Proxy unerreichbar. Starte mit `docker compose restart backend-api` neu — Sessions liegen in Postgres, und der Browser lädt beim Reconnect neu, also ist der Restart sicher. + +## Die UI arbeitet, aktualisiert sich aber nicht mehr von selbst + +Daten erscheinen beim Reload und werden dann alt: Die Änderung einer anderen Person taucht nie auf, ein fertiger Run sagt weiter „läuft". Der Browser hält eine langlebige `GET /events`-Verbindung zu `backend-api`, die Invalidierungs-Hinweise trägt, und bricht sie ab, gibt es keinen Fehler zu sehen — die Seite erfährt einfach nicht mehr, dass sich etwas geändert hat. ```bash -docker compose logs --tail=200 tale-convex +curl -sS -H "Authorization: Bearer $METRICS_BEARER_TOKEN" \ + https://dein-host.example.com/metrics/backend | grep hint_streams +# tale_backend_hint_streams_open 0 ``` -Der Convex-Container startet wahrscheinlich neu (such nach `panic` in den Logs) oder ist vom Proxy unerreichbar. Starte mit `docker compose restart tale-convex` neu — Sessions sind serverseitig, und Clients reabonnieren beim Reconnect, also ist der Restart sicher. +Null offene Streams bei angemeldeten Usern heisst, die Bahn wird gekappt. Die übliche Ursache ist etwas zwischen Browser und Backend, das eine streamende Antwort puffert oder timeoutet — ein Firmen-Proxy, ein CDN oder ein zusätzlicher Reverse-Proxy vor Caddy. Tales eigener Proxy schaltet das Puffern auf diesem Pfad ab; was du davor stellst, muss dasselbe tun. ## Uploads stecken in „indexing" -Die Dokument-Ingestion läuft im Convex-Backend und schreibt die extrahierten Chunks und Embeddings in die Datenbank des Wissens-Korpus. Ein langer „indexing"-Zustand bedeutet entweder, dass das Backend `tale-knowledge-db` nicht erreicht oder dass die Datei selbst nicht extrahiert werden konnte. Prüf zuerst die Convex-Logs und die Korpus-Datenbank: +Die Dokument-Ingestion ist ein Hintergrund-Job. `backend-worker` nimmt ihn auf, extrahiert den Text, embeddet ihn und schreibt Chunks und Embeddings in die Datenbank des Wissens-Korpus. Ein langer „indexing"-Zustand hat deshalb drei Verdächtige, in dieser Reihenfolge: Es läuft kein Worker, der Worker erreicht die Korpus-Datenbank nicht, oder die Datei selbst konnte nicht extrahiert werden. + +Fang bei der Warteschlange an, denn ein gestoppter Worker sieht genau aus wie ein langsamer: + +```bash +docker compose exec db psql -U tale -d tale_app \ + -c "SELECT name, state, count(*) FROM pgboss.job WHERE name LIKE 'rag.%' GROUP BY 1, 2;" +docker compose logs --tail=200 backend-worker | grep -iE "knowledge|ingest|embed|rag" +docker compose ps knowledge-db +``` + +Ein Rückstand in `created` ohne `active`-Zeilen heisst, der Worker ist down — starte ihn, und er arbeitet den Rückstand von selbst ab. Zeigen die Logs Verbindungsfehler zu `knowledge-db`, starte die Korpus-Datenbank neu (`docker compose restart knowledge-db`); die Ingestion versucht es beim nächsten Durchlauf erneut, Uploads müssen also nicht erneut eingereicht werden. Ist die Warteschlange leer und die Datenbank healthy, aber ein Upload steckt, ist die Datei selbst der Verdächtige — beschädigte PDFs und passwortgeschützte Dokumente landen in einem Fehlzustand und brauchen Löschung und Re-Upload. + +## Uploads oder Downloads scheitern komplett + +Ein Upload, der nie startet, oder ein Dokument, das gelistet wird, beim Öffnen aber 5xx liefert, zeigt auf den Blob-Store, nicht auf die Datenbank. Jede Datei liegt in einem S3-kompatiblen Store, und der Browser überträgt sie direkt über eine präsignierte URL, die der Proxy weiterleitet. ```bash -docker compose logs --tail=200 tale-convex | grep -iE "knowledge|ingest|embed" -docker compose ps tale-knowledge-db +docker compose ps object-store +docker compose logs --tail=100 object-store ``` -Zeigen die Logs Verbindungsfehler zu `knowledge-db`, starte die Korpus-Datenbank neu (`docker compose restart tale-knowledge-db`); die Ingestion versucht es beim nächsten Durchlauf erneut, Uploads müssen also nicht erneut eingereicht werden. Ist die Datenbank healthy, aber ein bestimmter Upload steckt, ist die Datei selbst der Verdächtige — beschädigte PDFs und passwortgeschützte Dokumente landen in einem Fehlzustand und brauchen Löschung und Re-Upload. +Ist der Store healthy, ist der nächste Verdächtige der Ursprung der präsignierten URL: Sie ist gegen die Adresse signiert, die der Browser benutzt — ein Deployment, dessen öffentliche URL sich geändert hat, ohne dass `OBJECT_STORE_PUBLIC_ENDPOINT` (oder `SITE_URL`) mitgezogen ist, signiert also URLs, die der Browser nicht erreicht. Eine Organisation mit eigenem Bucket ist ein separater Pfad — prüf, dass ihre CORS-Policy deinen Ursprung für `GET`, `PUT` und `HEAD` erlaubt, denn der Verbindungstest in der App läuft serverseitig und fängt das nicht. ## Chat-Antworten hören mitten im Stream auf @@ -57,14 +88,14 @@ Ein `429` ist der häufige Fall. Entweder trifft das Budget der Org das Rate-Lim ## Speichern scheitert mit „saving failed"-Toast -Der Convex-Container konnte nicht in Postgres schreiben. Entweder ist `tale-db` down oder seine Platte ist voll: +Das Backend konnte nicht in Postgres schreiben. Entweder ist `tale-db` down oder seine Platte ist voll: ```bash -docker compose ps tale-db +docker compose ps db docker compose exec db df -h /var/lib/postgresql/data ``` -Eine Platte bei 100 % ist der Fehler, der die meisten überraschten Gesichter erzeugt. Schaff Platz, starte `tale-db` neu, und die gepufferten Writes flushen. Hat die Platte Platz, ist der Verdächtige Verbindungs-Pool-Erschöpfung oder ein Lock — starte `tale-convex` neu, um den Pool zu räumen. +Eine Platte bei 100 % ist der Fehler, der die meisten überraschten Gesichter erzeugt. Schaff Platz und starte `db` neu. Beachte, was eine volle Platte hier kostet: Die Datenbank hält die Anwendungsdaten, die Sessions und die Job-Warteschlange, eine verweigerte Schreiboperation stoppt also auch die Hintergrundarbeit. Hat die Platte Platz, ist der Verdächtige Verbindungs-Pool-Erschöpfung oder ein Lock — starte `backend-api` neu, um den Pool zu räumen. ## „Run code"-Tool scheitert mit „egress denied" diff --git a/docs/de/self-hosted/operate/security/cryptography.md b/docs/de/self-hosted/operate/security/cryptography.md index 73ce2a32d7..aa4bd94d53 100644 --- a/docs/de/self-hosted/operate/security/cryptography.md +++ b/docs/de/self-hosted/operate/security/cryptography.md @@ -15,9 +15,9 @@ Tale verschlüsselt zwei Klassen von Secrets im Ruhezustand, mit zwei verschiede **Anwendungsverschlüsselte Felder** — OAuth-Connector-Tokens und ähnliche Credentials in der Datenbank — werden mit **AES-256-GCM** über ein kompaktes JWE (`alg: dir`, `enc: A256GCM`) verschlüsselt. Der 32-Byte-Schlüssel kommt aus `ENCRYPTION_SECRET` (base64) oder `ENCRYPTION_SECRET_HEX` (hex); die Plattform verweigert den Start des Verschlüsselungspfads mit einem Schlüssel, der nicht exakt 32 Byte hat. -Der Convex-Datenspeicher und die Postgres-Volumes werden vom Host geschützt: betreibe sie auf einem verschlüsselten Dateisystem (LUKS oder die Volume-Verschlüsselung deines Cloud-Anbieters). Tale speichert Credentials nicht im Klartext — ein Provider-Schlüssel oder OAuth-Token ist entweder SOPS-verschlüsselt auf der Festplatte oder AES-256-GCM-verschlüsselt in der Datenbank, nie im Klartext geschrieben. +Die Postgres-Volumes und der Blob-Store werden vom Host geschützt: betreibe sie auf einem verschlüsselten Dateisystem (LUKS oder die Volume-Verschlüsselung deines Cloud-Anbieters). Tale speichert Credentials nicht im Klartext — ein Provider-Schlüssel oder OAuth-Token ist entweder SOPS-verschlüsselt auf der Festplatte oder AES-256-GCM-verschlüsselt in der Datenbank, nie im Klartext geschrieben. -**Kunden-PII und Anwendungsdaten** — Namen, E-Mail- und Postadressen, Gesprächsinhalte — sind im Ruhezustand durch dieselben Schichten geschützt, die die Datenbank als Ganzes schützen: Convex' Verschlüsselung im Ruhezustand, TLS 1.3 bei der Übertragung und zeilenbasierte Sicherheitsregeln (RLS), die jeden Lesezugriff auf die Organisation des Aufrufers begrenzen. +**Kunden-PII und Anwendungsdaten** — Namen, E-Mail- und Postadressen, Gesprächsinhalte — sind durch die Schichten geschützt, die die Datenbank als Ganzes schützen: das verschlüsselte Host-Dateisystem im Ruhezustand, TLS 1.3 bei der Übertragung und ein Organisations-Scope, den das Backend auf jeden Lesezugriff legt. Sei beim Ersten davon genau: Postgres legt diese Zeilen unverschlüsselt ab, die Verschlüsselung im Ruhezustand ist also die des Volumes, nicht die der Datenbank. Wenn deine Vorgaben verlangen, dass die Datenbank selbst Chiffrat hält, ist das eine Entscheidung über Managed Postgres oder das Dateisystem, die du unterhalb von Tale triffst. Anwendungsseitige Feldverschlüsselung ist gezielt für Secrets gemacht — Provider-Schlüssel und OAuth-Tokens, die einmal geschrieben und von einem einzigen Code-Pfad gelesen werden. PII ist anders: Sie wird gefiltert, sortiert und über den exakten Wert nachgeschlagen, und die Kundentabelle ist nach Organisation und E-Mail indiziert. Diese Spalten auf Feldebene zu verschlüsseln würde Gleichheitsabfragen und indizierte Suche brechen — sofern nicht mit einem suchbaren Hash-Verfahren kombiniert, das genau die Gleichheit preisgibt, die es verbergen soll — bei zusätzlichen Kosten für die Schlüsselrotation und ohne Schutz, den die verschlüsselte Host-Festplatte unter der Anwendung gegen ein gestohlenes Volume nicht ohnehin bietet. diff --git a/docs/de/self-hosted/operate/upgrades.md b/docs/de/self-hosted/operate/upgrades.md index ae77fe9f6e..343baa30a6 100644 --- a/docs/de/self-hosted/operate/upgrades.md +++ b/docs/de/self-hosted/operate/upgrades.md @@ -46,14 +46,14 @@ tale update --dry-run `tale deploy` macht den eigentlichen Rolling-Restart und deployt immer die eigene Version des CLI — die dank der Angleichung die Version ist, die dein Workspace aufzeichnet. Es sortiert die Services in drei Tiers: - **App-Tier** — `platform` — rollt bei **jedem** Deploy ohne Downtime (Blue-Green: die neue Farbe startet neben der alten, Healthchecks bestehen, der Traffic kippt, die alte Farbe drainet). -- **Backend und Compute** — `convex`, `sandbox`, `sandbox-egress` — rollen ebenfalls bei jedem Deploy, sodass sie nie gegenüber `platform` versions-skewen. Jeder ist ein einzelner Container, der sich **in-place** neu erstellt, wenn sich sein Image tatsächlich geändert hat; der Deploy drainet zuerst die laufende Arbeit (Chat-Generierungen bei `convex`, Agent-Runs bei `sandbox`), damit der kurze Neustart keine lebende Anfrage abschneidet. -- **Stop-gegateter Tier** — `db`, `proxy` — bleibt standardmäßig **laufend und unangetastet** (Postgres oder den Proxy neu zu erstellen ist eine kurze Ausfallzeit, die du bei einem Routine-Roll nicht willst). Mit `--stop` aktualisierst du sie; der Deploy warnt und nennt sie, wenn er sie überspringt. +- **Backend und Compute** — `backend-api`, `backend-worker`, `sandbox`, `sandbox-egress`, `sandbox-llm-gateway` — rollen ebenfalls bei jedem Deploy, sodass sie nie gegenüber `platform` versions-skewen. Die zwei Backend-Services liefern *dasselbe Image* wie `platform` und teilen dessen Wire-Contracts, ein Skew ist also gar keine Option. Jeder erstellt sich **in-place** neu, wenn sich sein Image tatsächlich geändert hat; der Deploy drainet zuerst die laufende Arbeit (Chat-Generierungen beim Backend, Agent-Runs bei der Sandbox), damit der kurze Neustart keine lebende Anfrage abschneidet. +- **Stop-gegateter Tier** — `db`, `object-store`, `proxy` — bleibt standardmäßig **laufend und unangetastet** (Postgres, den Blob-Store oder den Proxy neu zu erstellen ist eine kurze Ausfallzeit, die du bei einem Routine-Roll nicht willst). Mit `--stop` aktualisierst du sie; der Deploy warnt und nennt sie, wenn er sie überspringt. ```bash -# Nach tale update die Container passend rollen (App-Tier + convex) +# Nach tale update die Container passend rollen (App-Tier + Backend + Sandbox) tale deploy -# Auch db/proxy aktualisieren (kurze Downtime, während sie neu erstellt werden) +# Auch db/object-store/proxy aktualisieren (kurze Downtime beim Neuerstellen) tale deploy --stop # Nur bestimmte Services rollen diff --git a/docs/de/self-hosted/overview.md b/docs/de/self-hosted/overview.md index 238bbe4508..b562b395e0 100644 --- a/docs/de/self-hosted/overview.md +++ b/docs/de/self-hosted/overview.md @@ -1,54 +1,63 @@ --- title: Selbst gehostete Architektur -description: Acht Container, eine compose-Datei, zwei Postgres-Datenbanken. Diese Seite vermittelt das mentale Modell, was jeder Container tut, wo Daten auf dem Storage liegen und welche Secrets beim ersten Boot zählen. +description: Elf Container in einer compose-Datei, davon zwei Postgres-Datenbanken und ein S3-kompatibler Blob-Store. Diese Seite vermittelt das mentale Modell, was jeder Container tut, wo Daten auf dem Storage liegen und welche Secrets beim ersten Boot zählen. --- -Eine Tale-Instanz besteht aus acht Containern hinter einem Caddy-Proxy, die mit zwei Postgres-Datenbanken sprechen — einer operativen, einer für den Wissens-Korpus; zwei davon sind Sandbox-Container an der Seite für Code-Ausführung. Die compose-Datei ist der Vertrag — was läuft, was exponiert ist, was gemountet ist. Diese Seite vermittelt das mentale Modell, sodass die Install-, Konfigurations- und Betriebsseiten es nicht erneut erklären müssen. +Eine Tale-Instanz besteht aus elf Containern hinter einem Caddy-Proxy, die mit zwei Postgres-Datenbanken sprechen — einer operativen, einer für den Wissens-Korpus — und mit einem S3-kompatiblen Blob-Store; zwei davon sind Sandbox-Container an der Seite für Code-Ausführung. Die compose-Datei ist der Vertrag — was läuft, was exponiert ist, was gemountet ist. Diese Seite vermittelt das mentale Modell, sodass die Install-, Konfigurations- und Betriebsseiten es nicht erneut erklären müssen. Lies das, bevor du `docker compose up` ausführst. Komm zurück, wenn du einen Ausfall debuggst und wissen musst, welches Container-Log du zuerst öffnen solltest. -## Die acht Container +## Die elf Container -**tale-proxy** ist Caddy am Rand. Er terminiert TLS, leitet alles unter `/` an den Plattform-Container und alles unter `/api/` und die Convex-Pfade an den Convex-Container weiter. Health-Checks leben hier. +**tale-proxy** ist Caddy am Rand. Er terminiert TLS, liefert HTML und statische Assets aus dem Plattform-Container und leitet alles unter `/api/` — plus `/events`, `/dav` und die Maschinen-API — an das Backend weiter. Er veröffentlicht außerdem den Bucket-Pfad des Blob-Stores, damit präsignierte Upload- und Download-URLs im Browser funktionieren. Health-Checks leben hier. -**tale-platform** ist der React + TanStack Start-Server. Er rendert die UI, liefert statische Assets aus und ist der einzige Container, der dem Browser exponiert ist. Er hält keinen Geschäfts-State — alles, was persistieren muss, spricht mit Convex. +**tale-platform** ist der React + TanStack Start-Server. Er rendert die UI, liefert statische Assets aus und terminiert den Screencast-Socket für die Live-Browser-Ansicht. Er hält keinen Geschäfts-State und erreicht keine Datenbank — alles, was persistiert, geht durch das Backend. -**tale-convex** ist das Backend: die Actions, Queries, Mutations und die WebSocket-Schicht, die die UI abonniert. Provider-Keys, Agent-Definitionen, Workflow-Läufe, Audit-Logs — alles davon lebt hier. Es läuft auch die Wissens-Arbeit im Prozess — Dokument-Ingestion, Web-Crawling, RAG-Suche und Dokumentgenerierung sind Convex-Node-Actions, keine separaten Services. Die Headless-Arbeit, die diese Jobs brauchen (eine Webseite rendern, HTML in ein PDF oder Bild verwandeln), wird an die Sandbox-Laufzeit delegiert, die ohnehin schon Chromium und Playwright mitbringt. +**backend-api** ist das Anwendungs-Backend: ein Node-Prozess mit einer Hono-App, der jede Tür bedient, die die UI und die Maschinen-API brauchen — Anmeldung, App-API, WebDAV, den Live-Update-Stream. Provider-Keys, Agent-Definitionen, Workflow-Läufe und Audit-Logs liegen dahinter. Die Wissens-*Suche* läuft in diesem Prozess und fragt die Korpus-Datenbank direkt ab, nicht über einen separaten Retrieval-Dienst. -**tale-db** ist das operative Postgres (ParadeDB). Es hält die Daten des Convex-Backends — Agents, Runs, das Audit-Log — und ist einer der zwei zustandsbehafteten Container, die für Backups zählen. +**backend-worker** ist dasselbe Image in der Worker-Rolle. Er fährt die Hintergrund-Jobs — Dokument-Ingestion und Embedding, Web-Crawling, Automation-Runs, Retention-Sweeps — aus einer pg-boss-Warteschlange, die in der Anwendungsdatenbank liegt: Ein Job committet damit in derselben Transaktion wie der Write, der ihn geplant hat. Die Headless-Arbeit, die einige dieser Jobs brauchen (eine Webseite rendern, HTML in ein PDF oder Bild verwandeln), geht an die Sandbox-Laufzeit, die ohnehin schon Chromium und Playwright mitbringt. Der Worker bedient kein HTTP. -**tale-knowledge-db** ist das Postgres des Wissens-Korpus (ParadeDB), die `tale_knowledge`-Datenbank mit zwei Schemata: `private_knowledge` (Chunks hochgeladener Dokumente, Embeddings, der BM25-Index, der semantische Cache) und `public_web` (gecrawlte Webseiten). Es ist von `tale-db` getrennt, damit der Korpus — der datenresidenz-sensible Speicher — sich für sich allein verlagern oder ersetzen lässt. Das Convex-Backend verbindet sich direkt mit ihm; nichts sonst tut das. +**tale-db** ist das operative Postgres (ParadeDB). Es hält die `tale_app`-Datenbank — Agents, Runs, Sessions, das Audit-Log und die Job-Warteschlange — und das Backend legt seine Schema-Migrationen beim Boot darauf an, unter einem Advisory Lock, sodass ein rollender Deploy genau einmal migriert. + +**tale-object-store** ist der Blob-Store: eine S3-kompatible MinIO-Instanz mit jedem hochgeladenen Dokument, Chat-Anhang, Audio und generierten Medium. S3-kompatibler Speicher ist das einzige Blob-Backend, ein Deployment ohne einen lehnt also jeden Upload ab. Er ist nur intern erreichbar; das Backend signiert präsignierte URLs, und der Proxy leitet sie weiter. + +**tale-knowledge-db** ist das Postgres des Wissens-Korpus (ParadeDB), die `tale_knowledge`-Datenbank mit zwei Schemata: `private_knowledge` (Chunks hochgeladener Dokumente, Embeddings, der BM25-Index, der semantische Cache) und `public_web` (gecrawlte Webseiten). Dass er über einen eigenen Connection-String adressierbar bleibt, ist genau das, was den Korpus — den datenresidenz-sensiblen Speicher — für sich allein verlagerbar oder ersetzbar macht. Auf einem Single-Host-`tale deploy`-Stack ist er in `tale-db` gefaltet, das den Netzwerk-Alias `knowledge-db` trägt, sodass der Connection-String in beiden Fällen auflöst. **tale-sandbox-llm-gateway** ist das LLM-Gateway für Harness-Züge. Es ist der einzige Pfad von einem sandboxierten Harness zu einem Modell-Provider; die Plattform stellt es bereit und prägt Per-Session-Keys. -**tale-sandbox** und **tale-sandbox-egress** führen sandboxierten Code für das **Code-ausführen**-Tool und Fähigkeits-Skripte aus und dienen als die Headless-Browser-Laufzeit, die das Convex-Backend für Web-Render und Dokumentgenerierung aufruft. Der Egress-Container ist der einzige Netzwerkweg, den die Sandbox hat. Egress ist standardmäßig offen — sandboxierter Code erreicht jeden öffentlichen Host über HTTPS, Cloud-Metadaten und private Adressbereiche bleiben auf IP-Ebene blockiert. Einschränken kannst du das mit `SANDBOX_EGRESS_ALLOWLIST` auf eine Hostname-Allowlist; die Anleitung steht in [Hardening](/de/self-hosted/operate/security/hardening). +**bgutil-provider** ist ein Drittanbieter-Helfer für die Video-Link-Aufnahme: Er stellt die Tokens aus, die YouTube verlangt, bevor ein Transkript geholt werden kann. Es ist das einzige Image im Stack, das Tale nicht selbst baut, es ist nur intern erreichbar, und ein Deployment, das nie Video-Links aufnimmt, kann es stoppen, ohne dass sonst etwas leidet. + +**tale-sandbox** und **tale-sandbox-egress** führen sandboxierten Code für das **Code-ausführen**-Tool und Fähigkeits-Skripte aus und dienen als die Headless-Browser-Laufzeit, die das Backend für Web-Render und Dokumentgenerierung aufruft. Der Egress-Container ist der einzige Netzwerkweg, den die Sandbox hat. Egress ist standardmäßig offen — sandboxierter Code erreicht jeden öffentlichen Host über HTTPS, Cloud-Metadaten und private Adressbereiche bleiben auf IP-Ebene blockiert. Einschränken kannst du das mit `SANDBOX_EGRESS_ALLOWLIST` auf eine Hostname-Allowlist; die Anleitung steht in [Hardening](/de/self-hosted/operate/security/hardening). ## Daten auf dem Storage -Vier Volumes überleben ein `docker compose down`: +Fünf Volumes überleben ein `docker compose down`: -- `db-data` — das Datenverzeichnis des operativen Postgres: die Datenbank hinter Agents, Runs und dem Audit-Log. -- `knowledge-db-data` — das Datenverzeichnis des Postgres für den Wissens-Korpus: Dokument-Chunks, Embeddings, die Such-Indizes und gecrawlte Webseiten. Sichert separat von `db-data`, weil es eine eigene Datenbank ist. +- `db-data` — das Datenverzeichnis des operativen Postgres: die Datenbank hinter Agents, Runs, Sessions, dem Audit-Log und der Job-Warteschlange. +- `knowledge-db-data` — das Datenverzeichnis des Postgres für den Wissens-Korpus: Dokument-Chunks, Embeddings, die Such-Indizes und gecrawlte Webseiten. Getrennt von `db-data`, weil es eine eigene Datenbank ist, und auf einem Stack, der den Korpus in `tale-db` gefaltet hat, gar nicht vorhanden. +- `object-store-data` — der Blob-Store: jedes hochgeladene Dokument, jeder Chat-Anhang, jede Audiodatei und jedes generierte Medium. +- `convex-data` — der Org-Config-Baum: Agents, Automations, Connectors, Anbieter, Skills, Governance-Policies, SSO-Verbindungen, Branding. Der Name ist historisch und bleibt bewusst, damit die Abschaltung des Convex-Backends niemanden zwingt, ein Volume nur für eine Umbenennung zu migrieren. - `backups` — checksummengesicherte Volume-Snapshots, geschrieben von `tale backup` und automatisch vor migrierenden Deploys; [Backups und Restore](/de/self-hosted/operate/backups-and-restore) ist der Drill. -- Der Object-Store-Mount von Convex — hochgeladene Dateien, generierte Dokumente, exportierte Bundles. -Alles andere ist flüchtig. Container können ohne Datenverlust ersetzt werden, solange die Volumes überleben. +Auf `object-store-data` musst du achten: Ein `tale backup`-Snapshot enthält es **nicht**, hochgeladene Dateien brauchen also ihren eigenen Platz in deinem Backup-Job. Alles andere ist flüchtig. Container können ohne Datenverlust ersetzt werden, solange die Volumes überleben. ## Provider-Secrets und die SOPS-Schicht -Provider-Keys (OpenAI, Anthropic, Azure, Ollama, etc.) leben auf dem Storage in einem `providers/`-Verzeichnis, das in den Plattform-Container gemountet wird. Jeder Provider hat eine `.json` und eine `.secrets.json`; die Secrets-Datei ist mit SOPS und der Variable [`SOPS_AGE_KEY`](/de/self-hosted/configuration/environment-reference) verschlüsselt. +Config-Datei-Secrets — die Secrets-Sidecars der Anbieter, die Passwörter der Wissens- und Objektspeicher-Verbindungen, die Secrets der Deployment-Config selbst — leben auf dem Storage im Org-Config-Baum, verschlüsselt mit SOPS und der Variable [`SOPS_AGE_KEY`](/de/self-hosted/configuration/environment-reference). Die Backend-Container mounten diesen Baum read-write und sind die einzigen Prozesse, die den age-Schlüssel halten; die Web-Schicht mountet dasselbe Volume read-only für Branding-Bilder und entschlüsselt nie etwas. -Diese Trennung existiert aus zwei Gründen. Einen Provider-Key zu rotieren ist eine Datei zu bearbeiten, nicht die Plattform neu zu starten; die verschlüsselte Datei zu sichern ist sicher, sie neben der Infrastruktur zu committen. Der Klartext-Modus (kein SOPS, Secrets in Klartext) wird für streng kontrollierte Umgebungen unterstützt, wo der Storage selbst at-rest verschlüsselt ist. +Diese Trennung existiert aus zwei Gründen. Ein Secret zu rotieren ist eine Datei zu bearbeiten, nicht die Plattform neu zu starten; die verschlüsselte Datei zu sichern ist sicher, sie neben der Infrastruktur zu committen. Der Klartext-Modus (kein SOPS, Secrets in Klartext) wird für streng kontrollierte Umgebungen unterstützt, wo der Storage selbst at-rest verschlüsselt ist. ## Auth und Sessions -Sign-in ist Better Auth, das im Convex-Container läuft. Vier Sign-in-Modi sind dabei: lokales Passwort, Microsoft Entra (OAuth/OIDC), generisches OIDC und Trusted Headers (der Reverse-Proxy liefert die Identität). Der Plattform-Container liest das Cookie, übergibt es an Convex, und Convex entscheidet, was die Session tun darf, basierend auf der Rolle des Benutzers und der Berechtigungs-Matrix pro Ressource, die in [Mitglieder und Rollen](/de/platform/admin/members-and-roles) dokumentiert ist. +Sign-in ist Better Auth, das im Backend läuft. Vier Sign-in-Modi sind dabei: lokales Passwort, Microsoft Entra (OAuth/OIDC), generisches OIDC und Trusted Headers (der Reverse-Proxy liefert die Identität). Der Proxy schickt alles unter `/api/auth/` direkt an `backend-api`, die Web-Schicht steht also gar nicht im Anmelde-Pfad: Der Browser hält ein Session-Cookie, das Backend löst es bei jeder Anfrage auf, und das Backend entscheidet aus der Rolle des Benutzers und der Berechtigungs-Matrix pro Ressource, was die Session tun darf — dokumentiert in [Mitglieder und Rollen](/de/platform/admin/members-and-roles). Sessions liegen in Postgres, und deshalb meldet ein Neustart eines Backend-Containers niemanden ab. Die [Authentifizierungs-Referenz](/de/self-hosted/configuration/authentication) behandelt die Umgebungsvariablen und die Trade-offs pro Modus. ## Wenn du Single-Host hinter dir lässt -Die Standard-compose-Datei betreibt alle acht Container auf einem Host. Die Architektur ist single-tenant: nichts im Design teilt Arbeit über Hosts hinweg. Das Erste, was du ohne Re-Architektur von der Box bewegen kannst, ist der Wissens-Korpus — `tale-knowledge-db` ist ein eigenständiges Postgres, also ist es eine Connection-String-Änderung, es auf verwaltete Infrastruktur zu zeigen (für Kapazität oder eine Residenz-Anforderung), behandelt in [Datenresidenz](/de/self-hosted/configuration/data-residency). Die Convex-Schicht ist immer noch Single-Instance; horizontale Skalierung des Backends ist kein v1-Feature. +Die Standard-compose-Datei betreibt alle elf Container auf einem Host. Das Erste, was du ohne Re-Architektur von der Box bewegen kannst, ist der Wissens-Korpus — er wird über einen eigenen Connection-String adressiert, ihn auf verwaltete Infrastruktur zu zeigen (für Kapazität oder eine Residenz-Anforderung) ist also eine Änderung an `KNOWLEDGE_DATABASE_URL`, behandelt in [Datenresidenz](/de/self-hosted/configuration/data-residency). Der Blob-Store bewegt sich genauso: Du richtest die Objektspeicher-Verbindung des Deployments auf einen Bucket, der dir gehört. + +Die Backend-Schicht skaliert nach außen, nicht nach oben. `backend-api` und `backend-worker` nehmen beide `--scale`: Jeder API-Container pollt die Hinweis-Outbox und fächert Updates an seine eigenen Clients aus, es gibt also keine Koordination zwischen Containern und keine Sticky Sessions zu arrangieren, und jeder Worker konkurriert um dieselbe pg-boss-Warteschlange. Einzeln bleibt Postgres — ein Primary, und der Blob-Store daneben. ## Wo das hingehört diff --git a/docs/en/cloud/data-residency.md b/docs/en/cloud/data-residency.md index 90c4c3302e..2225de7765 100644 --- a/docs/en/cloud/data-residency.md +++ b/docs/en/cloud/data-residency.md @@ -9,7 +9,7 @@ The default region for new Cloud orgs is Switzerland. Switching region after sig ## A worked example — one chat round-trip -The user in Zürich opens Chat and sends "summarise the latest customer call". The request hits Tale's edge in the chosen region, lands on `tale-platform`, which calls into `tale-convex` (the backend), reads knowledge from the corpus database when the agent's knowledge tool asks for it, and emits an outbound call to the provider behind the model the sender picked. Knowledge retrieval runs inside the Convex backend — it queries the corpus database directly, with no separate retrieval service in the path. The model provider returns tokens; Tale streams them back across the same path. The reply and citations land in the operational database, the corpus stays in the knowledge database, and both are replicated within the region. +The user in Zürich opens Chat and sends "summarise the latest customer call". The request hits Tale's edge in the chosen region, which serves the page from the web tier and routes the message itself to the application backend. The backend reads knowledge from the corpus database when the agent's knowledge tool asks for it, and emits an outbound call to the provider behind the model the sender picked. Knowledge retrieval runs inside that same backend process — it queries the corpus database directly, with no separate retrieval service in the path. The model provider returns tokens; the backend streams them back to the browser. The reply and citations land in the operational database, the corpus stays in the knowledge database, any file the turn produced lands in the region's object store, and all three are replicated within the region. Two arrows cross the regional boundary in this trip: the call to the model provider (always external) and any sub-processor the agent's tools triggered (web fetch, OneDrive read, MCP server in another region). Everything else stays in region. diff --git a/docs/en/develop/api-reference.md b/docs/en/develop/api-reference.md index 16e4e442cd..9aa56b2d78 100644 --- a/docs/en/develop/api-reference.md +++ b/docs/en/develop/api-reference.md @@ -240,7 +240,7 @@ curl -sS "https://your-host.example.com/api/v1/tasks/" \ # → 200 { "task": { "id": "", "title": "...", "status": "in_progress", "externalId": "case-991", "labels": [], ... } } ``` -And fetch the results. What the automation reported lands in the task's discussion; what it filed lands as files in the quarter's folder — both readable through the door. The content endpoint streams a Convex-stored blob directly and answers a **302** to a short-lived presigned URL on an organization with its own object storage, so follow redirects: +And fetch the results. What the automation reported lands in the task's discussion; what it filed lands as files in the quarter's folder — both readable through the door. The content endpoint never streams bytes itself: every file lives in object storage, so it always answers a **302** to a short-lived presigned URL. Follow redirects, and treat that URL as a credential — it grants the bytes to whoever holds it until it expires: ```bash curl -sS "https://your-host.example.com/api/v1/tasks//comments" \ diff --git a/docs/en/develop/status-page.md b/docs/en/develop/status-page.md index ea4c85dfb2..b578faa295 100644 --- a/docs/en/develop/status-page.md +++ b/docs/en/develop/status-page.md @@ -21,11 +21,11 @@ The RSS feed carries every state change — open, update, resolved — for every | Service | What it covers | When it goes red | | ---------- | ---------------------------------------------------------------------------------- | ------------------------------------------------ | -| `platform` | The TanStack Start + Convex application — agents, workflows, connectors, UI. | UI unreachable; API returns 5xx; auth broken. | -| `rag` | The Python FastAPI document-processing service — indexing, retrieval. | Document uploads stall; retrieval is empty. | -| `crawler` | The Crawl4AI web-extraction service — used by document ingest and Tavily fallback. | Web-pulled documents fail; deep research stalls. | -| `proxy` | The Caddy edge — TLS termination, HTTP routing. | All Tale Cloud traffic affected. | -| `db` | TimescaleDB — durable state for the Convex layer and platform metadata. | Writes refused; the platform row also goes red. | +| `platform` | The TanStack Start UI server and the Node backend behind it — agents, workflows, connectors, UI. | UI unreachable; API returns 5xx; auth broken. | +| `rag` | The Python FastAPI document-processing service — indexing, retrieval. | Document uploads stall; retrieval is empty. | +| `crawler` | The Crawl4AI web-extraction service — used by document ingest and Tavily fallback. | Web-pulled documents fail; deep research stalls. | +| `proxy` | The Caddy edge — TLS termination, HTTP routing. | All Tale Cloud traffic affected. | +| `db` | Postgres — durable application state and the background job queue. | Writes refused; the platform row also goes red. | Each row carries the last 90 days of uptime as a sparkline. An incident reads as a coloured band on the row; clicking the band opens the timeline — first update, follow-ups, resolution, post-mortem when one is owed. @@ -37,7 +37,9 @@ The page is owned by the on-call rotation. Updates are pushed by the engineer ho ## Self-hosted: what changes -Self-hosted instances do not appear on `status.tale.dev` — that page covers Tale Cloud. Each deployment ships its own status page instead, served by the platform and reachable without signing in at `https:///status`. It renders a server-side health summary — operational, degraded, or outage — from a liveness probe against the Convex backend, so an operator (or an end user checking whether it is just them) can read availability without a login. The machine-readable form is `https:///status.json`, which returns the same result as JSON for an uptime monitor to poll. +Self-hosted instances do not appear on `status.tale.dev` — that page covers Tale Cloud. Each deployment ships its own status page instead, served by the platform and reachable without signing in at `https:///status`. It renders a server-side health summary — operational, degraded, or outage — from a liveness probe against the backend tier's own `/ping` route, the same route the `backend-api` container's healthcheck uses. So an operator (or an end user checking whether it is just them) can read availability without a login. The machine-readable form is `https:///status.json`, which returns the same result as JSON for an uptime monitor to poll. + +The probe reports one component, `backend`, because that tier serves every request the app makes: if it answers, data flows. The platform container's own liveness is implicit — the status page could not have rendered otherwise. Results are cached for five seconds and each probe times out after two, so pointing an uptime monitor at `/status.json` on a tight interval costs the backend almost nothing. That page reports the availability of the deployment itself. For deeper operational signal — container health from `tale status`, request metrics from the Caddy logs, and control-plane events in the in-product audit log — the [observability troubleshooting page](/self-hosted/operate/observability/troubleshooting) maps symptoms to logs. diff --git a/docs/en/develop/webdav-api.md b/docs/en/develop/webdav-api.md index d6f1f0d690..a385e20d2c 100644 --- a/docs/en/develop/webdav-api.md +++ b/docs/en/develop/webdav-api.md @@ -39,11 +39,11 @@ Every authenticated request also verifies the requesting user is an active membe | PROPFIND | List a resource (Depth 0) or a collection's immediate children (Depth 1). The property list emitted is documented below. **Depth: infinity is rejected with 403** to prevent unbounded responses. | Required | | PROPPATCH | Returns 207 success per-property without storing values. Dead properties are not persisted in v1; PROPPATCH succeeds optimistically for client compatibility. | Required | | GET / HEAD | Stream the document blob. Sets `Content-Type`, `Content-Length`, `ETag`, and `Last-Modified`. GET on a collection returns 405. | Required | -| PUT | Create or replace a document. New blob is stored in Convex storage with content-hash dedup; the document row picks up `sourceProvider: "webdav"`. Returns 201 on create, 204 on overwrite. | Required | +| PUT | Create or replace a document. The body streams into the organisation's object store under a fresh key; the document row picks up `sourceProvider: "webdav"`. Returns 201 on create, 204 on overwrite. A request with no `Content-Length` (chunked transfer encoding) is refused — presigning the upload needs the length up front. | Required | | DELETE | Soft-delete a document (sets `lifecycleStatus: "trashed"`) or a folder (cascades trash on contained documents, hard-deletes the folder rows). Returns 204. | Required | | MKCOL | Create a folder under an existing parent. Empty body only. Returns 201, 405 if the target exists, or 409 if the parent does not. | Required | | MOVE | Rename or relocate. Atomic for documents. For folders, updates the `parentId` of the moved folder. Honours `Overwrite: T/F` and `If` headers. Returns 201 (new destination) or 204 (overwrite). | Required | -| COPY | Server-side copy. Document copies reuse the same Convex storage id (dedup). Folder copies recurse. Honours `Overwrite` and `If`. | Required | +| COPY | Server-side copy. A document copy is a second row pointing at the same stored object — no bytes move, and the object survives until the last row referencing it goes. Folder copies recurse. Honours `Overwrite` and `If`. | Required | | LOCK | Class 2 exclusive or shared write-lock. Timeout from `Timeout: Second-N` header, capped at 3600. Refresh by re-sending LOCK with `If: ()` and an empty body. | Required | | UNLOCK | Release a lock by its token. Only the lock owner can release. Returns 204. | Required | @@ -67,7 +67,7 @@ Dead properties are not stored. PROPPATCH echoes 200 for a dead property set on ## Lock semantics -Locks live in their own Convex table, keyed by `(organizationId, resourcePath)`. Wire form is `opaquelocktoken:`. The server: +Locks live in their own Postgres table, indexed on `(organizationId, resourcePath)`. Wire form is `opaquelocktoken:`. The server: - Caps timeout at 3600 seconds. Requests for longer windows are clamped silently. - Treats `LOCK` with an `If: ()` header and an empty body as a refresh — the existing lock's expiry is bumped. @@ -111,16 +111,16 @@ The server advertises `DAV: 1, 2` in the OPTIONS response. - `Depth: infinity` on PROPFIND is rejected with `403`. - `Timeout: Second-N` on LOCK is clamped to `[1, 3600]`. -- PUT body size is capped at **5 GB** by default (`413` once exceeded), enforced both at the reverse proxy and in the platform server. Operators can raise or lower it with the `WEBDAV_MAX_PUT_BYTES` environment variable. The body is streamed to a Convex presigned URL with backpressure, so a large upload does not buffer in platform memory. +- PUT body size is capped at **5 GB** by default (`413` once exceeded), enforced both at the reverse proxy and in the backend. Operators can raise or lower it with the `WEBDAV_MAX_PUT_BYTES` environment variable — set it on the proxy container too, or the proxy stays the binding cap. The body is streamed to a presigned object-store URL with backpressure, so a large upload never buffers in the backend's memory. - XML request bodies (PROPFIND / PROPPATCH / MKCOL / LOCK) are capped at **64 KB** (`413` once exceeded) — these envelopes are tiny by design. - App-passwords are hashed with HMAC-SHA256; the secret never appears in any response after the create call. - `lastUsedAt` is patched at most once per minute per app-password to avoid write storms on busy mounts. ## Network requirements -The WebDAV endpoint runs inside the platform Hono server (`platform:3000` in compose). Caddy routes `/dav/*` to it via the default fallback — no extra configuration is required. The path requires the platform server to have `ADMIN_KEY` set in its environment so it can call internal Convex queries with admin auth. +The WebDAV endpoint is served by the backend tier (`backend-api:3005` in compose). Caddy has its own `handle /dav/*` block that forwards to it and applies the body cap — no extra configuration is required. The endpoint needs no deployment-level credential: every request authenticates with its own app-password, and the handlers read and write Postgres in-process. -For dev (`bun dev`), the same dispatch is mounted as a Vite middleware (`vite-plugins/serve-webdav.ts`) — `curl` and clients can hit `http://localhost:3000/dav//...` against a running dev server without rebuilding. +For dev (`bun run dev`), Vite proxies `/dav` to the same backend, so `curl` and mounted clients can hit `http://localhost:3000/dav//...` against a running dev server. ## Security diff --git a/docs/en/self-hosted/configuration/data-residency.md b/docs/en/self-hosted/configuration/data-residency.md index 6bc8b4fa75..be5fe83537 100644 --- a/docs/en/self-hosted/configuration/data-residency.md +++ b/docs/en/self-hosted/configuration/data-residency.md @@ -3,29 +3,35 @@ title: Data residency description: Point a self-hosted Tale deployment's knowledge database, application database, and uploaded-file storage at infrastructure you control, configured by administrators in Settings > Data residency and applied on restart. --- -A self-hosted Tale deployment runs on infrastructure you already control, so its data lives on your hosts by default. **Data residency** is for the case where you want individual data stores pointed at your own managed Postgres or object storage instead of the bundled containers — for example to keep document text in a database your team operates, or uploaded files in your own S3 bucket. The knowledge corpus runs as its own container (`knowledge-db`) precisely so it can be relocated or replaced independently of the operational database — it is the store most residency requirements care about. Administrators configure this in **Settings > Data residency**; the change is written to a single deployment-level config file and **takes effect when the affected containers restart**. +A self-hosted Tale deployment runs on infrastructure you already control, so its data lives on your hosts by default. **Data residency** is for the case where you want individual data stores pointed at your own managed Postgres or object storage instead of the bundled containers — for example to keep document text in a database your team operates, or uploaded files in your own S3 bucket. The knowledge corpus is a database of its own, addressed by its own connection string, precisely so it can be relocated or replaced independently of the operational database — it is the store most residency requirements care about. -This page covers what can be relocated, the one prerequisite that bites (ParadeDB), how the configuration is stored and applied, and how to restart safely. +Two mechanisms sit behind that. A **deployment-wide** store is repointed on the host, in `.env` and the config tree, and takes effect when the backend containers restart. A **per-organization** store is configured by an org owner or admin in **Settings > Data residency**, lands in that organization's own config directory, and takes effect on the next request. This page covers both, the one prerequisite that bites (ParadeDB), how the configuration is stored, and how to restart safely. ## Enabling editing -**Settings > Data residency** is one page with two kinds of section: the deployment-wide stores every organization shares, and the stores a single organization brings for itself. Each section renders read-only or editable depending on what the reader may change, and the page says which state you are in. Viewing is open to any organization owner or admin; **editing the deployment-wide stores** — repointing a data store, saving secrets, running a connection test, or applying a restart — is restricted to a named allowlist of operators. List their sign-in emails (comma-separated) in `.env` and restart: +**Settings > Data residency** is one page with two kinds of section: the deployment-wide stores every organization shares, and the stores a single organization brings for itself. Each section renders read-only or editable depending on what the reader may change, and the page says which state you are in. Viewing is open to any organization owner or admin; **editing the deployment-wide stores** — repointing a data store, saving secrets, running a connection test — is restricted to a named allowlist of operators. List their sign-in emails (comma-separated) in `.env` and restart: ```bash TALE_DEPLOYMENT_CONFIG_ADMINS=alice@example.com,bob@example.com ``` -With the allowlist empty or unset, the deployment sections still show the current configuration to administrators, but read-only — the **Save deployment** and **Apply & restart** header actions appear only for allowlisted operators. Only a signed-in admin whose email is on the list gets those sections editable; the page tells you which email to add. The entrypoints always consume the config file regardless of the allowlist, so an operator who prefers to hand-edit the file on disk can do so without naming any UI editors. +With the allowlist empty or unset, the deployment sections still show the current configuration to administrators, but read-only — the **Save deployment** header action appears only for allowlisted operators. Only a signed-in admin whose email is on the list gets those sections editable; the page tells you which email to add. There is no restart button: a save prints the two commands that apply it, and the section below repeats them. An operator who prefers to work on the host can skip the allowlist entirely and edit `.env` and the config files directly. ## What you can relocate Three stores, each independent and optional. An absent setting means "use the bundled default" — so a fresh deployment with no config is unchanged. -- **Knowledge database** — the knowledge corpus: document metadata, the extracted chunk text, embeddings, the BM25 index, the semantic cache, and the crawled web pages. It ships as the bundled `knowledge-db` container (`tale_knowledge`, with the `private_knowledge` and `public_web` schemas) and is the store most residency requirements care about, because it holds your document content. Point it at your own managed Postgres to keep the corpus on infrastructure your team operates. -- **File storage** — where uploaded files (the original blobs) live. By default they sit in the bundled object store that ships with the stack (the `object-store` service, on its own volume); you can point them at an external S3-compatible bucket. -- **Application database** (advanced) — the operational Convex database (the bundled `db` container). The Convex backend derives this database's name from `INSTANCE_NAME` (`tale_platform`) and connects on host:port only, so the external Postgres must contain a database named exactly `tale_platform`. Its TLS mode is fixed by the Convex driver and is not configurable. + -> Note: the knowledge database and the application database are two separate Postgres instances — moving one does not touch the other. Relocating the knowledge database moves the extracted text and embeddings; the original uploaded files move only when you also relocate **File storage** to S3. +**Saving the deployment-wide sections does not repoint a store.** The backend opens the application database from `DATABASE_URL`, the knowledge corpus from `KNOWLEDGE_DATABASE_URL`, and the blob store from the `default` config tree's `object-storage/connection.json`. Nothing at boot reads the `dataStores` block that these sections write to `deployment.yml`. Relocate a deployment-wide store with the environment variable or the file named under it below, and read the deployment sections as a record of the intended topology rather than the switch that applies it. The **per-organization** sections further down this page are a different mechanism and do take effect. + + + +- **Knowledge database** — the knowledge corpus: document metadata, the extracted chunk text, embeddings, the BM25 index, the semantic cache, and the crawled web pages. It ships as the `tale_knowledge` database, with the `private_knowledge` and `public_web` schemas, reached at host `knowledge-db`, and is the store most residency requirements care about, because it holds your document content. Point it at your own managed Postgres with `KNOWLEDGE_DATABASE_URL` in `.env` to keep the corpus on infrastructure your team operates. +- **File storage** — where uploaded files (the original blobs) live. By default they sit in the bundled object store that ships with the stack (the `object-store` service, on its own volume). Point them at an external S3-compatible bucket by editing `$TALE_CONFIG_DIR/default/object-storage/connection.json` and its `connection.secrets.json` sidecar; the backend seeds that file against the bundled store on first boot and never overwrites one that exists. +- **Application database** (advanced) — the operational store: chats, tasks, automation runs, the audit log, the background job queue. It ships as the `tale_app` database on the bundled `db` container, and the backend reaches it through one connection string, `DATABASE_URL`. Point that at your own managed Postgres to relocate it; the backend applies its schema migrations to whatever it finds there, at boot, under an advisory lock. + +> Note: the knowledge database and the application database are two separate databases — moving one does not touch the other. On a single-host `tale deploy` stack they share one Postgres container, so a residency requirement that separates them is a reason to relocate at least one. Relocating the knowledge database moves the extracted text and embeddings; the original uploaded files move only when you also relocate **File storage**. ## The ParadeDB prerequisite @@ -64,7 +70,7 @@ The connection lives next to the knowledge one, under the organization's config - `$TALE_CONFIG_DIR//object-storage/connection.json` — region, optional endpoint (for MinIO/R2), path-style flag, bucket, and an optional key prefix. - `$TALE_CONFIG_DIR//object-storage/connection.secrets.json` — the access key pair, SOPS-encrypted when a SOPS age key is configured (see [Secrets with SOPS](/self-hosted/configuration/secrets-with-sops)). -Unlike the deployment-wide S3 switch above, this path is **not** greenfield-only: from the moment the config exists, new uploads go to the org's bucket, while files stored earlier stay readable where they are in Convex storage — mixed references are supported, so you can switch at any time and relocate the older files afterward with the blob backfill below. Removing the config sends new uploads back to the deployment default; files already written to the bucket stay there, but Tale can't read them until the connection is added again. No restart is needed in either direction. +This path is **not** greenfield-only: from the moment the config exists, new uploads go to the org's bucket, while files stored earlier stay readable in the deployment default store — so you can switch at any time and relocate the older files afterward with the blob backfill below. Removing the config sends new uploads back to the deployment default; files already written to the bucket stay there, but Tale can't read them until the connection is added again. No restart is needed in either direction: the resolver caches a connection for fifteen seconds, so a change is live almost immediately. Org admins can manage this connection from the same per-organization sections of **Settings > Data residency**; its connection test performs a real upload/read/delete round-trip against the bucket before you commit. As with the knowledge connection, the JSON files remain the source of truth. @@ -72,39 +78,25 @@ Org admins can manage this connection from the same per-organization sections of ### Moving pre-existing files into the bucket -Connecting the bucket only reroutes **new** uploads; the blobs written before you connected it stay in Convex's `_storage` and keep working through the mixed references above. To bring that history onto your own infrastructure as well — the whole point of data residency — run the **blob backfill**: it copies each pre-existing blob into the org's bucket, verifies it round-trips byte-for-byte, rewrites every row that references it, and deletes the Convex copy. - -An org admin runs it from the UI: with the bucket connection saved, the Object storage section of **Settings > Data residency** shows **Move existing files** — confirm, and the move runs in the background while uploads keep working; a status line on the same section reports progress and the outcome of the latest run. - -An operator with Convex CLI access can run the same engine from a shell instead, passing the organization's id. Dry-run first to see what would move, then run it for real: - -```bash -# Dry run — counts and samples what would move, writes nothing: -bunx convex run object_storage/backfill_actions:migrateOrgBlobsToObjectStorage '{"organizationId":"","dryRun":true}' - -# The real move — drop dryRun once the counts look right: -bunx convex run object_storage/backfill_actions:migrateOrgBlobsToObjectStorage '{"organizationId":""}' -``` - -The backfill is **idempotent** and **org-scoped**: it moves only that organization's blobs, skips anything already in the bucket, and leaves each Convex source in place until its copy is verified — so a re-run after an interruption resumes safely. A real run needs the bucket connection configured first; a dry run does not. This is deliberately **not** a versioned framework migration — it runs on demand, per organization, when you choose to relocate a tenant's history, not at a release boundary. +Connecting the bucket only reroutes **new** uploads; the blobs written before you connected it stay in the deployment default store and keep working, because a stored reference names the object key and the resolver decides which store to read it from. To bring that history onto your own infrastructure as well — the whole point of data residency — run the **blob backfill**: it walks the organization's documents (current files and every version in their history) and its file metadata, and copies each object from the deployment default store into the org's bucket under the same key. -## File storage on S3 +An org admin runs it from the UI: with the bucket connection saved, the Object storage section of **Settings > Data residency** shows **Move existing files** — confirm, and the move runs as a background job while uploads keep working; a status line on the same section reports progress and the outcome of the latest run. -External file storage is all-or-nothing across Convex's storage use-cases, so you provide **five buckets** — files, exports, snapshot-imports, modules, and search — plus a region and credentials. For S3-compatible services (MinIO, Cloudflare R2) set the endpoint and enable path-style addressing. +Two properties make it safe to re-run. Keys never change, so no row is rewritten and no reference can go stale mid-run: an object flips from being read out of the default store to being read out of the bucket the moment its copy lands. And every object already present in the bucket is skipped, so an interrupted run resumes rather than re-copying. The run is org-scoped, and it needs the bucket connection saved first. -> **Greenfield only.** Switching file storage from local to S3 does **not** migrate the blobs already on the local volume — Convex will look for them in the bucket and not find them. Set S3 at initial deployment, or copy the existing local storage into the bucket out of band before switching. +What it does not do is delete. The source object stays in the deployment default store, so a backfill relocates a copy rather than moving the bytes — plan a separate cleanup pass if the residency requirement is that the old copy stop existing. This is deliberately **not** a versioned framework migration: it runs on demand, per organization, when you choose to relocate a tenant's history, not at a release boundary. ## How the configuration is stored -Saving writes two files at the config root (not under an org directory): +Saving the deployment sections writes two files at the config root (not under an org directory): -- `deployment.json` — the non-secret config (hosts, ports, buckets, modes). +- `deployment.yml` — the non-secret config (hosts, ports, buckets, modes). A deployment still carrying the retired `deployment.json` is read as-is and converted on the next save. - `deployment.secrets.json` — the database passwords and S3 keys, SOPS-encrypted (see [Secrets with SOPS](/self-hosted/configuration/secrets-with-sops)). -At boot the `convex` entrypoint reads these and derives its connections before starting. Knowledge ingestion and retrieval run inside the Convex backend, so it is the only container that opens the knowledge-database connection — there is no separate retrieval service to configure. The contract is **fail-closed**: a present-but-unparseable `deployment.json`, an undecryptable secret, or a config missing required fields **aborts startup** rather than silently falling back to the bundled database — mis-routing regulated data is worse than not starting. An absent file is the normal default path. +The per-organization sections write into the organization's own directory instead, at the paths listed above. Those are the files the backend actually resolves a connection from, and the read is **fail-closed**: an org config that is present but unparseable, or whose secret will not decrypt, refuses that organization's reads rather than silently falling back to the bundled store — mis-routing regulated data is worse than failing loudly. An absent file is the normal default path. ## Applying a change: restart -The config is read at boot, so a save does not take effect until the backend containers (`backend-api` and `backend-worker`) restart. Run `docker compose restart backend-api backend-worker`, or `tale deploy` for a zero-downtime blue-green roll — the settings page shows the same commands after a save. +A deployment-wide connection is read at boot, so a change to `.env` or the `default` config tree does not take effect until the backend containers (`backend-api` and `backend-worker`) restart. Run `docker compose restart backend-api backend-worker`, or `tale deploy` for a zero-downtime blue-green roll — the settings page shows the same commands after a save. A per-organization connection needs no restart. The relevant environment variable is `TALE_DEPLOYMENT_CONFIG_ADMINS` (the comma-separated email allowlist of operators allowed to edit). Set it in `.env`. See also [Environment reference](/self-hosted/configuration/environment-reference) and [Secrets with SOPS](/self-hosted/configuration/secrets-with-sops). diff --git a/docs/en/self-hosted/configuration/environment-reference.md b/docs/en/self-hosted/configuration/environment-reference.md index 13d710cf29..3f87af7d63 100644 --- a/docs/en/self-hosted/configuration/environment-reference.md +++ b/docs/en/self-hosted/configuration/environment-reference.md @@ -9,7 +9,7 @@ i18nLintExclude: Tale reads its configuration from a single `.env` file at the repo root. About a dozen variables are mandatory at first boot; the rest tune behaviour. This page lists every variable the [`.env.example`](https://github.com/tale-project/tale/blob/main/.env.example) ships with, what it defaults to, and which surface in the product consumes it. -Groups are ordered by when you first need them: domain identity, TLS, secrets, database, instance, observability, provider encryption. If a variable changes value, restart the platform container (`docker compose restart tale-platform tale-convex`) for it to take effect. +Groups are ordered by when you first need them: domain identity, TLS, secrets, database, instance, observability, provider encryption. If a variable changes value, restart the containers that read it. Most of these are read by the backend, so `docker compose restart backend-api backend-worker` is the usual command; the few the web tier reads need `platform` restarted as well, and `tale deploy` rolls everything. ## How to read this page @@ -42,22 +42,23 @@ The `SITE_URL` must match what the user types in the browser exactly. A trailing | ----------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BETTER_AUTH_SECRET` | example value in shipped file | **Required.** Base64 secret for the Better Auth session signer. Generate with `openssl rand -base64 32`. Rotating invalidates every session. | | `ENCRYPTION_SECRET_HEX` | example value in shipped file | **Required.** 32-byte hex key. AES-256 key for OAuth and connector credentials and HKDF input for the guardrails secret box. Generate with `openssl rand -hex 32`. Rotating invalidates every DB-stored ciphertext; operators must re-enter affected secrets. | -| `INSTANCE_SECRET` | example value in shipped file | **Required.** Used to derive the Convex admin key for `tale deploy`. Deploy fails if unset. | +| `INSTANCE_SECRET` | example value in shipped file | **Required.** 64-character hex string. Derives the WebDAV app-password HMAC key and the sandbox stage token when those are not set explicitly. Deploy fails if unset or malformed; rotating it invalidates every issued WebDAV app-password. | Replace the values that ship in `.env.example` before exposing the instance — they are intentionally insecure placeholders. ## Database -Tale runs two Postgres databases: the operational store (`db`, port 5432) behind the Convex backend, and the knowledge corpus (`knowledge-db`, port 5433) that holds document chunks, embeddings, and crawled pages. Both are ParadeDB and share `DB_PASSWORD`, but they are independent — point either at external infrastructure on its own. +Tale runs two Postgres databases: the operational store (`tale_app` on `db`, port 5432) that the backend uses for application state, sessions, and the job queue, and the knowledge corpus (`tale_knowledge`, reached at host `knowledge-db`) that holds document chunks, embeddings, and crawled pages. Both are ParadeDB and share `DB_PASSWORD`, but they are independent databases — point either at external infrastructure on its own. On a single-host `tale deploy` stack they live in the same Postgres container, which carries the `knowledge-db` network alias. | Name | Default | Description | | ------------------------ | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DB_PASSWORD` | `tale_password_change_me` | **Required.** Password for the self-hosted Postgres user. Change before production. Used by both database containers. | -| `POSTGRES_URL` | constructed from `DB_PASSWORD` | **Optional.** Override the auto-constructed operational-database URL. Use when pointing at an external Postgres or a non-standard host/port. | -| `KNOWLEDGE_DATABASE_URL` | `postgresql://tale:${DB_PASSWORD}@knowledge-db:5432/tale_knowledge` | **Optional.** Connection URL the Convex backend uses for the knowledge corpus. Override to relocate the corpus to your own managed ParadeDB — the data-residency-sensitive store moves independently. | +| `DATABASE_URL` | set by compose from `DB_PASSWORD` and `APP_DB_NAME` | **Required** for the backend, and set for you by compose and `tale deploy`. The application database's full connection URL, database name included. Override it to point the backend at an external Postgres. | +| `APP_DB_NAME` | `tale_app` | **Optional.** Name of the application database on the bundled `db` container. | +| `KNOWLEDGE_DATABASE_URL` | `postgresql://tale:${DB_PASSWORD}@knowledge-db:5432/tale_knowledge` | **Optional.** Connection URL the backend uses for the knowledge corpus. Override to relocate the corpus to your own managed ParadeDB — the data-residency-sensitive store moves independently. | | `KNOWLEDGE_DB_NAME` | `tale_knowledge` | **Optional.** Name of the knowledge database. The bundled `knowledge-db` container creates this database on first boot. | -The auto-constructed operational form is `postgresql://tale:${DB_PASSWORD}@db:5432`. Convex expects this URL without a database name; the name is derived from the instance configuration. The knowledge corpus lives in `tale_knowledge` with the `private_knowledge` and `public_web` schemas; the **Settings > Data residency** UI writes a richer per-store config than these raw variables, covered in [Data residency](/self-hosted/configuration/data-residency). +The backend applies its own schema migrations to whatever `DATABASE_URL` points at, at boot, inside an advisory lock — so a rolling deploy migrates exactly once no matter how many containers start together. The knowledge corpus lives in `tale_knowledge` with the `private_knowledge` and `public_web` schemas. A single organisation can override the corpus connection without touching these variables, covered in [Data residency](/self-hosted/configuration/data-residency). ## Observability @@ -67,7 +68,7 @@ The auto-constructed operational form is `postgresql://tale:${DB_PASSWORD}@db:54 | `SENTRY_TRACES_SAMPLE_RATE` | unset | Optional sample rate for browser performance traces (`0.0`–`1.0`). Browser-only — the backend reports errors, never traces. | | `METRICS_BEARER_TOKEN` | unset | Bearer token required to access the Prometheus `/metrics/*` endpoints. Leave unset to keep metrics endpoints unreachable from outside. | -Setting `METRICS_BEARER_TOKEN` exposes two endpoints behind the token: `/metrics/platform` and `/metrics/convex` (Convex's 261 built-in metrics, which now carry the RAG and crawl timings as well). See [Observability config](/self-hosted/configuration/observability-config) for the scrape config. +Setting `METRICS_BEARER_TOKEN` exposes three endpoints behind the token: `/metrics/backend`, `/metrics/platform`, and `/metrics/sla-rules`. Scrape `/metrics/backend` — it is the tier that serves every request and drains the job queue. See [Observability config](/self-hosted/configuration/observability-config) for the scrape config. ## Provider secrets encryption @@ -78,7 +79,7 @@ Setting `METRICS_BEARER_TOKEN` exposes two endpoints behind the token: `/metrics When both age vars are unset, Tale stores `providers/*.secrets.json` as plaintext JSON at mode 0600. Reach this mode only when the host disk is encrypted at rest or the files are produced by external tooling (a Kubernetes Secret mount, a Vault template). Rotating an age key is appending the new key, re-saving each provider in the UI, then dropping the old key. See [Secrets with SOPS](/self-hosted/configuration/secrets-with-sops) for the full rotation walk. -The env-var key source needs no environment-level switch: a provider credential can hold the _name_ of an environment variable instead of a stored key, as long as that name carries the reserved `TALE_PROVIDER_KEY_` prefix. The gate is fail-closed — any other name is rejected, so the field can never point at an unrelated deployment secret — and names are capped at 40 characters. Define the variable here or in your secret manager so both the platform and the Convex backend can read it; the full mechanism is documented in [Providers](/self-hosted/configuration/providers). A subscription-broker credential has a second, separate namespace for the secret Tale presents **to the broker**: that field takes an environment-variable name under the reserved `TALE_TOKEN_SOURCE_` prefix, capped at 60 characters. The two prefixes stay distinct on purpose — a broker secret is not a provider API key, and neither field can name a variable outside its own namespace. +The env-var key source needs no environment-level switch: a provider credential can hold the _name_ of an environment variable instead of a stored key, as long as that name carries the reserved `TALE_PROVIDER_KEY_` prefix. The gate is fail-closed — any other name is rejected, so the field can never point at an unrelated deployment secret — and names are capped at 40 characters. Define the variable here or in your secret manager so both backend roles can read it; the full mechanism is documented in [Providers](/self-hosted/configuration/providers). A subscription-broker credential has a second, separate namespace for the secret Tale presents **to the broker**: that field takes an environment-variable name under the reserved `TALE_TOKEN_SOURCE_` prefix, capped at 60 characters. The two prefixes stay distinct on purpose — a broker secret is not a provider API key, and neither field can name a variable outside its own namespace. ## Connector OAuth apps @@ -130,7 +131,7 @@ Optional toggles for features not enabled by default. Each flag turns one featur ## RAG retrieval tuning -Optional knobs for knowledge-base search. The in-process RAG path (Convex node-actions) re-scores results with a cross-encoder when re-ranking is on. All carry the `RAG_` prefix and are read by the `platform` and `convex` containers at boot; after changing one, run `docker compose restart platform convex` for it to take effect. +Optional knobs for knowledge-base search. Retrieval runs in-process in the backend and re-scores results with a cross-encoder when re-ranking is on. All carry the `RAG_` prefix and are read at boot; after changing one, run `docker compose restart backend-api backend-worker` for it to take effect. | Name | Default | Description | | ---------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | @@ -154,7 +155,7 @@ Leave it unset to keep the default session lifetime. When set, an idle session e ## Video-link ingestion (yt-dlp) -When Tale ingests a video link, it fetches the transcript for the agent. YouTube blocks automated access from datacenter/server IPs, so this can fail on a cloud deployment. The deployment ships a PO-token provider wired up by default (see [Video ingestion](/self-hosted/configuration/video-ingestion) for the full picture); the options below are optional overrides and escalations. None guarantees a bypass — a clean egress IP is the single biggest lever. Read by the `convex` container and re-read on each ingestion, so a change takes effect without a restart. +When Tale ingests a video link, it fetches the transcript for the agent. YouTube blocks automated access from datacenter/server IPs, so this can fail on a cloud deployment. The deployment ships a PO-token provider wired up by default (see [Video ingestion](/self-hosted/configuration/video-ingestion) for the full picture); the options below are optional overrides and escalations. None guarantees a bypass — a clean egress IP is the single biggest lever. Read by `backend-worker`, which runs the ingestion, and re-read on each ingestion, so a change takes effect without a restart. | Name | Default | Description | | -------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -166,7 +167,7 @@ When Tale ingests a video link, it fetches the transcript for the agent. YouTube | `VIDEO_INGEST_PLAYER_CLIENT` | `default,tv_simply` | Comma-separated YouTube player-client fallback list. When a PO-token provider is wired the default widens to `default,mweb,tv_simply` (mweb needs a GVS token); set explicitly to force a list. | | `VIDEO_INGEST_PO_TOKEN` | unset | Manually pinned PO token (`CLIENT.CONTEXT+TOKEN`). Mainly for testing — tokens are video-ID-bound and short-lived; prefer the provider. | | `VIDEO_INGEST_IMPERSONATE` | unset | Browser TLS/JA3 impersonation target (e.g. `safari`). Requires `curl_cffi` in the image; leave unset unless you know it's available. | -| `VIDEO_INGEST_BIN_DIR` | unset | Directory prepended to the yt-dlp/ffmpeg child's `PATH` so a self-provisioned `yt-dlp` (and its Deno runtime) installed outside the image's pinned bin dirs is found first. The `convex` image bakes yt-dlp into `PATH`, so leave it unset there; set it on a host or dev box running its own toolchain. | +| `VIDEO_INGEST_BIN_DIR` | unset | Directory prepended to the yt-dlp/ffmpeg child's `PATH` so a self-provisioned `yt-dlp` (and its Deno runtime) installed outside the image's pinned bin dirs is found first. The platform image bakes yt-dlp into `PATH`, so leave it unset in a container deployment; set it on a host or dev box running its own toolchain. | | `VIDEO_INGEST_FFMPEG_LOCATION` | `/usr/bin/ffmpeg` | Absolute path to the ffmpeg yt-dlp uses for post-processing (subtitle conversion, audio extraction). Override when ffmpeg lives elsewhere — e.g. Homebrew's `/opt/homebrew/bin/ffmpeg` on a macOS dev box. | None of these guarantees success against YouTube's adversarial detection. Ordinary public videos, less aggressive platforms, or a residential-IP/self-hosted deployment typically work without any of them. diff --git a/docs/en/self-hosted/configuration/observability-config.md b/docs/en/self-hosted/configuration/observability-config.md index 272d15078c..c87fe5b2d8 100644 --- a/docs/en/self-hosted/configuration/observability-config.md +++ b/docs/en/self-hosted/configuration/observability-config.md @@ -19,26 +19,25 @@ Tale does not ship a log shipper. The driver swap is the supported connector poi ## Metrics -The Caddy proxy exposes up to four metrics paths gated by a single bearer token: +The Caddy proxy exposes three metrics paths gated by a single bearer token: -| Path | Source | What's inside | -| -------------------- | --------------- | ----------------------------------------------------------------------------------- | -| `/metrics/platform` | `tale-platform` | HTTP latency, route counters, Node process metrics, response-time SLA target gauges | -| `/metrics/convex` | `tale-convex` | 261 built-in Convex metrics, plus the RAG and crawl timings | -| `/metrics/sla-rules` | `tale-platform` | Generated Prometheus recording + alerting rules for the response-time SLAs | -| `/metrics/backend` | `tale-backend-api` | Process metrics, HTTP counters and latency by route class, queue depth per job state, in-flight chat generations, open hint streams, drain state, and the same SLA target gauges | +| Path | Source | What's inside | +| -------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/metrics/backend` | `backend-api` | Process metrics, HTTP counters and latency by route class, queue depth per job state, in-flight chat generations, open hint streams, drain state, and the SLA target gauges | +| `/metrics/platform` | `tale-platform` | Node process metrics (CPU, memory, event-loop lag, GC) and the response-time SLA target gauges. The web tier serves static files, so it emits no HTTP request series | +| `/metrics/sla-rules` | `tale-platform` | Generated Prometheus recording + alerting rules for the response-time SLAs | -Knowledge work (RAG search, document ingestion, web crawling) runs inside the Convex backend now, so its timings ride the `/metrics/convex` series rather than a separate endpoint. Set `METRICS_BEARER_TOKEN` in `.env` to enable these endpoints; leave it unset to keep them returning 401 to every request. The `/metrics/sla-rules` path is a read-only YAML rules file you load into Prometheus, not a scrape target — the thresholds it carries are documented in [Operations](/self-hosted/operate/observability/operations). Anything other than the listed paths returns 401 too, so a misrouted scraper does not accidentally see the platform's internal health endpoints. +`/metrics/backend` is the one that matters: it is the tier that serves every request, runs knowledge search, and drains the job queue. Set `METRICS_BEARER_TOKEN` in `.env` to enable these endpoints; leave it unset to keep them returning 401 to every request. The `/metrics/sla-rules` path is a read-only YAML rules file you load into Prometheus, not a scrape target — the thresholds it carries are documented in [Operations](/self-hosted/operate/observability/operations). Anything other than the listed paths returns 404 inside the gate and 401 outside it, so a misrouted scraper never accidentally sees another service's numbers under the wrong name. -`/metrics/backend` appears only once a deployment has cut over to the Postgres backend (`BACKEND_UPSTREAM` set in `.env`). Before that the path answers 404 rather than quietly serving another service's numbers under the backend's name, so a scrape target you add early fails loudly instead of charting the wrong process. +There is nothing to scrape on `backend-worker`: the worker role serves no HTTP. Its behaviour is visible on `/metrics/backend` instead, because the queue gauge reads the shared job table — `tale_backend_jobs{state="created"}` climbing and never draining is what a stalled worker looks like. A working Prometheus scrape stanza: ```yaml scrape_configs: - - job_name: tale-platform + - job_name: tale-backend scheme: https - metrics_path: /metrics/platform + metrics_path: /metrics/backend authorization: credentials: static_configs: @@ -61,7 +60,9 @@ The sample rate caps browser performance traces and applies only there — the b ## What does not ship yet -OpenTelemetry traces are not built into the containers. The data is reachable indirectly — Convex action durations and HTTP route timings come through the Prometheus metrics — but there is no OTLP exporter on the box today. If you need full trace export, run an OpenTelemetry Collector alongside Tale and scrape the Prometheus endpoints from it. +OpenTelemetry traces are not built into the containers. The data is reachable indirectly — request durations by route class come through the Prometheus metrics — but there is no OTLP exporter on the box today. If you need full trace export, run an OpenTelemetry Collector alongside Tale and scrape the Prometheus endpoints from it. + +Neither is there a request log. The backend records every request as a metric, not a line, so there is no per-request audit in `docker compose logs backend-api` — the proxy's access log is the closest thing, and the in-product audit log is what covers control-plane actions. ## Where this fits diff --git a/docs/en/self-hosted/configuration/providers.md b/docs/en/self-hosted/configuration/providers.md index 2218da4a62..b78e22f758 100644 --- a/docs/en/self-hosted/configuration/providers.md +++ b/docs/en/self-hosted/configuration/providers.md @@ -66,11 +66,11 @@ TALE_PROVIDER_KEY_OPENAI_PROD=sk-... -The gate is fail-closed: any name outside the reserved prefix is rejected, which is what stops a credential from naming an unrelated deployment secret such as `SOPS_AGE_KEY` or `BETTER_AUTH_SECRET` and having it sent as a bearer token to a provider endpoint. Names are capped at 40 characters, the limit of the platform-to-Convex environment sync — a longer name would silently never reach the backend runtime. +The gate is fail-closed: any name outside the reserved prefix is rejected, which is what stops a credential from naming an unrelated deployment secret such as `SOPS_AGE_KEY` or `BETTER_AUTH_SECRET` and having it sent as a bearer token to a provider endpoint. Names are capped at 40 characters. -Define the variable so that both the platform container and the Convex backend can read it. The platform syncs its environment to Convex at boot, so the in-process actions resolve the same value; a variable added or changed after boot needs a restart of the platform container before it is visible. Values are trimmed, which spares you the trailing newline a mounted secret file often carries and the `401` it produces. +Define the variable where the backend containers read it — `.env`, or whatever your secret store injects into `backend-api` and `backend-worker`. Both roles resolve a credential, so both need the value: the worker runs the same provider calls for background work that the api runs for a live turn. A variable added or changed after boot needs `docker compose restart backend-api backend-worker` before it is visible. Values are trimmed, which spares you the trailing newline a mounted secret file often carries and the `401` it produces. ## Broker secrets from the environment diff --git a/docs/en/self-hosted/configuration/retention.md b/docs/en/self-hosted/configuration/retention.md index 6d835c72f1..939303f062 100644 --- a/docs/en/self-hosted/configuration/retention.md +++ b/docs/en/self-hosted/configuration/retention.md @@ -41,7 +41,7 @@ The admin-chosen retention windows live in a separate file, `retention-policy.js ## The retention sweep -A scheduled cron inside `tale-convex` runs the actual deletion. Each category is swept independently — a slow run on one does not block the others. Deletions are audited (every category has its own `*.retention_deleted` event), and restoring an entity inside its grace window is possible from **Trash** before the final sweep. +A daily scheduled job on `backend-worker` runs the actual deletion, at 04:00 UTC. Each category is swept independently — a slow run on one does not block the others, and a sweep that fails is retried by the queue rather than skipped until tomorrow. Deletions are audited (every category has its own `*.retention_deleted` event), and restoring an entity inside its grace window is possible from **Trash** before the final sweep. Audit log entries are themselves subject to retention, but their floor is enforced per-deployment, not per-org: the strictest (shortest) audit-log retention across all orgs is what actually runs. A stricter tenant pulls everyone tighter — keep this in mind on multi-tenant instances. diff --git a/docs/en/self-hosted/configuration/secrets-with-sops.md b/docs/en/self-hosted/configuration/secrets-with-sops.md index 28cc763895..49d4693156 100644 --- a/docs/en/self-hosted/configuration/secrets-with-sops.md +++ b/docs/en/self-hosted/configuration/secrets-with-sops.md @@ -15,7 +15,7 @@ The env vars that drive the modes are `SOPS_AGE_KEY` and `SOPS_AGE_KEY_FILE` — | Key file | `SOPS_AGE_KEY_FILE=/path/to/keys` | Required for rotation. One age key per line, `#` comments. | | Plaintext at 0600 | Both unset | Disk encrypted at rest, or external tooling writes the files. | -The platform container picks the mode at boot. The inline form is the simplest; the file form is the only one that supports multiple readers (which is what makes rotation possible without downtime); the plaintext form skips SOPS entirely and trusts the filesystem. +The backend containers pick the mode at boot — they own every write to the config store and are the only processes that decrypt it; the web tier mounts the same volume read-only and never touches the age key. The inline form is the simplest; the file form is the only one that supports multiple readers (which is what makes rotation possible without downtime); the plaintext form skips SOPS entirely and trusts the filesystem. ## First-boot encrypted mode @@ -30,7 +30,7 @@ cat providers/openai.secrets.json # } ``` -Decryption happens in-process when the platform container reads the file. The age key never leaves the platform container's memory. +Decryption happens in-process when a backend container reads the file. The age key never leaves that container's memory. ## Rotating the age key @@ -43,10 +43,10 @@ age-keygen -o /etc/tale/age-keys.txt # 2. Append the new key as a second line in the file echo "AGE-SECRET-KEY-1NEW..." >> /etc/tale/age-keys.txt -# 3. Point .env at the file and restart the platform container +# 3. Point .env at the file and restart the backend containers sed -i 's|^SOPS_AGE_KEY=.*|# SOPS_AGE_KEY=|' .env sed -i 's|^# SOPS_AGE_KEY_FILE=.*|SOPS_AGE_KEY_FILE=/etc/tale/age-keys.txt|' .env -docker compose restart tale-platform tale-convex +docker compose restart backend-api backend-worker ``` Now both old and new keys can decrypt existing files. Re-save each provider's API key under **Settings > Providers** — each save produces ciphertext readable by both keys. Once every provider has been re-saved (the **Last rotated** column in the providers table tells you which still hold old ciphertext), remove the old key from the file: @@ -54,10 +54,10 @@ Now both old and new keys can decrypt existing files. Re-save each provider's AP ```bash # 4. Drop the old key line and restart again sed -i '/^AGE-SECRET-KEY-1OLD/d' /etc/tale/age-keys.txt -docker compose restart tale-platform tale-convex +docker compose restart backend-api backend-worker ``` -The order is load-bearing: never remove the old key before every file is re-encrypted, or the platform container will fail to read the still-old files at the next decryption. +The order is load-bearing: never remove the old key before every file is re-encrypted, or the backend will fail to read the still-old files at the next decryption. ## Switching to plaintext diff --git a/docs/en/self-hosted/contributing-docker.md b/docs/en/self-hosted/contributing-docker.md index 184b19ff7b..4dc3e88469 100644 --- a/docs/en/self-hosted/contributing-docker.md +++ b/docs/en/self-hosted/contributing-docker.md @@ -14,15 +14,14 @@ The stack is entirely TypeScript — no Python image. Each image has one Dockerf | Image | Source path | Base | | ------------------------ | ----------------------------- | ---------------------------- | | `tale-proxy` | `services/proxy/` | Caddy | -| `tale-platform` | `services/platform/` | Bun + Debian slim | -| `tale-convex` | `services/convex/` | Convex local-backend | +| `tale-platform` | `services/platform/` | Debian slim + Bun + Node | | `tale-db` | `services/db/` | ParadeDB (Postgres) | | `tale-sandbox` | `services/sandbox/` | Bun + Docker CLI | | `tale-sandbox-egress` | `services/sandbox-egress/` | Alpine + tinyproxy | | `tale-sandbox-runtime` | `services/sandbox-runtime/` | Bun + Chromium + Playwright | | `tale-sandbox-buildkitd` | `services/sandbox-buildkitd/` | Debian + BuildKit + redsocks | -Both database containers — `db` and `knowledge-db` — build from the same `tale-db` ParadeDB image; the difference is the database each one serves. The LLM gateway, `tale-sandbox-llm-gateway`, is a pinned upstream image (`maximhq/bifrost`), so it has no Dockerfile in the repo. The compose files at the repo root (`compose.yml` for development, the CLI-generated production compose) reference these by `ghcr.io/tale-project/tale/:`. A local build replaces the registry pull with a `build:` block in compose. +Both database containers — `db` and `knowledge-db` — build from the same `tale-db` ParadeDB image; the difference is the database each one serves. The same image also serves both backend roles: `backend-api` and `backend-worker` are `tale-platform` started with a different `TALE_ROLE`, which is why they can never version-skew from the web tier. Two containers have no Dockerfile of their own: the blob store is an upstream MinIO image referenced directly from compose, and `tale-sandbox-llm-gateway` is a thin re-tag of the pinned upstream `maximhq/bifrost` gateway that changes nothing at runtime. The compose files at the repo root (`compose.yml` for development, the CLI-generated production compose) reference these by `ghcr.io/tale-project/tale/:`. A local build replaces the registry pull with a `build:` block in compose. ## Building locally @@ -47,7 +46,7 @@ The supported extension points for forks are at the Dockerfile level. The image' - **Sandbox runtime image** — `services/sandbox-runtime/Dockerfile` is the execution environment for `Run code`, web rendering, and document generation; it already carries Chromium and Playwright. A fork that needs an extra system package or a different browser build patches here. - **Sandbox egress proxy** — `services/sandbox-egress/tinyproxy.conf.template` is the proxy config the entrypoint renders at startup: open egress by default, or a default-deny hostname filter when `SANDBOX_EGRESS_ALLOWLIST` is set. A fork that needs different proxy behaviour patches here. -What is not a supported seam: the convex backend's application code, including document extraction and the RAG and crawler logic that now live in-process (`services/platform/convex/`), and the platform container's runtime code (`services/platform/app/`). Those files are application code, not configuration — adding a document-format extractor or changing retrieval behaviour is a real fork and rides the upgrade tax. +What is not a supported seam: the backend's application code (`services/platform/backend/`), including document extraction and the RAG and crawler logic that run in-process there, and the web tier's runtime code (`services/platform/app/`). Those files are application code, not configuration — adding a document-format extractor or changing retrieval behaviour is a real fork and rides the upgrade tax. ## Tagging and pushing your own registry diff --git a/docs/en/self-hosted/install/docker-compose-reference.md b/docs/en/self-hosted/install/docker-compose-reference.md index 99fc1014c2..8e49a99376 100644 --- a/docs/en/self-hosted/install/docker-compose-reference.md +++ b/docs/en/self-hosted/install/docker-compose-reference.md @@ -38,15 +38,19 @@ The leftmost file is the base; each subsequent file merges its keys on top. Conf ## Services and their roles -The base graph brings up eight containers: - -- `tale-proxy` — Caddy. TLS, reverse proxy, 301s. -- `tale-platform` — the TanStack Start app. The user-facing UI and API. -- `tale-convex` — the Convex backend. WebSocket, queries, mutations, actions — and the in-process RAG search, document ingestion, web crawling, and document generation that used to be separate services. -- `tale-db` — operational Postgres (ParadeDB). The Convex backend's persistent store. +The base graph brings up ten containers: + +- `tale-proxy` — Caddy. TLS, reverse proxy, 301s. It also publishes the blob store's bucket path so presigned URLs work in the browser. +- `tale-platform` — the TanStack Start app. The user-facing UI, the static assets, and the public `/status` page. +- `backend-api` — the application backend: a Node process serving every door under `/api/`, plus `/events`, `/dav`, and the machine API. Knowledge search runs in this process. +- `backend-worker` — the same image in the worker role, draining the pg-boss job queue: document ingestion and embedding, web crawling, automation runs, retention sweeps. It serves no HTTP. Both backend services take `--scale`, which is why neither has a fixed container name. +- `tale-db` — operational Postgres (ParadeDB). The `tale_app` database: application state, sessions, and the job queue. +- `tale-object-store` — the blob store (MinIO). Every uploaded document, chat attachment, audio file, and generated medium. Internal-only. - `tale-knowledge-db` — knowledge corpus Postgres (ParadeDB). The `tale_knowledge` database holding document chunks, embeddings, and crawled pages, on port 5433 so it never clashes with `tale-db` on 5432. - `tale-sandbox-llm-gateway` — the LLM gateway for harness turns (pinned external image). -- `tale-sandbox-egress` and `tale-sandbox` — the sandbox plane. Run-code containers behind an egress proxy (open by default; lock down with `SANDBOX_EGRESS_ALLOWLIST`), also the headless-browser runtime the convex backend calls for web rendering and document generation. +- `tale-sandbox-egress` and `tale-sandbox` — the sandbox plane. Run-code containers behind an egress proxy (open by default; lock down with `SANDBOX_EGRESS_ALLOWLIST`), also the headless-browser runtime the backend calls for web rendering and document generation. + +A `bgutil-provider` sidecar joins them for YouTube ingestion; it is best-effort, and the stack works without it. A single-host `tale deploy` stack drops `tale-knowledge-db` and folds the corpus into `tale-db` under the `knowledge-db` network alias. The stack is now entirely TypeScript — there is no Python service in the graph. [Container architecture](/self-hosted/operate/container-architecture) goes deeper on what owns what. diff --git a/docs/en/self-hosted/install/quickstart.md b/docs/en/self-hosted/install/quickstart.md index 813988655e..e9840a199d 100644 --- a/docs/en/self-hosted/install/quickstart.md +++ b/docs/en/self-hosted/install/quickstart.md @@ -83,7 +83,7 @@ On an empty instance there is no sign-up page to hunt for: the first visit lands -[First admin](/self-hosted/install/first-admin) covers the wizard in detail, how teammates join, and the Convex dashboard admin key — a backend-inspection tool that plays no part in sign-in. +[First admin](/self-hosted/install/first-admin) covers the wizard in detail and how teammates join. diff --git a/docs/en/self-hosted/operate/backups-and-restore.md b/docs/en/self-hosted/operate/backups-and-restore.md index 59af5e724c..83e240479d 100644 --- a/docs/en/self-hosted/operate/backups-and-restore.md +++ b/docs/en/self-hosted/operate/backups-and-restore.md @@ -3,21 +3,31 @@ title: Backups and restore description: Volume snapshots via `tale backup`, the automatic pre-migration snapshot, retention, the off-host copy, and the `tale restore` drill. --- -Tale's backup unit is the volume snapshot: a paused, checksummed tar of every data volume in the instance, written into a dedicated `backups` volume that lives next to the data it protects. The CLI takes one automatically before any deploy step that can migrate data, and `tale backup` takes one on demand. Recovery is `tale restore ` plus a redeploy of the matching version — that pair is the answer to a failed upgrade, and the reason `tale rollback` can afford to refuse anything beyond a patch step. +Tale's backup unit is the volume snapshot: a paused, checksummed tar of the database, the org config tree, and the proxy state, written into a dedicated `backups` volume that lives next to the data it protects. The CLI takes one automatically before any deploy step that can migrate data, and `tale backup` takes one on demand. Recovery is `tale restore ` plus a redeploy of the matching version — that pair is the answer to a failed upgrade, and the reason `tale rollback` can afford to refuse anything beyond a patch step. -The architecture context lives in [Container architecture](/self-hosted/operate/container-architecture); this page covers what a snapshot contains, when one is taken, how the copy gets off the host, and the restore walk. +A snapshot is not the whole instance. Uploaded file blobs sit outside it, so the off-host job below is what makes a full rebuild possible — read that section even if you never take a manual snapshot. + +The architecture context lives in [Container architecture](/self-hosted/operate/container-architecture); this page covers what a snapshot contains, what it leaves to you, when one is taken, how the copy gets off the host, and the restore walk. ## What a snapshot contains -| Volume | Holds | -| ---------------------------- | ----------------------------------------------- | -| `db-data` | Postgres — agents, runs, the audit log | -| `convex-data` | Org config, provider secrets, uploaded branding | -| `rag-data` | The vector index built from your documents | -| `crawler-data` | Crawled website knowledge | -| `caddy-data`, `caddy-config` | TLS certificates and proxy state | +| Volume | Holds | +| ---------------------------- | ---------------------------------------------------------------------------------------------- | +| `db-data` | Postgres — the application database (chats, tasks, automation runs, the audit log) and, on a single-host `tale deploy` stack where both databases share one Postgres, the knowledge corpus | +| `convex-data` | The org config tree — agents, automations, connectors, providers, skills, governance policies, SSO connections, branding | +| `caddy-data`, `caddy-config` | TLS certificates and proxy state | + +`convex-data` is the config volume's historical name. It is kept deliberately so that retiring the Convex backend did not force every operator to migrate a volume for a rename; nothing Convex-related runs in it. + +Each snapshot is a directory named like `20260611-142530-deploy` inside the project's `backups` volume: one `.tar.gz` per volume, a `.sha256` sidecar each, and a `manifest.json` written last. A directory without a manifest is an incomplete snapshot — it never shows up in listings and can never be restored. + + + +**Uploaded files are not in the snapshot.** Document blobs, chat attachments, audio, and generated media live in the blob store on the `object-store-data` volume, and `tale backup` does not capture it. A restore therefore brings back rows that reference blobs the store no longer has — the app renders the document list and fails on open. Capture `object-store-data` in the same job that copies the `backups` volume off the host, or point the deployment at an object store that carries its own backups. + + -Each snapshot is a directory named like `20260611-142530-deploy` inside the project's `backups` volume: one `.tar.gz` per volume, a `.sha256` sidecar each, and a `manifest.json` written last. A directory without a manifest is an incomplete snapshot — it never shows up in listings and can never be restored. Two things live outside the volumes and need separate capture: the project workspace (the directory holding `tale.json`) and `.env`. +Three more things live outside the snapshotted volumes and need separate capture: the blob store above, the project workspace (the directory holding `tale.json`), and `.env`. ## When snapshots are taken @@ -36,19 +46,20 @@ Rotation keeps the newest five snapshots and everything from the last 14 days ## Off-host copy -The snapshots live on the same host as the data they protect — a dead disk takes both. Point your existing backup tooling (Restic, Borg, Velero, cloud-provider snapshots) at the `backups` volume, and capture the project workspace and `.env` in the same job. Tale does not ship an upload step — keeping the off-host copy under your existing backup contract is deliberate. +The snapshots live on the same host as the data they protect — a dead disk takes both. Point your existing backup tooling (Restic, Borg, Velero, cloud-provider snapshots) at the `backups` volume **and** at `object-store-data`, and capture the project workspace and `.env` in the same job. Tale does not ship an upload step — keeping the off-host copy under your existing backup contract is deliberate. ```bash -# crontab on the host — hourly Restic copy of the backups volume to S3 +# crontab on the host — hourly Restic copy of the snapshots and the blob store 0 * * * * restic -r s3:s3.amazonaws.com/bucket/tale backup \ - /var/lib/docker/volumes/_backups/_data + /var/lib/docker/volumes/_backups/_data \ + /var/lib/docker/volumes/_object-store-data/_data ``` -Find the volume's host path with `docker volume inspect _backups`; the project id lives in `tale.json`. +Find a volume's host path with `docker volume inspect _backups`; the project id lives in `tale.json`. ## Restoring a snapshot -`tale restore` without arguments lists what is available; with an id it verifies the checksums, wipes the data volumes, and extracts the snapshot. It refuses while any project container runs — pass `--stop` to stop them — and asks for confirmation before touching anything. +`tale restore` without arguments lists what is available; with an id it verifies the checksums, wipes the volumes the snapshot covers, and extracts the snapshot. It refuses while any project container runs — pass `--stop` to stop them — and asks for confirmation before touching anything. It restores only the volumes listed in the table above; the blob store is yours to restore from the off-host copy, before you bring the stack back up. ```bash # See what's available @@ -66,7 +77,7 @@ The redeploy of the matching version is part of the restore, not an optional ext ## Restore drill -Run the drill quarterly on a non-production host. The drill is not "does a snapshot exist" — it is "can a fresh host be rebuilt from the off-host copy of the `backups` volume, the project workspace, and `.env` in under an hour." The failure modes the drill catches: an off-host job that never captured the workspace, and a stale `.env` that no longer matches the current binary's requirements. +Run the drill quarterly on a non-production host. The drill is not "does a snapshot exist" — it is "can a fresh host be rebuilt from the off-host copy of the `backups` volume, the blob store, the project workspace, and `.env` in under an hour." Finish by opening a document that was uploaded before the snapshot: that is the one step that proves the blob store came back with the database, and it is the step a snapshot-only drill skips. The other failure modes the drill catches: an off-host job that never captured the workspace, and a stale `.env` that no longer matches the current binary's requirements. ## Where this fits diff --git a/docs/en/self-hosted/operate/container-architecture.md b/docs/en/self-hosted/operate/container-architecture.md index 1e376ee341..35c904006d 100644 --- a/docs/en/self-hosted/operate/container-architecture.md +++ b/docs/en/self-hosted/operate/container-architecture.md @@ -3,59 +3,71 @@ title: Container architecture description: Which container owns which job in a running Tale instance, the request path of a chat message, and what an outage in each container looks like. --- -A Tale instance is eight containers wired by docker compose. The architecture page covered what each container is for; this page is the operator's version — which container owns which job, how a chat message flows through them, and what the failure mode looks like when one of them dies. +A Tale instance is ten containers wired by docker compose. The architecture page covered what each container is for; this page is the operator's version — which container owns which job, how a chat message flows through them, and what the failure mode looks like when one of them dies. Read this when you are on call. Come back when you are deciding which container to roll first during an upgrade. -## The eight containers, with their jobs +## The ten containers, with their jobs -| Container | Job | Crashes affect | -| -------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| `tale-proxy` | TLS termination + edge routing | All ingress — no client can reach the UI | -| `tale-platform` | UI server, static asset delivery | Browser sees 502; the API is still reachable | -| `tale-convex` | Backend actions/queries/mutations + WebSocket, plus in-process RAG, crawling, and document gen | UI loads, but no data; in-flight chats stall; ingestion stalls | -| `tale-db` | Operational Postgres for Convex | Convex falls back to read-only; writes block | -| `tale-knowledge-db` | Knowledge corpus Postgres (document chunks, embeddings, crawled pages) | Knowledge search returns empty; ingestion fails | -| `tale-sandbox-llm-gateway` | LLM gateway for harness turns | Harness turns can't reach a model; chat is unaffected | -| `tale-sandbox-egress` | Network egress for sandboxed code | `Run code` tool errors with "egress denied"; web render fails | -| `tale-sandbox` | Sandbox runtime + headless browser for web render and document generation | `Run code`, web crawl render, and document generation all fail | +| Container | Job | Crashes affect | +| -------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| `tale-proxy` | TLS termination + edge routing | All ingress — no client can reach the UI | +| `tale-platform` | UI server, static asset delivery, the public `/status` page | Browser sees 502; the API is still reachable | +| `backend-api` | Every application request: auth, the app API, the machine API, WebDAV, the live-update stream, and knowledge search in-process | UI loads, but no data; in-flight chats stall | +| `backend-worker` | Background jobs: document ingest and embedding, web crawling, automation runs, retention sweeps, the cron schedule | The UI keeps working; uploads sit in "indexing" and automations do not fire | +| `tale-db` | Postgres — the application database, the job queue, and the knowledge corpus | Writes are refused; the app degrades to whatever is already loaded | +| `tale-knowledge-db` | Knowledge corpus Postgres (document chunks, embeddings, crawled pages) | Knowledge search returns empty; ingestion fails | +| `tale-object-store` | The blob store — uploaded documents, chat attachments, audio, generated media | Every upload and every download fails; the rest of the app works | +| `tale-sandbox-llm-gateway` | LLM gateway for harness turns | Harness turns can't reach a model; chat is unaffected | +| `tale-sandbox-egress` | Network egress for sandboxed code | `Run code` tool errors with "egress denied"; web render fails | +| `tale-sandbox` | Sandbox runtime + headless browser for web render and document generation | `Run code`, web crawl render, and document generation all fail | -One container is exposed to the public network (`tale-proxy` for HTTPS, and optionally `tale-sandbox-egress` outbound for the sandbox); the rest are internal-only. +`backend-api` and `backend-worker` are the same image as `tale-platform` started in a different role, and both scale independently — `docker compose up -d --scale backend-worker=3` is a supported topology, which is why the shipped compose file gives them no fixed container name. Address them by service name. A `tale deploy` stack names them `-backend-api` and `-backend-worker`. + +`tale-knowledge-db` is its own container in the shipped compose file. A single-host `tale deploy` stack folds the corpus into `tale-db` instead and gives that container the `knowledge-db` network alias, so the same connection string resolves either way — if `tale status` shows no knowledge database, that is why, and `tale-db` is the container to look at. + +One container is exposed to the public network (`tale-proxy` for HTTPS, and optionally `tale-sandbox-egress` outbound for the sandbox); the rest are internal-only, including the blob store — blobs reach the browser through presigned URLs that the proxy forwards under the bucket path. ## The request path A chat message takes one round trip through the containers: 1. Browser → `tale-proxy` (TLS terminated). -2. `tale-proxy` → `tale-platform` for HTML/JS, → `tale-convex` for API + WebSocket. -3. `tale-convex` reads the org's provider config, picks the model, opens a stream to the upstream provider. -4. If the agent retrieves knowledge: `tale-convex` runs the RAG search in-process, querying `tale-knowledge-db` directly — no separate retrieval service in the path. -5. If the agent runs code: `tale-convex` → `tale-sandbox` → `tale-sandbox-egress` for any outbound network. -6. The provider stream tokens back through `tale-convex` to the browser over the WebSocket. +2. `tale-proxy` → `tale-platform` for HTML, JS, and the static assets → `backend-api` for everything under `/api/`, plus `/events`, `/dav`, and the machine API. +3. `backend-api` reads the org's provider config, picks the model, opens a stream to the upstream provider, and streams tokens back to the browser over server-sent events. +4. If the agent retrieves knowledge: `backend-api` runs the search in-process, querying the corpus database directly — no separate retrieval service in the path. +5. If the agent runs code: `backend-api` → `tale-sandbox` → `tale-sandbox-egress` for any outbound network. +6. Anything the turn deferred — indexing a new upload, a follow-up automation — commits to the job queue in the same transaction as the write, and `backend-worker` picks it up. + +Alongside the token stream the browser holds one long-lived `GET /events` connection to `backend-api`. It carries no data, only invalidation hints: the app refetches the affected query when a hint arrives. A dead hint stream therefore looks like a UI that has stopped updating on its own, not like an outage. -The hot path is short. If chat latency feels wrong, the container to blame is almost always the upstream provider, not Tale; the metrics endpoints on `tale-convex` (which now carries the RAG and crawl timings as well) surface the time spent in each hop. +The hot path is short. If chat latency feels wrong, the container to blame is almost always the upstream provider, not Tale; the backend's own request histograms on `/metrics/backend` surface the time spent in each hop. ## The sandbox plane Sandboxed code execution runs in `tale-sandbox` with `tale-sandbox-egress` as the only network seam. The two-container split is deliberate: `tale-sandbox` itself has no outbound network; every request the sandboxed code makes goes through `tale-sandbox-egress`, which blocks cloud-metadata and private-range targets at the IP layer and — when the operator sets `SANDBOX_EGRESS_ALLOWLIST` — enforces a default-deny hostname allowlist on top. If the egress container is down, sandboxed code that needs the network fails closed with "egress denied" — not a silent timeout. -The sandbox runtime carries Chromium and Playwright, so the convex backend reuses it for the headless work it cannot do in-process: rendering a JavaScript page during a web crawl, and turning generated HTML into a PDF or image. Those jobs run as ephemeral sandbox executions rather than user code, but they ride the same egress and isolation seam. The sandbox is the only container that runs untrusted-ish code (user-supplied skill scripts, agent `Run code` invocations); the rest of the stack runs the platform's own code. +The sandbox runtime carries Chromium and Playwright, so the backend reuses it for the headless work it cannot do in-process: rendering a JavaScript page during a web crawl, and turning generated HTML into a PDF or image. Those jobs run as ephemeral sandbox executions rather than user code, but they ride the same egress and isolation seam. The sandbox is the only container that runs untrusted-ish code (user-supplied skill scripts, agent `Run code` invocations); the rest of the stack runs the platform's own code. ## Failure modes — what each container's outage looks like -**`tale-proxy` down.** TLS handshake fails; every client sees a connection error. Inside the host, the platform and convex containers are still up — restart proxy first. +**`tale-proxy` down.** TLS handshake fails; every client sees a connection error. Inside the host, the platform and backend containers are still up — restart proxy first. + +**`tale-platform` down.** Browser gets 502 from proxy; the API keeps working. Existing browser tabs with cached assets continue to talk to the backend and may not notice until they reload. + +**`backend-api` down.** Browser loads the UI shell but nothing populates, and the public `/status` page reads `outage` — that page's only probe is this tier's `/ping`. Restarting is safe: sessions live in Postgres, and the browser re-establishes its hint stream and refetches on reconnect. -**`tale-platform` down.** Browser gets 502 from proxy; the API keeps working. Existing browser tabs with cached assets continue to talk to convex over the WebSocket and may not notice until they reload. +**`backend-worker` down.** Nothing breaks in front of the user, which is what makes this one easy to miss. Requests keep being served, but nothing deferred runs: uploads stay in "indexing", automations do not fire, scheduled sweeps stop. The work is not lost — pg-boss holds the jobs in Postgres and the worker drains the backlog when it comes back. Watch `tale_backend_jobs{state="created"}` climbing on `/metrics/backend`, because the container itself has no healthcheck (it serves no HTTP), so `tale status` will only ever say `running`. -**`tale-convex` down.** Browser loads the UI shell but nothing populates. WebSocket reconnects loop. Restarting convex is safe — sessions are server-side; clients re-subscribe on reconnect. +**`tale-db` down.** Every write is refused and most reads with it; sign-in fails, and the job queue stops accepting work. Nothing degrades gracefully here — the database is the store of record for the application, the queue, and the sessions. -**`tale-db` down.** Convex enters its degraded mode: reads from cache, writes are queued. Long outages eventually surface as "saving failed" toasts. +**`tale-knowledge-db` down.** Document ingestion fails and knowledge search returns empty — agents that retrieve knowledge get an empty result set and a warning in the execution log. The rest of the app keeps working; chats without knowledge are unaffected. Restarting the container clears it, and in-flight uploads retry on the next pass. On a stack that folded the corpus into `tale-db`, this failure and the one above are the same failure. -**`tale-knowledge-db` down.** Document ingestion fails and knowledge search returns empty — agents that retrieve knowledge get an empty result set and a warning in the execution log. The rest of the app keeps working; chats without knowledge are unaffected. Restarting the container clears it, and in-flight uploads retry on the next pass. +**`tale-object-store` down.** Uploading a file fails, and so does opening one already uploaded — a document list still renders from the database, but every download 5xx's. Chat, tasks, and automations that touch no files are unaffected. The store is also where a per-organisation bring-your-own bucket is not involved: an org pointed at its own S3 keeps working while the bundled store is down. -**`tale-sandbox` / `tale-sandbox-egress` down.** `Run code` tool calls return an error and skill scripts fail. Because the convex backend renders web pages and generates documents through the sandbox runtime, a web crawl that needs JavaScript rendering and document generation also fail closed while the sandbox is down. Agents that use none of these keep working. +**`tale-sandbox` / `tale-sandbox-egress` down.** `Run code` tool calls return an error and skill scripts fail. Because the backend renders web pages and generates documents through the sandbox runtime, a web crawl that needs JavaScript rendering and document generation also fail closed while the sandbox is down. Agents that use none of these keep working. -**`tale-sandbox-llm-gateway` down.** Harness turns lose their path to a model provider. Regular chat — which calls providers directly from convex, not through the LLM gateway — is unaffected. +**`tale-sandbox-llm-gateway` down.** Harness turns lose their path to a model provider. Regular chat — which calls providers directly from the backend, not through the LLM gateway — is unaffected. ## Where this fits diff --git a/docs/en/self-hosted/operate/observability/operations.md b/docs/en/self-hosted/operate/observability/operations.md index 3b1502a1ee..d29e1b1925 100644 --- a/docs/en/self-hosted/operate/observability/operations.md +++ b/docs/en/self-hosted/operate/observability/operations.md @@ -12,29 +12,47 @@ The symptom-first index is at [Troubleshooting](/self-hosted/operate/observabili | Signal | Severity | Why it matters | | ------------------------------------------- | -------- | --------------------------------------------------- | | `tale-proxy` health probe failing > 1 min | page | Every user sees a connection error | -| `tale-platform` HTTP 5xx rate > 5 % | page | The UI is broken for a meaningful share of requests | -| `tale-convex` WebSocket reconnect storm | page | UI loads but no data flows | +| `tale-platform` health probe failing | page | The UI stops loading; the proxy answers 502 | +| `backend-api` HTTP 5xx rate > 5 % | page | Every request the app makes goes through this tier | | Postgres connections > 80 % of pool | warn | The next spike will start blocking | | `db-data` volume > 80 % full | warn | The operational Postgres goes read-only at full | | `knowledge-db-data` volume > 80 % full | warn | Ingestion fails when the corpus database is full | -| `tale-knowledge-db` unreachable from convex | warn | Knowledge search returns empty; ingestion stalls | +| `tale-knowledge-db` unreachable | warn | Knowledge search returns empty; ingestion stalls | +| `tale_backend_jobs{state="created"}` rising | warn | The worker has stalled; nothing deferred is running | +| `tale_backend_jobs{state="failed"}` growing | warn | Jobs are exhausting their retries | +| `tale-object-store` health probe failing | page | No file can be uploaded or opened | | Provider request error rate > 20 % | warn | The upstream LLM provider is having a bad day | | Daily backup did not write | page | Restore drill will fail at the worst moment | | TLS cert renewal failed | warn | Renews 30 d before expiry — you have time | -The first two pages are the actually-customer-impacting ones. The warns are catching trends before they tip into page territory. +The pages are the actually-customer-impacting ones. The warns are catching trends before they tip into page territory. + +The 5xx rate comes from `tale_backend_http_requests_total{status="5xx"}` on `/metrics/backend`. The web tier emits no request series of its own — it serves static files — so its failures are visible as a failing container health probe and as 502s at the proxy, not as a Tale metric. ## Log signals to grep for -Logs come through stdout per container, captured by Docker's `json-file` driver. The four phrases that consistently mean trouble: +Logs come through stdout per container, captured by Docker's `json-file` driver. The backend prefixes its own lines with `[backend]`, and it logs no per-request line at all — requests are metrics, not log entries — so a quiet `backend-api` log is normal. The phrases that consistently mean trouble: -- `panic` or `unexpected error` in `tale-convex` logs — Convex action crash. -- `decryption failed` in `tale-platform` logs — SOPS age key mismatch with the file on disk. +- `[backend] fatal startup error` in `backend-api` or `backend-worker` — the process could not boot. Usually a bad `DATABASE_URL` or a migration that will not apply. +- `[backend] task (job ) failed` in `backend-worker` — a background job threw. Repeated for the same task name is the tell that it will exhaust its retries. +- `[backend] pg-boss error` in `backend-worker` — the queue engine itself is unhappy, which usually means Postgres is. +- `decryption failed` in a backend log — SOPS age key mismatch with the file on disk. - `429 Too Many Requests` repeated from a provider — rate limit hit, agents will start failing. -- `connection refused` or `ECONNREFUSED` to `knowledge-db` in `tale-convex` logs — the backend cannot reach the corpus database; ingestion and knowledge search fail. +- `connection refused` or `ECONNREFUSED` to `knowledge-db` in a backend log — the corpus database is unreachable; ingestion and knowledge search fail. Pipe these to your aggregator as derived alerts; the metrics endpoints do not surface them as gauges. +## Inspecting the job queue + +There is no queue UI and no CLI subcommand for jobs. Two doors exist, and both are enough. The gauge `tale_backend_jobs{state}` on `/metrics/backend` is the one to alert on. When you need the detail — which task, which payload — query the queue table directly in the application database: + +```bash +docker compose exec db psql -U tale -d tale_app \ + -c "SELECT name, state, count(*) FROM pgboss.job GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 20;" +``` + +`name` is the task identifier, one queue per identifier. A backlog concentrated on one name is a stuck task; a backlog spread across all of them is a stopped worker. + ## Oncall checklist When a page lands, the first five minutes follow the same shape every time. @@ -58,7 +76,7 @@ Two response-time budgets are tracked as first-class signals: interactive dialog | Dialog input | mean | ~1 s | 30 m | `tale_dialog_ttft_seconds` | | Long operation | mean | ~40 s | 6 h | `tale_long_operation_seconds` | -Each target also rides the platform metrics endpoint as `tale_sla_target_seconds{sla,statistic}`, so a Grafana panel draws the budget line straight from Prometheus instead of hard-coding it. The underlying latency series are the Convex function-execution histograms on `/metrics/convex`; relabel or record them to the names above so the rules resolve. The platform serves the ready-made recording and alerting rules at `/metrics/sla-rules` (behind the same bearer token as the other metrics paths) — fetch it once and reference the file under `rule_files:`, or paste the equivalent: +Each target also rides the metrics endpoints as `tale_sla_target_seconds{sla,statistic}`, so a Grafana panel draws the budget line straight from Prometheus instead of hard-coding it. The `Underlying series` names above are not emitted directly — derive them from the backend's request histogram `tale_backend_http_request_duration_seconds` with a recording rule, so the SLA aggregation stays correct whichever route class carries the operation. The platform serves the ready-made recording and alerting rules at `/metrics/sla-rules` (behind the same bearer token as the other metrics paths) — fetch it once and reference the file under `rule_files:`, or paste the equivalent: ```yaml groups: diff --git a/docs/en/self-hosted/operate/observability/prometheus-grafana.md b/docs/en/self-hosted/operate/observability/prometheus-grafana.md index cf77f72615..3d5b2f217a 100644 --- a/docs/en/self-hosted/operate/observability/prometheus-grafana.md +++ b/docs/en/self-hosted/operate/observability/prometheus-grafana.md @@ -1,15 +1,15 @@ --- title: Prometheus and Grafana -description: A copy-paste Prometheus and Grafana stack that scrapes Tale's two metrics endpoints, plus a starter dashboard and a first alert rule. +description: A copy-paste Prometheus and Grafana stack that scrapes Tale's metrics endpoints, plus a starter dashboard and a first alert rule. --- -This is the worked example behind [Observability config](/self-hosted/configuration/observability-config): a Prometheus and Grafana pair you can drop next to Tale, pointed at the two bearer-token metrics endpoints, with a starter dashboard and one alert rule to build on. It's for self-hosted operators who have already set `METRICS_BEARER_TOKEN` and now want live graphs instead of a `curl` against `/metrics`. +This is the worked example behind [Observability config](/self-hosted/configuration/observability-config): a Prometheus and Grafana pair you can drop next to Tale, pointed at the bearer-token metrics endpoints, with a starter dashboard and one alert rule to build on. It's for self-hosted operators who have already set `METRICS_BEARER_TOKEN` and now want live graphs instead of a `curl` against `/metrics`. The config-reference page lists the endpoints and the single scrape stanza; this page stands the whole stack up end to end. Everything here runs on the same host as Tale, so no metric leaves the box. ## Before you start -Set `METRICS_BEARER_TOKEN` in your `.env` and restart the proxy — without it the two endpoints return 401 to every request, and Prometheus will show each target as down. The endpoints, and what each one carries, are the table in [Observability config](/self-hosted/configuration/observability-config#metrics): `/metrics/platform` and `/metrics/convex` (the latter now carries the in-process RAG and crawl timings), both served by `tale-proxy` over the same hostname as the app. +Set `METRICS_BEARER_TOKEN` in your `.env` and restart the proxy — without it every endpoint returns 401, and Prometheus will show each target as down. The endpoints, and what each one carries, are the table in [Observability config](/self-hosted/configuration/observability-config#metrics): `/metrics/backend` and `/metrics/platform`, both served by `tale-proxy` over the same hostname as the app. Scrape `/metrics/backend` first — it is the tier that serves every request. ## Add Prometheus and Grafana to your stack @@ -45,22 +45,22 @@ volumes: ## Scrape configuration -Tale's two endpoints share one bearer token, so the scrape config is the published stanza repeated once per path. Save this as `prometheus.yml` next to the override above and substitute your host and token — Prometheus reads the token from the file, so keep it `chmod 600` and out of version control. +Tale's endpoints share one bearer token, so the scrape config is the published stanza repeated once per path. Save this as `prometheus.yml` next to the override above and substitute your host and token — Prometheus reads the token from the file, so keep it `chmod 600` and out of version control. ```yaml global: scrape_interval: 30s scrape_configs: - - job_name: tale-platform + - job_name: tale-backend scheme: https - metrics_path: /metrics/platform + metrics_path: /metrics/backend authorization: { credentials: '${METRICS_BEARER_TOKEN}' } static_configs: - targets: ['tale.example.com'] - - job_name: tale-convex + - job_name: tale-platform scheme: https - metrics_path: /metrics/convex + metrics_path: /metrics/platform authorization: { credentials: '${METRICS_BEARER_TOKEN}' } static_configs: - targets: ['tale.example.com'] @@ -72,14 +72,18 @@ Open `http://127.0.0.1:9090/targets` after start — both jobs should read **UP* Point Grafana at Prometheus first — add a Prometheus data source at `http://prometheus:9090` (Grafana reaches it by the compose service name). Then build a dashboard from these panels; the first three use metrics that are always present, and the rest map to the signals in [Operations](/self-hosted/operate/observability/operations). -| Panel | Query | Reads as | -| --------------- | ---------------------------------------------------- | ------------------------------------------------- | -| Targets up | `up{job=~"tale-.*"}` | `1` per healthy endpoint, `0` when scraping fails | -| Platform memory | `process_resident_memory_bytes{job="tale-platform"}` | Resident memory of the platform container | -| Event-loop lag | `nodejs_eventloop_lag_seconds{job="tale-platform"}` | Spikes when the platform is saturated | -| Convex up | `up{job="tale-convex"}` | Backend reachability — `0` is a page | +| Panel | Query | Reads as | +| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| Targets up | `up{job=~"tale-.*"}` | `1` per healthy endpoint, `0` when scraping fails | +| Backend 5xx | `sum(rate(tale_backend_http_requests_total{status="5xx"}[5m])) / sum(rate(tale_backend_http_requests_total[5m]))` | Share of requests failing — the customer-impacting signal | +| Request latency| `histogram_quantile(0.95, sum by (le) (rate(tale_backend_http_request_duration_seconds_bucket[5m])))` | p95 across route classes; break down by `route` for detail | +| Queue backlog | `tale_backend_jobs{state="created"}` | Work waiting for a worker — a rising floor means it stalled | +| Failed jobs | `tale_backend_jobs{state="failed"}` | Jobs that exhausted their retries | +| Live turns | `tale_backend_generations_inflight` | Chat generations running right now | +| Hint streams | `tale_backend_hint_streams_open` | Connected browsers; `0` with users online means SSE is broken | +| Event-loop lag | `nodejs_eventloop_lag_seconds{job="tale-platform"}` | Spikes when the web tier is saturated | -The platform endpoint carries Node's default process metrics (CPU, memory, event-loop lag, GC), which is why the concrete queries above target it. The Convex endpoint exposes its own richer series, including the in-process RAG and crawl timings — open it once (`curl -H "Authorization: Bearer $TOKEN" https://tale.example.com/metrics/convex`) to read the exact metric names your version exposes, then add panels for knowledge-ingestion throughput and provider error rate called out in Operations. +Both endpoints carry Node's default process metrics (CPU, memory, event-loop lag, GC). The application series above are the backend's own, and the `route` label is a bounded class (`/api/app/`, `/api/v1`, `/dav`, `/events`, …) rather than the raw path, so breaking a panel down by route never explodes the series count. Open the endpoint once (`curl -H "Authorization: Bearer $TOKEN" https://tale.example.com/metrics/backend`) to read the exact names your version exposes. ## A first alert rule @@ -97,10 +101,10 @@ groups: summary: 'Tale metrics target {{ $labels.job }} is down' ``` -The full list of what's worth paging on versus what can wait — platform 5xx rate, Postgres pool saturation, knowledge-database reachability, daily-backup-did-not-write — is the signal table in [Operations](/self-hosted/operate/observability/operations); translate each row into a rule once the matching series is on your dashboard. +The full list of what's worth paging on versus what can wait — backend 5xx rate, Postgres pool saturation, job-queue backlog, knowledge-database reachability, daily-backup-did-not-write — is the signal table in [Operations](/self-hosted/operate/observability/operations); translate each row into a rule once the matching series is on your dashboard. ## Where this fits -This page turns the two documented metrics endpoints into a running Prometheus and Grafana stack: a compose override, a two-job scrape config, a starter dashboard, and a target-down alert you extend with the Operations thresholds. Keep both services bound to localhost and the bearer token off disk-in-the-clear, and the whole monitoring surface stays on the host with Tale. +This page turns the documented metrics endpoints into a running Prometheus and Grafana stack: a compose override, a two-job scrape config, a starter dashboard, and a target-down alert you extend with the Operations thresholds. Keep both services bound to localhost and the bearer token off disk-in-the-clear, and the whole monitoring surface stays on the host with Tale. The endpoints and the token that gate them are owned by [Observability config](/self-hosted/configuration/observability-config); the thresholds and the oncall checklist are [Operations](/self-hosted/operate/observability/operations). When a panel goes red, the symptom-to-fix lookup is [Troubleshooting](/self-hosted/operate/observability/troubleshooting). diff --git a/docs/en/self-hosted/operate/observability/troubleshooting.md b/docs/en/self-hosted/operate/observability/troubleshooting.md index 24ad5e8612..202ae24d2c 100644 --- a/docs/en/self-hosted/operate/observability/troubleshooting.md +++ b/docs/en/self-hosted/operate/observability/troubleshooting.md @@ -26,24 +26,55 @@ If the mode is already `letsencrypt`, check the proxy logs for ACME failures — ## UI loads but no data appears -The UI shell is static assets served by `tale-platform`; everything else flows through `tale-convex` over a WebSocket. When the WebSocket cannot connect, the shell loads and stays empty. Symptoms: spinners that never resolve, "reconnecting" toasts, the chat input that never accepts a message. +The UI shell is static assets served by `tale-platform`; every request behind it goes to the backend tier. When the backend cannot answer, the shell loads and stays empty. Symptoms: spinners that never resolve, an offline banner, the chat input that never accepts a message. + +Confirm it in one request — the public status page probes exactly this tier and needs no login: + +```bash +curl -sS https://your-host.example.com/status.json +# → {"status":"outage","checkedAt":"...","components":[{"id":"backend","status":"outage"}]} +docker compose logs --tail=200 backend-api +``` + +The backend is probably restarting (look for `[backend] fatal startup error`) or unreachable from the proxy. Restart with `docker compose restart backend-api` — sessions live in Postgres and the browser refetches on reconnect, so the restart is safe. + +## The UI works but stops updating on its own + +Data appears when you reload and then goes stale: someone else's change never shows up, a finished run keeps saying "running". The browser holds one long-lived `GET /events` connection to `backend-api` that carries invalidation hints, and when it drops there is no error to see — the page simply stops being told anything changed. ```bash -docker compose logs --tail=200 tale-convex +curl -sS -H "Authorization: Bearer $METRICS_BEARER_TOKEN" \ + https://your-host.example.com/metrics/backend | grep hint_streams +# tale_backend_hint_streams_open 0 ``` -The convex container is probably restarting (look for `panic` in the logs) or unreachable from the proxy. Restart with `docker compose restart tale-convex` — sessions are server-side and clients re-subscribe on reconnect, so the restart is safe. +Zero open streams with users signed in means the lane is being cut. The usual cause is something between the browser and the backend buffering or timing out a streaming response — a corporate proxy, a CDN, or an added reverse proxy in front of Caddy. Tale's own proxy disables buffering on that path; anything you put in front of it must do the same. ## Uploads stuck in "indexing" -Document ingestion runs inside the Convex backend and writes the extracted chunks and embeddings to the knowledge corpus database. A long "indexing" state means either the backend cannot reach `tale-knowledge-db` or the file itself failed to extract. Check the convex logs and the corpus database first: +Document ingestion is a background job. `backend-worker` picks it up, extracts the text, embeds it, and writes the chunks and embeddings to the knowledge corpus database. A long "indexing" state therefore has three suspects, in this order: no worker is running, the worker cannot reach the corpus database, or the file itself failed to extract. + +Start with the queue, because a stopped worker looks exactly like a slow one: + +```bash +docker compose exec db psql -U tale -d tale_app \ + -c "SELECT name, state, count(*) FROM pgboss.job WHERE name LIKE 'rag.%' GROUP BY 1, 2;" +docker compose logs --tail=200 backend-worker | grep -iE "knowledge|ingest|embed|rag" +docker compose ps knowledge-db +``` + +A backlog in `created` with no `active` rows means the worker is down — start it, and it drains the backlog on its own. If the logs show connection errors to `knowledge-db`, restart the corpus database (`docker compose restart knowledge-db`); ingestion retries on the next pass, so uploads do not have to be re-submitted. If the queue is empty and the database is healthy but one upload is stuck, the file itself is the suspect — corrupt PDFs and password-protected documents land in a failure state and require deletion and re-upload. + +## Uploads or downloads fail outright + +An upload that never starts, or a document that lists but 5xx's on open, points at the blob store rather than the database. Every file lives in an S3-compatible store, and the browser transfers it directly through a presigned URL the proxy forwards. ```bash -docker compose logs --tail=200 tale-convex | grep -iE "knowledge|ingest|embed" -docker compose ps tale-knowledge-db +docker compose ps object-store +docker compose logs --tail=100 object-store ``` -If the logs show connection errors to `knowledge-db`, restart the corpus database (`docker compose restart tale-knowledge-db`); ingestion retries on the next pass, so uploads do not have to be re-submitted. If the database is healthy but a specific upload is stuck, the file itself is the suspect — corrupt PDFs and password-protected documents land in a failure state and require deletion and re-upload. +If the store is healthy, the next suspect is the presigned URL's origin: it is signed against the address the browser uses, so a deployment whose public URL changed without `OBJECT_STORE_PUBLIC_ENDPOINT` (or `SITE_URL`) following it signs URLs the browser cannot reach. An organisation that brought its own bucket is a separate path — check its CORS policy allows your origin for `GET`, `PUT`, and `HEAD`, because the in-app connection test runs server-side and will not catch that. ## Chat replies stop mid-stream @@ -57,14 +88,14 @@ A `429` is the common case. Either the org's budget is hitting the provider's ra ## Saving fails with "saving failed" toast -The convex container could not write to Postgres. Either `tale-db` is down or its disk is full: +The backend could not write to Postgres. Either `tale-db` is down or its disk is full: ```bash -docker compose ps tale-db +docker compose ps db docker compose exec db df -h /var/lib/postgresql/data ``` -A disk at 100 % is the failure that produces the most surprised faces. Free space, restart `tale-db`, and the queued writes flush. If the disk has room, the suspect is connection-pool exhaustion or a lock — restart `tale-convex` to clear the pool. +A disk at 100 % is the failure that produces the most surprised faces. Free space and restart `db`. Note what a full disk costs here: the database holds the application data, the sessions, and the job queue, so a write refusal stops background work too. If the disk has room, the suspect is connection-pool exhaustion or a lock — restart `backend-api` to clear the pool. ## "Run code" tool errors with "egress denied" diff --git a/docs/en/self-hosted/operate/security/cryptography.md b/docs/en/self-hosted/operate/security/cryptography.md index 421afb629a..f30c159e65 100644 --- a/docs/en/self-hosted/operate/security/cryptography.md +++ b/docs/en/self-hosted/operate/security/cryptography.md @@ -11,13 +11,13 @@ The claims here are verified against the source; where a primitive is configurab Tale encrypts two classes of secret at rest, with two different mechanisms. -**Provider API keys** live in `providers/*.secrets.json` and are encrypted with [SOPS](/self-hosted/configuration/secrets-with-sops) using an **age** key. SOPS encrypts each value with **AES-256-GCM** and wraps the data key to the age recipient, whose key agreement is **X25519**. An encrypted value reads `ENC[AES256_GCM,data:…,iv:…,tag:…]` on disk; decryption happens in-process and the age private key never leaves the platform container's memory. +**Provider API keys** live in `providers/*.secrets.json` and are encrypted with [SOPS](/self-hosted/configuration/secrets-with-sops) using an **age** key. SOPS encrypts each value with **AES-256-GCM** and wraps the data key to the age recipient, whose key agreement is **X25519**. An encrypted value reads `ENC[AES256_GCM,data:…,iv:…,tag:…]` on disk; decryption happens in-process and the age private key never leaves the backend container's memory — it is the only tier that reads the config store's secrets. **Application-encrypted fields** — OAuth connector tokens and similar credentials stored in the database — are encrypted with **AES-256-GCM** through a compact JWE (`alg: dir`, `enc: A256GCM`). The 32-byte key comes from `ENCRYPTION_SECRET` (base64) or `ENCRYPTION_SECRET_HEX` (hex); the platform refuses to start the encryption path with a key that is not exactly 32 bytes. -The Convex data store and Postgres volumes are protected by the host: run them on an encrypted filesystem (LUKS, or your cloud provider's volume encryption). Tale does not store credentials in plaintext — a provider key or OAuth token is either SOPS-encrypted on disk or AES-256-GCM-encrypted in the database, never written in the clear. +The Postgres volumes and the blob store are protected by the host: run them on an encrypted filesystem (LUKS, or your cloud provider's volume encryption). Tale does not store credentials in plaintext — a provider key or OAuth token is either SOPS-encrypted on disk or AES-256-GCM-encrypted in the database, never written in the clear. -**Customer PII and application records** — names, email and postal addresses, conversation content — are protected at rest by the same layers that protect the database as a whole: Convex's at-rest encryption, TLS 1.3 in transit, and row-level security that scopes every read to the caller's organisation. +**Customer PII and application records** — names, email and postal addresses, conversation content — are protected by the layers that protect the database as a whole: the encrypted host filesystem at rest, TLS 1.3 in transit, and an organisation scope the backend applies to every read. Be precise about the first of those: Postgres stores these rows unencrypted, so the encryption at rest is the volume's, not the database's. If your regime requires the database itself to hold ciphertext, that is a managed-Postgres or filesystem decision you make below Tale. Application-level field encryption is purpose-built for secrets — provider keys and OAuth tokens, written once and read by a single code path. PII is different: it is filtered, sorted, and looked up by exact value, and the customer table is indexed by organisation and email. Encrypting those columns at the field level would break equality lookups and indexed search — unless paired with a searchable-hash scheme that leaks the very equality it is meant to hide — while adding a key-rotation cost and no protection the encrypted host disk beneath the application doesn't already provide against a stolen volume. diff --git a/docs/en/self-hosted/operate/upgrades.md b/docs/en/self-hosted/operate/upgrades.md index a4a4ee69ff..f175fe8e73 100644 --- a/docs/en/self-hosted/operate/upgrades.md +++ b/docs/en/self-hosted/operate/upgrades.md @@ -46,14 +46,14 @@ tale update --dry-run `tale deploy` does the actual rolling restart, and it always deploys the CLI's own version — which, thanks to alignment, is the version your workspace records. It sorts the services into three tiers: - **App tier** — `platform` — rolls on **every** deploy with zero downtime (blue-green: the new colour starts alongside the old, healthchecks pass, traffic flips, the old colour drains). -- **Backend & compute** — `convex`, `sandbox`, `sandbox-egress` — roll on every deploy too, so they never version-skew from `platform`. Each is a single container that recreates **in place** when its image actually changed; the deploy first drains its in-flight work (chat generations for `convex`, agent runs for `sandbox`) so the brief restart doesn't cut a live request. -- **Stop-gated tier** — `db`, `proxy` — left **running and untouched** by default (recreating Postgres or the proxy is a brief outage you don't want on a routine roll). Pass `--stop` to update them; the deploy warns and names them when it skips. +- **Backend & compute** — `backend-api`, `backend-worker`, `sandbox`, `sandbox-egress`, `sandbox-llm-gateway` — roll on every deploy too, so they never version-skew from `platform`. The two backend services ship the *same image* as `platform` and share its wire contracts, which is why skew is not an option. Each recreates **in place** when its image actually changed; the deploy first drains its in-flight work (chat generations for the backend, agent runs for the sandbox) so the brief restart doesn't cut a live request. +- **Stop-gated tier** — `db`, `object-store`, `proxy` — left **running and untouched** by default (recreating Postgres, the blob store, or the proxy is a brief outage you don't want on a routine roll). Pass `--stop` to update them; the deploy warns and names them when it skips. ```bash -# After tale update, roll the containers to match (app tier + convex) +# After tale update, roll the containers to match (app tier + backend + sandbox) tale deploy -# Also update db/proxy (brief downtime while they recreate) +# Also update db/object-store/proxy (brief downtime while they recreate) tale deploy --stop # Roll only specific services diff --git a/docs/en/self-hosted/overview.md b/docs/en/self-hosted/overview.md index ea65bcf0a1..22f3e0a405 100644 --- a/docs/en/self-hosted/overview.md +++ b/docs/en/self-hosted/overview.md @@ -1,54 +1,63 @@ --- title: Self-hosted architecture -description: Eight containers, one compose file, two Postgres databases. This page hands you the mental model for what each container does, where data lives on disk, and which secrets matter at first boot. +description: Eleven containers in one compose file, two of them Postgres databases and one an S3-compatible blob store. This page hands you the mental model for what each container does, where data lives on disk, and which secrets matter at first boot. --- -A Tale instance is eight containers behind a Caddy proxy, talking to two Postgres databases — one operational, one for the knowledge corpus; two of them are sandbox containers off to the side for code execution. The compose file is the contract — what runs, what is exposed, what is mounted. This page hands you the mental model so the install, configure, and operate pages do not have to re-explain it. +A Tale instance is eleven containers behind a Caddy proxy, talking to two Postgres databases — one operational, one for the knowledge corpus — and an S3-compatible blob store; two of them are sandbox containers off to the side for code execution. The compose file is the contract — what runs, what is exposed, what is mounted. This page hands you the mental model so the install, configure, and operate pages do not have to re-explain it. Read this before you `docker compose up`. Come back when you are debugging an outage and need to know which container's logs to open first. -## The eight containers +## The eleven containers -**tale-proxy** is Caddy at the edge. It terminates TLS, routes everything under `/` to the platform container, and forwards everything under `/api/` and the Convex paths to the convex container. Healthchecks live here. +**tale-proxy** is Caddy at the edge. It terminates TLS, serves the HTML and static assets from the platform container, and forwards everything under `/api/` — plus `/events`, `/dav`, and the machine API — to the backend. It also publishes the blob store's bucket path so presigned upload and download URLs work in the browser. Healthchecks live here. -**tale-platform** is the React + TanStack Start server. It renders the UI, serves static assets, and is the only container exposed to the browser. It does not hold business state — everything that needs to persist talks to convex. +**tale-platform** is the React + TanStack Start server. It renders the UI, serves static assets, and terminates the live-browser screencast socket. It holds no business state and reaches no database — everything that persists goes through the backend. -**tale-convex** is the backend: the actions, queries, mutations, and the WebSocket layer the UI subscribes to. Provider keys, agent definitions, workflow runs, audit logs all live here. It also runs the knowledge work in-process — document ingestion, web crawling, RAG search, and document generation are Convex node-actions, not separate services. The headless work those jobs need (rendering a web page, turning HTML into a PDF or image) is delegated to the sandbox runtime, which already ships Chromium and Playwright. +**backend-api** is the application backend: a Node process running a Hono app that serves every door the UI and the machine API use — sign-in, the app API, WebDAV, the live-update stream. Provider keys, agent definitions, workflow runs, and audit logs live behind it. Knowledge *search* runs in this process, querying the corpus database directly rather than through a separate retrieval service. -**tale-db** is the operational Postgres (ParadeDB). It holds the Convex backend data — agents, runs, the audit log — and is one of the two stateful containers that matter for backups. +**backend-worker** is the same image in the worker role. It runs the background jobs — document ingestion and embedding, web crawling, automation runs, retention sweeps — off a pg-boss queue that lives in the application database, so a job commits in the same transaction as the write that scheduled it. The headless work some of those jobs need (rendering a web page, turning HTML into a PDF or image) is delegated to the sandbox runtime, which already ships Chromium and Playwright. The worker serves no HTTP. -**tale-knowledge-db** is the knowledge corpus Postgres (ParadeDB), the `tale_knowledge` database with two schemas: `private_knowledge` (uploaded-document chunks, embeddings, the BM25 index, the semantic cache) and `public_web` (crawled web pages). It is split from `tale-db` so the corpus — the data-residency-sensitive store — can be relocated or replaced on its own. The Convex backend connects to it directly; nothing else does. +**tale-db** is the operational Postgres (ParadeDB). It holds the `tale_app` database — agents, runs, sessions, the audit log, and the job queue — and the backend applies its schema migrations to it at boot, under an advisory lock, so a rolling deploy migrates exactly once. + +**tale-object-store** is the blob store: an S3-compatible MinIO instance holding every uploaded document, chat attachment, audio file, and generated medium. S3-compatible storage is the only blob backend, so a deployment without one refuses every upload. It is internal-only; the backend signs presigned URLs and the proxy forwards them. + +**tale-knowledge-db** is the knowledge corpus Postgres (ParadeDB), the `tale_knowledge` database with two schemas: `private_knowledge` (uploaded-document chunks, embeddings, the BM25 index, the semantic cache) and `public_web` (crawled web pages). Keeping it addressable on its own connection string is what lets the corpus — the data-residency-sensitive store — be relocated or replaced independently. On a single-host `tale deploy` stack it is folded into `tale-db`, which carries the `knowledge-db` network alias so the connection string resolves either way. **tale-sandbox-llm-gateway** is the LLM gateway for harness turns. It is the only path from a sandboxed harness to a model provider; the platform provisions it and mints per-session keys. -**tale-sandbox** and **tale-sandbox-egress** run sandboxed code on behalf of the `Run code` tool and skill scripts, and serve as the headless-browser runtime the convex backend calls for web rendering and document generation. The egress container is the only path the sandbox has to the network. Egress is open by default — sandboxed code reaches any public host over HTTPS while cloud-metadata and private-range targets stay blocked at the IP layer; lock it down to a hostname allowlist with `SANDBOX_EGRESS_ALLOWLIST`, described in [Hardening](/self-hosted/operate/security/hardening). +**bgutil-provider** is a third-party helper for video-link ingestion: it issues the tokens YouTube requires before a transcript can be fetched. It is the only image in the stack Tale does not build, it is internal-only, and a deployment that never ingests video links can stop it without affecting anything else. + +**tale-sandbox** and **tale-sandbox-egress** run sandboxed code on behalf of the `Run code` tool and skill scripts, and serve as the headless-browser runtime the backend calls for web rendering and document generation. The egress container is the only path the sandbox has to the network. Egress is open by default — sandboxed code reaches any public host over HTTPS while cloud-metadata and private-range targets stay blocked at the IP layer; lock it down to a hostname allowlist with `SANDBOX_EGRESS_ALLOWLIST`, described in [Hardening](/self-hosted/operate/security/hardening). ## Data on disk -Four volumes survive a `docker compose down`: +Five volumes survive a `docker compose down`: -- `db-data` — the operational Postgres data directory: the database behind agents, runs, and the audit log. -- `knowledge-db-data` — the knowledge corpus Postgres data directory: document chunks, embeddings, the search indexes, and crawled web pages. Backs up separately from `db-data` because it is a separate database. +- `db-data` — the operational Postgres data directory: the database behind agents, runs, sessions, the audit log, and the job queue. +- `knowledge-db-data` — the knowledge corpus Postgres data directory: document chunks, embeddings, the search indexes, and crawled web pages. Separate from `db-data` because it is a separate database, and absent on a stack that folded the corpus into `tale-db`. +- `object-store-data` — the blob store: every uploaded document, chat attachment, audio file, and generated medium. +- `convex-data` — the org config tree: agents, automations, connectors, providers, skills, governance policies, SSO connections, branding. The name is historical and deliberately unchanged, so that retiring the Convex backend did not force operators to migrate a volume for a rename. - `backups` — checksummed volume snapshots written by `tale backup` and automatically before migrating deploys; [Backups and restore](/self-hosted/operate/backups-and-restore) is the drill. -- The convex object-store mount — uploaded files, generated documents, exported bundles. -Everything else is ephemeral. Containers can be replaced without data loss as long as the volumes survive. +`object-store-data` is the one to notice: a `tale backup` snapshot does **not** include it, so uploaded files need their own place in your backup job. Everything else is ephemeral. Containers can be replaced without data loss as long as the volumes survive. ## Provider secrets and the SOPS layer -Provider keys (OpenAI, Anthropic, Azure, Ollama, etc.) live on disk in a `providers/` directory mounted into the platform container. Each provider has a `.json` and a `.secrets.json`; the secrets file is encrypted with SOPS and the [`SOPS_AGE_KEY`](/self-hosted/configuration/environment-reference) variable. +Config-file secrets — provider secret sidecars, the knowledge and object-storage connection passwords, the deployment config's own secrets — live on disk in the org config tree, encrypted with SOPS and the [`SOPS_AGE_KEY`](/self-hosted/configuration/environment-reference) variable. The backend containers mount that tree read-write and are the only processes that hold the age key; the web tier mounts the same volume read-only for branding images and never decrypts anything. -This split exists for two reasons. Rotating a provider key is editing one file, not re-running the platform; backing up the encrypted file is safe to commit alongside infrastructure. The plaintext mode (no SOPS, secrets in cleartext) is supported for tightly controlled environments where the disk itself is encrypted at rest. +This split exists for two reasons. Rotating a secret is editing one file, not re-running the platform; backing up the encrypted file is safe to commit alongside infrastructure. The plaintext mode (no SOPS, secrets in cleartext) is supported for tightly controlled environments where the disk itself is encrypted at rest. ## Auth and sessions -Sign-in is Better Auth running inside the convex container. Four sign-in modes ship: local password, Microsoft Entra (OAuth/OIDC), generic OIDC, and trusted headers (the reverse proxy provides the identity). The platform container reads the cookie, hands it to convex, and convex decides what the session can do based on the user's role and the per-resource permission matrix documented in [Members and roles](/platform/admin/members-and-roles). +Sign-in is Better Auth running inside the backend. Four sign-in modes ship: local password, Microsoft Entra (OAuth/OIDC), generic OIDC, and trusted headers (the reverse proxy provides the identity). The proxy sends everything under `/api/auth/` straight to `backend-api`, so the web tier is not in the sign-in path at all: the browser holds a session cookie, the backend resolves it on every request, and the backend decides what the session can do from the user's role and the per-resource permission matrix documented in [Members and roles](/platform/admin/members-and-roles). Sessions live in Postgres, which is why restarting a backend container never signs anyone out. The [authentication reference](/self-hosted/configuration/authentication) covers the env vars and the per-mode trade-offs. ## When you outgrow single-host -The default compose file runs all eight containers on one host. The architecture is single-tenant: nothing in the design splits work across hosts. The first thing you can move off the box without re-architecting is the knowledge corpus — `tale-knowledge-db` is a standalone Postgres, so pointing it at managed infrastructure (for capacity or for a residency requirement) is a connection-string change, covered in [Data residency](/self-hosted/configuration/data-residency). The Convex layer is still single-instance; horizontal scaling of the backend is not a v1 feature. +The default compose file runs all eleven containers on one host. The first thing you can move off the box without re-architecting is the knowledge corpus — it is addressed by its own connection string, so pointing it at managed infrastructure (for capacity or for a residency requirement) is a `KNOWLEDGE_DATABASE_URL` change, covered in [Data residency](/self-hosted/configuration/data-residency). The blob store moves the same way, by repointing the deployment's object-storage connection at a bucket you own. + +The backend tier scales out rather than up. `backend-api` and `backend-worker` both take `--scale`: every api container polls the hint outbox and fans updates out to its own clients, so there is no cross-container coordination and no sticky sessions to arrange, and every worker competes for the same pg-boss queue. What stays single is Postgres — one primary, and the blob store beside it. ## Where this fits diff --git a/docs/fr/cloud/data-residency.md b/docs/fr/cloud/data-residency.md index adba903c10..6c6c7772d1 100644 --- a/docs/fr/cloud/data-residency.md +++ b/docs/fr/cloud/data-residency.md @@ -9,7 +9,7 @@ La région par défaut pour les nouvelles organisations Cloud est la Suisse. Cha ## Un exemple déroulé — un aller-retour de chat -L'utilisateur à Zurich ouvre Chat et envoie « résume le dernier appel client ». La requête frappe le edge de Tale dans la région choisie, atterrit sur `tale-platform`, qui appelle dans `tale-convex` (le backend), lit les connaissances depuis la base de connaissances dès que l'outil de connaissances de l'agent les demande, et émet un appel sortant vers le fournisseur derrière le modèle choisi par la personne qui envoie. La récupération de connaissances tourne dans le backend Convex — elle interroge directement la base de connaissances, sans service de récupération séparé sur le chemin. Le fournisseur de modèles retourne des tokens ; Tale les streame en retour sur le même chemin. La réponse et les citations atterrissent dans la base de données opérationnelle, le corpus reste dans la base de connaissances, et les deux sont répliqués dans la région. +L'utilisateur à Zurich ouvre Chat et envoie « résume le dernier appel client ». La requête frappe le edge de Tale dans la région choisie, qui sert la page depuis la couche web et route le message lui-même vers le backend applicatif. Le backend lit les connaissances depuis la base de connaissances dès que l'outil de connaissances de l'agent les demande, et émet un appel sortant vers le fournisseur derrière le modèle choisi par la personne qui envoie. La récupération de connaissances tourne dans ce même processus backend — elle interroge directement la base de connaissances, sans service de récupération séparé sur le chemin. Le fournisseur de modèles retourne des tokens ; le backend les streame vers le navigateur. La réponse et les citations atterrissent dans la base de données opérationnelle, le corpus reste dans la base de connaissances, tout fichier produit par le tour atterrit dans le stockage objet de la région, et les trois sont répliqués dans la région. Deux flèches franchissent la frontière régionale dans ce trajet : l'appel vers le fournisseur de modèles (toujours externe) et tout sous-traitant déclenché par les outils de l'agent (fetch web, lecture OneDrive, serveur MCP dans une autre région). Tout le reste reste dans la région. diff --git a/docs/fr/develop/api-reference.md b/docs/fr/develop/api-reference.md index 316c4a5aec..7dca4a37d5 100644 --- a/docs/fr/develop/api-reference.md +++ b/docs/fr/develop/api-reference.md @@ -240,7 +240,7 @@ curl -sS "https://your-host.example.com/api/v1/tasks/" \ # → 200 { "task": { "id": "", "title": "...", "status": "in_progress", "externalId": "case-991", "labels": [], ... } } ``` -Et récupère les résultats. Ce que l'automatisation a rapporté se trouve dans la discussion de la tâche ; ce qu'elle a déposé arrive comme fichiers dans le dossier du trimestre — les deux se lisent par le même accès. L'endpoint de contenu streame directement un blob stocké dans Convex ; sur une organisation avec son propre stockage objet, il répond **302** vers une URL présignée de courte durée, donc suis les redirections : +Et récupère les résultats. Ce que l'automatisation a rapporté se trouve dans la discussion de la tâche ; ce qu'elle a déposé arrive comme fichiers dans le dossier du trimestre — les deux se lisent par le même accès. L'endpoint de contenu ne streame jamais les octets lui-même : chaque fichier vit dans le stockage objet, donc il répond toujours **302** vers une URL présignée de courte durée. Suis les redirections, et traite cette URL comme un credential — elle donne les octets à quiconque la détient, jusqu'à son expiration : ```bash curl -sS "https://your-host.example.com/api/v1/tasks//comments" \ diff --git a/docs/fr/develop/status-page.md b/docs/fr/develop/status-page.md index 3246e5b1a6..549bf35233 100644 --- a/docs/fr/develop/status-page.md +++ b/docs/fr/develop/status-page.md @@ -21,11 +21,11 @@ Le flux RSS porte chaque changement d'état — ouvert, mise à jour, résolu | Service | Ce qu'il couvre | Quand il passe au rouge | | ---------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | -| `platform` | L'application TanStack Start + Convex — agents, workflows, connectors, UI. | UI injoignable ; l'API renvoie 5xx ; l'auth est cassée. | +| `platform` | Le serveur d'UI TanStack Start et le backend Node derrière — agents, workflows, connectors, UI. | UI injoignable ; l'API renvoie 5xx ; l'auth est cassée. | | `rag` | Le service Python FastAPI de traitement de documents — indexation, récupération. | Les téléversements de documents calent ; la récupération est vide. | | `crawler` | Le service d'extraction web Crawl4AI — utilisé par l'ingestion de documents et le repli Tavily. | Les documents tirés du web échouent ; la recherche profonde cale. | | `proxy` | Le bord Caddy — terminaison TLS, routage HTTP. | Tout le trafic Tale Cloud est touché. | -| `db` | TimescaleDB — état durable pour la couche Convex et les métadonnées de la plateforme. | Écritures refusées ; la ligne platform passe aussi au rouge. | +| `db` | Postgres — l'état applicatif durable et la file de jobs. | Écritures refusées ; la ligne platform passe aussi au rouge. | Chaque ligne porte les 90 derniers jours d'uptime comme un sparkline. Un incident se lit comme une bande colorée sur la ligne ; cliquer la bande ouvre le chronogramme — première mise à jour, suites, résolution, post-mortem quand l'incident en exige un. @@ -37,7 +37,9 @@ La page appartient à la rotation d'astreinte. Les mises à jour sont poussées ## Auto-hébergé : ce qui change -Les instances auto-hébergées n'apparaissent pas sur `status.tale.dev` — cette page couvre Tale Cloud. Chaque déploiement embarque sa propre page de statut à la place, servie par la plateforme et accessible sans connexion à `https:///status`. Elle rend côté serveur un résumé de santé — operational, degraded ou outage — à partir d'une sonde de liveness contre le backend Convex, si bien qu'un opérateur (ou un utilisateur qui vérifie si le souci ne vient que de lui) peut lire la disponibilité sans se connecter. La forme lisible par machine est `https:///status.json`, qui renvoie le même résultat en JSON qu'un moniteur d'uptime peut interroger. +Les instances auto-hébergées n'apparaissent pas sur `status.tale.dev` — cette page couvre Tale Cloud. Chaque déploiement embarque sa propre page de statut à la place, servie par la plateforme et accessible sans connexion à `https:///status`. Elle rend côté serveur un résumé de santé — operational, degraded ou outage — à partir d'une sonde de liveness contre la route `/ping` de la couche backend, celle-là même que le healthcheck du conteneur `backend-api` utilise. Un opérateur (ou un utilisateur qui vérifie si le souci ne vient que de lui) lit donc la disponibilité sans se connecter. La forme lisible par machine est `https:///status.json`, qui renvoie le même résultat en JSON qu'un moniteur d'uptime peut interroger. + +La sonde ne rapporte qu'un composant, `backend`, parce que cette couche sert chaque requête de l'app : si elle répond, les données circulent. La liveness du conteneur de plateforme est implicite — sans elle, la page de statut n'aurait pas pu s'afficher. Les résultats sont en cache cinq secondes et chaque sonde expire au bout de deux, donc pointer un moniteur d'uptime sur `/status.json` à intervalle serré ne coûte presque rien au backend. Cette page rapporte la disponibilité du déploiement lui-même. Pour un signal d'exploitation plus fin — santé des conteneurs depuis `tale status`, métriques de requêtes depuis les journaux Caddy, et événements du plan de contrôle dans le journal d'audit du produit — la [page de dépannage observabilité](/fr/self-hosted/operate/observability/troubleshooting) associe les symptômes aux journaux. diff --git a/docs/fr/develop/webdav-api.md b/docs/fr/develop/webdav-api.md index 92bae0ec76..b5826882e2 100644 --- a/docs/fr/develop/webdav-api.md +++ b/docs/fr/develop/webdav-api.md @@ -39,11 +39,11 @@ Chaque requête authentifiée vérifie aussi que l’utilisateur est membre acti | PROPFIND | Lister une ressource (Depth 0) ou les enfants directs d’une collection (Depth 1). La liste de propriétés émise est documentée plus bas. **Depth: infinity est rejeté avec 403** pour éviter des réponses sans borne. | Requise | | PROPPATCH | Renvoie succès 207 par propriété sans stocker les valeurs. Les dead properties ne sont pas persistées en v1 ; PROPPATCH réussit de manière optimiste pour la compatibilité client. | Requise | | GET / HEAD | Streamer le blob du document. Pose `Content-Type`, `Content-Length`, `ETag` et `Last-Modified`. GET sur une collection renvoie 405. | Requise | -| PUT | Créer ou remplacer un document. Le nouveau blob est stocké dans le stockage Convex avec déduplication par hash ; la ligne du document reçoit `sourceProvider: "webdav"`. Renvoie 201 à la création, 204 à l’écrasement. | Requise | +| PUT | Créer ou remplacer un document. Le corps est streamé dans le stockage objet de l’organisation sous une clé neuve ; la ligne du document reçoit `sourceProvider: "webdav"`. Renvoie 201 à la création, 204 à l’écrasement. Une requête sans `Content-Length` (transfert chunké) est refusée — la pré-signature a besoin de la taille à l’avance. | Requise | | DELETE | Soft-supprimer un document (`lifecycleStatus: "trashed"`) ou un dossier (corbeille en cascade sur les documents contenus, hard-supprime les lignes de dossier). Renvoie 204. | Requise | | MKCOL | Créer un dossier sous un parent existant. Corps vide uniquement. Renvoie 201, 405 si la cible existe, 409 si le parent manque. | Requise | | MOVE | Renommer ou déplacer. Atomique pour les documents. Pour les dossiers, met à jour le `parentId` du dossier déplacé. Respecte `Overwrite: T/F` et `If`. Renvoie 201 (nouvelle destination) ou 204 (écrasement). | Requise | -| COPY | Copie côté serveur. Les copies de documents réutilisent l’identifiant de stockage Convex (déduplication). Les copies de dossiers sont récursives. Respecte `Overwrite` et `If`. | Requise | +| COPY | Copie côté serveur. Une copie de document est une seconde ligne qui pointe vers le même objet stocké — aucun octet ne bouge, et l’objet survit jusqu’à ce que la dernière ligne qui le référence disparaisse. Les copies de dossiers sont récursives. Respecte `Overwrite` et `If`. | Requise | | LOCK | Verrou d’écriture Class 2 exclusif ou partagé. Timeout depuis le header `Timeout: Second-N`, plafonné à 3600. Rafraîchissement en renvoyant LOCK avec `If: ()` et un corps vide. | Requise | | UNLOCK | Libérer un verrou par son jeton. Seul le propriétaire peut libérer. Renvoie 204. | Requise | @@ -67,7 +67,7 @@ Les dead properties ne sont pas stockées. PROPPATCH renvoie 200 pour une dead p ## Sémantique des verrous -Les verrous vivent dans leur propre table Convex, indexés par `(organizationId, resourcePath)`. La forme filaire est `opaquelocktoken:`. Le serveur : +Les verrous vivent dans leur propre table Postgres, indexés par `(organizationId, resourcePath)`. La forme filaire est `opaquelocktoken:`. Le serveur : - Plafonne le timeout à 3600 secondes. Les requêtes pour des fenêtres plus longues sont silencieusement bornées. - Traite `LOCK` avec un header `If: ()` et un corps vide comme un refresh — l’expiration du verrou existant est repoussée. @@ -111,16 +111,16 @@ Le serveur annonce `DAV: 1, 2` dans la réponse OPTIONS. - `Depth: infinity` sur PROPFIND est rejeté avec `403`. - `Timeout: Second-N` sur LOCK est borné à `[1, 3600]`. -- La taille du corps PUT est plafonnée à **5 Go** par défaut (`413` au-delà), appliquée à la fois au reverse-proxy et dans le serveur de plateforme. Les opérateurs peuvent l’ajuster via la variable d’environnement `WEBDAV_MAX_PUT_BYTES`. Le corps est streamé vers une URL pré-signée Convex sans qu’un gros upload soit mis en mémoire tampon côté plateforme. +- La taille du corps PUT est plafonnée à **5 Go** par défaut (`413` au-delà), appliquée à la fois au reverse-proxy et dans le backend. Les opérateurs l’ajustent via la variable d’environnement `WEBDAV_MAX_PUT_BYTES` — définis-la aussi sur le conteneur du proxy, sinon c’est le proxy qui reste le plafond effectif. Le corps est streamé vers une URL pré-signée du stockage objet sans qu’un gros upload soit mis en mémoire tampon côté backend. - Les corps XML (PROPFIND / PROPPATCH / MKCOL / LOCK) sont plafonnés à **64 Ko** (`413` au-delà) — ces enveloppes sont minuscules par conception. - Les mots de passe applicatifs sont hachés avec HMAC-SHA256 ; le secret n’apparaît dans aucune réponse après l’appel de création. - `lastUsedAt` est patché au plus une fois par minute par mot de passe applicatif pour éviter les write-storms sur les montages actifs. ## Prérequis réseau -Le point de terminaison WebDAV tourne dans le serveur Hono de la plateforme (`platform:3000` en compose). Caddy route `/dav/*` vers lui via le fallback par défaut — aucune configuration supplémentaire n’est requise. Le chemin requiert que le serveur de plateforme ait `ADMIN_KEY` défini dans son environnement pour appeler les requêtes internes Convex avec auth admin. +Le point de terminaison WebDAV est servi par la couche backend (`backend-api:3005` en compose). Caddy a son propre bloc `handle /dav/*` qui y renvoie et applique le plafond de corps — aucune configuration supplémentaire n’est requise. Le point de terminaison n’a besoin d’aucun credential de déploiement : chaque requête s’authentifie avec son propre mot de passe applicatif, et les handlers lisent et écrivent Postgres dans le même processus. -Pour le dev (`bun dev`), le même dispatch est monté comme middleware Vite (`vite-plugins/serve-webdav.ts`) — `curl` et les clients peuvent atteindre `http://localhost:3000/dav//...` contre un serveur dev qui tourne sans rebuild. +Pour le dev (`bun run dev`), Vite proxie `/dav` vers ce même backend, donc `curl` et les clients montés atteignent `http://localhost:3000/dav//...` contre un serveur dev qui tourne. ## Sécurité diff --git a/docs/fr/self-hosted/configuration/data-residency.md b/docs/fr/self-hosted/configuration/data-residency.md index 48cb142884..5d661aa7c2 100644 --- a/docs/fr/self-hosted/configuration/data-residency.md +++ b/docs/fr/self-hosted/configuration/data-residency.md @@ -3,29 +3,35 @@ title: Résidence des données description: Pointe la base de connaissances, la base de données applicative et le stockage des fichiers téléversés d'une installation Tale auto-hébergée vers une infrastructure que tu contrôles — configuré par les administrateurs dans Paramètres > Résidence des données et appliqué au redémarrage. --- -Une installation Tale auto-hébergée tourne sur une infrastructure que tu contrôles déjà, donc ses données vivent sur tes hôtes par défaut. La **résidence des données** sert au cas où tu veux pointer des banques de données précises vers ton propre Postgres géré ou ton stockage objet plutôt que vers les conteneurs fournis — par exemple pour garder le texte des documents dans une base que ton équipe exploite, ou les fichiers téléversés dans ton propre bucket S3. Le corpus de connaissances tourne comme son propre conteneur (`knowledge-db`) précisément pour pouvoir être relocalisé ou remplacé indépendamment de la base opérationnelle — c'est la banque qui compte le plus pour la majorité des exigences de résidence. Les administrateurs configurent cela dans **Paramètres > Résidence des données** ; le changement est écrit dans un seul fichier de configuration au niveau du déploiement et **prend effet au redémarrage des conteneurs concernés**. +Une installation Tale auto-hébergée tourne sur une infrastructure que tu contrôles déjà, donc ses données vivent sur tes hôtes par défaut. La **résidence des données** sert au cas où tu veux pointer des banques de données précises vers ton propre Postgres géré ou ton stockage objet plutôt que vers les conteneurs fournis — par exemple pour garder le texte des documents dans une base que ton équipe exploite, ou les fichiers téléversés dans ton propre bucket S3. Le corpus de connaissances est une base à part, adressée par sa propre chaîne de connexion, précisément pour pouvoir être relocalisé ou remplacé indépendamment de la base opérationnelle — c'est la banque qui compte le plus pour la majorité des exigences de résidence. -Cette page couvre ce qui peut être déplacé, le seul prérequis qui mord (ParadeDB), comment la configuration est stockée et appliquée, et comment redémarrer sans risque. +Deux mécanismes se cachent derrière. Une banque **à l'échelle du déploiement** se repointe sur l'hôte, dans `.env` et l'arbre de config, et prend effet au redémarrage des conteneurs backend. Une banque **propre à une organisation** se configure par un owner ou un admin de l'organisation dans **Paramètres > Résidence des données**, atterrit dans le répertoire de config de cette organisation, et prend effet à la requête suivante. Cette page couvre les deux, le seul prérequis qui mord (ParadeDB), comment la configuration est stockée, et comment redémarrer sans risque. ## Activer la modification -**Paramètres > Résidence des données** est une seule page avec deux familles de sections : les banques à l'échelle du déploiement que toutes les organisations partagent, et celles qu'une organisation apporte pour elle seule. Chaque section s'affiche en lecture seule ou modifiable selon ce que la personne qui la lit a le droit de changer, et la page nomme l'état dans lequel tu te trouves. Voir la page est ouvert à tout owner ou admin d'une organisation ; **modifier les banques du déploiement** — repointer une banque de données, enregistrer des secrets, lancer un test de connexion ou appliquer un redémarrage — est réservé à une allowlist nommée d'opérateurs. Liste leurs courriels de connexion (séparés par des virgules) dans `.env` et redémarre : +**Paramètres > Résidence des données** est une seule page avec deux familles de sections : les banques à l'échelle du déploiement que toutes les organisations partagent, et celles qu'une organisation apporte pour elle seule. Chaque section s'affiche en lecture seule ou modifiable selon ce que la personne qui la lit a le droit de changer, et la page nomme l'état dans lequel tu te trouves. Voir la page est ouvert à tout owner ou admin d'une organisation ; **modifier les banques du déploiement** — repointer une banque de données, enregistrer des secrets, lancer un test de connexion — est réservé à une allowlist nommée d'opérateurs. Liste leurs courriels de connexion (séparés par des virgules) dans `.env` et redémarre : ```bash TALE_DEPLOYMENT_CONFIG_ADMINS=alice@example.com,bob@example.com ``` -Si l'allowlist est vide ou non définie, les sections de déploiement montrent toujours la configuration actuelle aux administrateurs, mais en lecture seule — les actions d'en-tête **Enregistrer le déploiement** et **Appliquer & redémarrer** n'apparaissent que pour les opérateurs de l'allowlist. Seul un admin connecté dont le courriel figure sur la liste rend ces sections modifiables ; la page t'indique quel courriel ajouter. Les entrypoints consomment le fichier de configuration quelle que soit l'allowlist, donc un opérateur qui préfère éditer le fichier à la main sur le disque peut le faire sans nommer d'éditeurs UI. +Si l'allowlist est vide ou non définie, les sections de déploiement montrent toujours la configuration actuelle aux administrateurs, mais en lecture seule — l'action d'en-tête **Enregistrer le déploiement** n'apparaît que pour les opérateurs de l'allowlist. Seul un admin connecté dont le courriel figure sur la liste rend ces sections modifiables ; la page t'indique quel courriel ajouter. Il n'y a pas de bouton de redémarrage : un enregistrement affiche les deux commandes qui l'appliquent, et la section plus bas les répète. Un opérateur qui préfère travailler sur l'hôte saute l'allowlist entièrement et édite `.env` et les fichiers de config directement. ## Ce que tu peux relocaliser Trois banques de données, chacune indépendante et optionnelle. Un réglage absent signifie « utilise le défaut fourni » — une installation neuve sans configuration reste donc inchangée. -- **Base de connaissances** — le corpus de connaissances : métadonnées des documents, texte des fragments extraits, embeddings, index BM25, cache sémantique et pages web crawlées. Elle est livrée comme le conteneur `knowledge-db` (`tale_knowledge`, avec les schémas `private_knowledge` et `public_web`) et c'est la banque qui compte le plus pour les exigences de résidence, car elle détient le contenu de tes documents. Pointe-la vers ton propre Postgres géré pour garder le corpus sur une infrastructure que ton équipe exploite. -- **Stockage de fichiers** — où vivent les fichiers téléversés (les blobs d'origine). Par défaut ils résident dans le magasin d'objets fourni avec la pile (le service `object-store`, sur son propre volume) ; tu peux les pointer vers un bucket externe compatible S3. -- **Base de données applicative** (avancé) — la base Convex opérationnelle (le conteneur `db` fourni). Le backend Convex déduit le nom de cette base de `INSTANCE_NAME` (`tale_platform`) et se connecte uniquement via hôte:port, donc le Postgres externe doit contenir une base nommée exactement `tale_platform`. Son mode TLS est fixé par le pilote Convex et n'est pas configurable. + -> Note : la base de connaissances et la base de données applicative sont deux instances Postgres séparées — déplacer l'une ne touche pas l'autre. Relocaliser la base de connaissances déplace le texte extrait et les embeddings ; les fichiers téléversés d'origine ne suivent que si tu relocalises aussi le **stockage de fichiers** vers S3. +**Enregistrer les sections à l'échelle du déploiement ne repointe aucune banque.** Le backend ouvre la base applicative depuis `DATABASE_URL`, le corpus de connaissances depuis `KNOWLEDGE_DATABASE_URL`, et le blob store depuis le `object-storage/connection.json` de l'arbre de config `default`. Rien ne lit au démarrage le bloc `dataStores` que ces sections écrivent dans `deployment.yml`. Relocalise une banque à l'échelle du déploiement avec la variable d'environnement ou le fichier nommé sous elle ci-dessous, et lis les sections de déploiement comme une note de la topologie visée plutôt que comme l'interrupteur qui l'applique. Les sections **propres à une organisation**, plus bas, sont un mécanisme différent et prennent bien effet. + + + +- **Base de connaissances** — le corpus de connaissances : métadonnées des documents, texte des fragments extraits, embeddings, index BM25, cache sémantique et pages web crawlées. Elle est livrée comme la base `tale_knowledge`, avec les schémas `private_knowledge` et `public_web`, joignable à l'hôte `knowledge-db`, et c'est la banque qui compte le plus pour les exigences de résidence, car elle détient le contenu de tes documents. Pointe-la vers ton propre Postgres géré avec `KNOWLEDGE_DATABASE_URL` dans `.env` pour garder le corpus sur une infrastructure que ton équipe exploite. +- **Stockage de fichiers** — où vivent les fichiers téléversés (les blobs d'origine). Par défaut ils résident dans le magasin d'objets fourni avec la pile (le service `object-store`, sur son propre volume). Pointe-les vers un bucket externe compatible S3 en éditant `$TALE_CONFIG_DIR/default/object-storage/connection.json` et son sidecar `connection.secrets.json` ; le backend seede ce fichier contre le store fourni au premier démarrage et n'écrase jamais celui qui existe. +- **Base de données applicative** (avancé) — la banque opérationnelle : chats, tâches, runs d'automation, l'audit log, la file de jobs. Elle est livrée comme la base `tale_app` sur le conteneur `db` fourni, et le backend l'atteint par une seule chaîne de connexion, `DATABASE_URL`. Pointe-la vers ton propre Postgres géré pour la relocaliser ; le backend applique ses migrations de schéma à ce qu'il y trouve, au démarrage, sous un advisory lock. + +> Note : la base de connaissances et la base de données applicative sont deux bases séparées — déplacer l'une ne touche pas l'autre. Sur un stack `tale deploy` mono-hôte elles partagent un conteneur Postgres, donc une exigence de résidence qui les sépare est une raison de relocaliser au moins l'une des deux. Relocaliser la base de connaissances déplace le texte extrait et les embeddings ; les fichiers téléversés d'origine ne suivent que si tu relocalises aussi le **stockage de fichiers**. ## Le prérequis ParadeDB @@ -64,7 +70,7 @@ La connexion vit à côté de celle des connaissances, dans le répertoire de co - `$TALE_CONFIG_DIR//object-storage/connection.json` — région, endpoint optionnel (pour MinIO/R2), indicateur path-style, bucket et un préfixe de clé optionnel. - `$TALE_CONFIG_DIR//object-storage/connection.secrets.json` — la paire de clés d'accès, chiffrée avec SOPS dès qu'une clé age SOPS est configurée (voir [Secrets avec SOPS](/fr/self-hosted/configuration/secrets-with-sops)). -Contrairement au basculement S3 au niveau du déploiement ci-dessus, ce chemin n'est **pas** réservé aux installations neuves : dès que la configuration existe, les nouveaux téléversements vont dans le bucket de l'org, tandis que les fichiers stockés avant restent lisibles là où ils sont — les références mixtes sont prises en charge, tu peux donc basculer à tout moment. Les fichiers stockés plus tôt restent dans le stockage Convex jusqu'à ce que tu les relocalises avec le backfill de blobs ci-dessous. Si tu supprimes la configuration, les nouveaux téléversements retournent au défaut du déploiement ; les fichiers déjà écrits dans le bucket y restent, mais Tale ne peut plus les lire tant que la connexion n'est pas rétablie. Aucun redémarrage n'est nécessaire, dans un sens comme dans l'autre. +Ce chemin n'est **pas** réservé aux installations neuves : dès que la configuration existe, les nouveaux téléversements vont dans le bucket de l'org, tandis que les fichiers stockés avant restent lisibles dans le store par défaut du déploiement — tu peux donc basculer à tout moment et relocaliser les fichiers plus anciens ensuite avec le backfill de blobs ci-dessous. Si tu supprimes la configuration, les nouveaux téléversements retournent au défaut du déploiement ; les fichiers déjà écrits dans le bucket y restent, mais Tale ne peut plus les lire tant que la connexion n'est pas rétablie. Aucun redémarrage n'est nécessaire, dans un sens comme dans l'autre : le resolver met une connexion en cache quinze secondes, donc un changement est live presque immédiatement. Les admins d'org gèrent aussi cette connexion dans les mêmes sections par organisation de **Paramètres > Résidence des données** ; son test de connexion effectue un aller-retour réel écriture-lecture-suppression contre le bucket avant que tu t'engages. Comme pour la connexion des connaissances, les fichiers JSON restent la source de vérité. @@ -72,39 +78,25 @@ Les admins d'org gèrent aussi cette connexion dans les mêmes sections par orga ### Déplacer les fichiers pré-existants dans le bucket -Connecter le bucket ne réachemine que les **nouveaux** téléversements ; les blobs écrits avant la connexion restent dans le `_storage` de Convex et continuent de fonctionner via les références mixtes ci-dessus. Pour amener aussi cet historique sur ta propre infrastructure — tout l'intérêt de la résidence des données — lance le **backfill de blobs** : il copie chaque blob pré-existant dans le bucket de l'org, vérifie qu'il revient identique octet pour octet, réécrit chaque ligne qui le référence et supprime la copie Convex. - -Un admin d'org le lance depuis l'UI : une fois la connexion au bucket enregistrée, la section Stockage d'objets de **Paramètres > Résidence des données** affiche **Déplacer les fichiers existants** — confirme, et le déplacement tourne en arrière-plan pendant que les téléversements continuent ; une ligne de statut dans la même section rapporte la progression et l'issue du dernier lancement. - -Un opérateur ayant accès à la CLI Convex peut lancer le même moteur depuis un shell, en passant l'id de l'organisation. Fais d'abord un essai à blanc pour voir ce qui serait déplacé, puis le vrai lancement : - -```bash -# Essai à blanc — compte et échantillonne ce qui serait déplacé, n'écrit rien : -bunx convex run object_storage/backfill_actions:migrateOrgBlobsToObjectStorage '{"organizationId":"","dryRun":true}' - -# Le vrai lancement — retire dryRun une fois les comptes vérifiés : -bunx convex run object_storage/backfill_actions:migrateOrgBlobsToObjectStorage '{"organizationId":""}' -``` - -Le backfill est **idempotent** et **limité à l'org** : il ne déplace que les blobs de cette organisation, saute tout ce qui est déjà dans le bucket, et laisse chaque source Convex en place tant que sa copie n'est pas vérifiée — un nouveau lancement après une interruption reprend donc sans risque. Un vrai lancement exige que la connexion au bucket soit déjà configurée ; un essai à blanc, non. Ce n'est délibérément **pas** une migration de framework versionnée — il tourne à la demande, par organisation, quand tu choisis de relocaliser l'historique d'un locataire, pas à une frontière de version. +Connecter le bucket ne réachemine que les **nouveaux** téléversements ; les blobs écrits avant la connexion restent dans le store par défaut du déploiement et continuent de fonctionner, parce qu'une référence stockée nomme la clé de l'objet et que c'est le resolver qui décide de quel store il la lit. Pour amener aussi cet historique sur ta propre infrastructure — tout l'intérêt de la résidence des données — lance le **backfill de blobs** : il parcourt les documents de l'organisation (les fichiers courants et chaque version de leur historique) et ses métadonnées de fichiers, et copie chaque objet depuis le store par défaut du déploiement vers le bucket de l'org, sous la même clé. -## Stockage de fichiers sur S3 +Un admin d'org le lance depuis l'UI : une fois la connexion au bucket enregistrée, la section Stockage d'objets de **Paramètres > Résidence des données** affiche **Déplacer les fichiers existants** — confirme, et le déplacement tourne comme job de fond pendant que les téléversements continuent ; une ligne de statut dans la même section rapporte la progression et l'issue du dernier lancement. -Le stockage de fichiers externe est tout-ou-rien à travers les cas d'usage de stockage de Convex, donc tu fournis **cinq buckets** — files, exports, snapshot-imports, modules et search — plus une région et des identifiants. Pour les services compatibles S3 (MinIO, Cloudflare R2), définis l'endpoint et active l'adressage path-style. +Deux propriétés le rendent sûr à relancer. Les clés ne changent jamais, donc aucune ligne n'est réécrite et aucune référence ne peut rancir en cours de route : un objet passe d'une lecture dans le store par défaut à une lecture dans le bucket à l'instant où sa copie atterrit. Et tout objet déjà présent dans le bucket est sauté, donc un lancement interrompu reprend au lieu de recopier. Le lancement est limité à l'org, et il exige que la connexion au bucket soit déjà enregistrée. -> **Greenfield uniquement.** Faire passer le stockage de fichiers de local à S3 ne migre **pas** les blobs déjà sur le volume local — Convex les cherche dans le bucket et ne les trouve pas. Définis S3 au déploiement initial, ou copie le stockage local existant dans le bucket hors bande avant de basculer. +Ce qu'il ne fait pas, c'est supprimer. L'objet source reste dans le store par défaut du déploiement, donc un backfill relocalise une copie plutôt que de déplacer les octets — prévois une passe de nettoyage séparée si l'exigence de résidence est que l'ancienne copie cesse d'exister. Ce n'est délibérément **pas** une migration de framework versionnée : il tourne à la demande, par organisation, quand tu choisis de relocaliser l'historique d'un locataire, pas à une frontière de version. ## Comment la configuration est stockée -Enregistrer écrit deux fichiers à la racine de configuration (pas sous un répertoire d'org) : +Enregistrer les sections de déploiement écrit deux fichiers à la racine de configuration (pas sous un répertoire d'org) : -- `deployment.json` — la configuration non secrète (hôtes, ports, buckets, modes). +- `deployment.yml` — la configuration non secrète (hôtes, ports, buckets, modes). Un déploiement qui porte encore le `deployment.json` retiré est lu tel quel et converti au prochain enregistrement. - `deployment.secrets.json` — les mots de passe de base de données et les clés S3, chiffrés avec SOPS (voir [Secrets avec SOPS](/fr/self-hosted/configuration/secrets-with-sops)). -Au démarrage, l'entrypoint `convex` les lit et en dérive ses connexions avant de démarrer. L'ingestion et la récupération de connaissances tournent dans le backend Convex, c'est donc le seul conteneur qui ouvre la connexion à la base de connaissances — il n'y a pas de service de récupération séparé à configurer. Le contrat est **fail-closed** : un `deployment.json` présent mais impossible à parser, un secret indéchiffrable ou une configuration sans champs requis **interrompt le démarrage** au lieu de retomber silencieusement sur la base fournie — mal router des données réglementées est pire que ne pas démarrer. Un fichier absent est le chemin par défaut normal. +Les sections propres à une organisation écrivent dans le répertoire de cette organisation à la place, aux chemins listés plus haut. Ce sont ces fichiers dont le backend résout réellement une connexion, et la lecture est **fail-closed** : une config d'org présente mais impossible à parser, ou dont le secret refuse de se déchiffrer, refuse les lectures de cette organisation au lieu de retomber silencieusement sur la banque fournie — mal router des données réglementées est pire qu'échouer bruyamment. Un fichier absent est le chemin par défaut normal. ## Appliquer un changement : redémarrage -La configuration est lue au démarrage, donc un enregistrement ne prend effet qu'au redémarrage des conteneurs backend (`backend-api` et `backend-worker`). Lance `docker compose restart backend-api backend-worker`, ou `tale deploy` pour un roulement blue-green sans interruption — la page de réglages montre les mêmes commandes après un enregistrement. +Une connexion à l'échelle du déploiement est lue au démarrage, donc un changement dans `.env` ou dans l'arbre de config `default` ne prend effet qu'au redémarrage des conteneurs backend (`backend-api` et `backend-worker`). Lance `docker compose restart backend-api backend-worker`, ou `tale deploy` pour un roulement blue-green sans interruption — la page de réglages montre les mêmes commandes après un enregistrement. Une connexion propre à une organisation ne demande aucun redémarrage. La variable d'environnement pertinente est `TALE_DEPLOYMENT_CONFIG_ADMINS` (l'allowlist de courriels, séparés par des virgules, des opérateurs autorisés à modifier). Définis-la dans `.env`. Voir aussi [Référence des variables d'environnement](/fr/self-hosted/configuration/environment-reference) et [Secrets avec SOPS](/fr/self-hosted/configuration/secrets-with-sops). diff --git a/docs/fr/self-hosted/configuration/environment-reference.md b/docs/fr/self-hosted/configuration/environment-reference.md index a65a3b34e4..685e07345b 100644 --- a/docs/fr/self-hosted/configuration/environment-reference.md +++ b/docs/fr/self-hosted/configuration/environment-reference.md @@ -9,7 +9,7 @@ i18nLintExclude: Tale lit sa configuration depuis un unique fichier `.env` à la racine du dépôt. Environ une douzaine de variables sont obligatoires au premier boot ; les autres ajustent le comportement. Cette page liste chaque variable que [`.env.example`](https://github.com/tale-project/tale/blob/main/.env.example) ship, sa valeur par défaut et la surface produit qui la consomme. -Les groupes sont ordonnés selon le moment où tu en as besoin la première fois : identité de domaine, TLS, secrets, base de données, instance, observabilité, chiffrement des fournisseurs. Si une variable change de valeur, redémarre le conteneur plateforme (`docker compose restart tale-platform tale-convex`) pour qu'elle prenne effet. +Les groupes sont ordonnés selon le moment où tu en as besoin la première fois : identité de domaine, TLS, secrets, base de données, instance, observabilité, chiffrement des fournisseurs. Si une variable change de valeur, redémarre les conteneurs qui la lisent. La plupart sont lues par le backend, donc `docker compose restart backend-api backend-worker` est la commande habituelle ; les quelques-unes que lit la couche web demandent aussi `platform`, et `tale deploy` roule tout. ## Comment lire cette page @@ -42,22 +42,23 @@ Le `SITE_URL` doit correspondre exactement à ce que l'utilisateur tape dans le | ----------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `BETTER_AUTH_SECRET` | valeur d'exemple dans le fichier | **Obligatoire.** Secret base64 pour le signeur de session Better Auth. Génère avec `openssl rand -base64 32`. La rotation invalide chaque session. | | `ENCRYPTION_SECRET_HEX` | valeur d'exemple dans le fichier | **Obligatoire.** Clé hex de 32 octets. Clé AES-256 pour les credentials OAuth et connectors et entrée HKDF pour la secret-box des garde-fous. Génère avec `openssl rand -hex 32`. La rotation invalide chaque ciphertext en base ; les opérateurs doivent réinscrire les secrets concernés. | -| `INSTANCE_SECRET` | valeur d'exemple dans le fichier | **Obligatoire.** Sert à dériver la clé admin Convex pour `tale deploy`. Le déploiement échoue si non défini. | +| `INSTANCE_SECRET` | valeur d'exemple dans le fichier | **Obligatoire.** Chaîne hexadécimale de 64 caractères. Dérive la clé HMAC des mots de passe applicatifs WebDAV et le jeton de stage sandbox quand ceux-ci ne sont pas définis explicitement. Le déploiement échoue si non défini ou mal formé ; le faire tourner invalide chaque mot de passe applicatif WebDAV émis. | Remplace les valeurs livrées dans `.env.example` avant d'exposer l'instance — ce sont des espaces réservés volontairement non sûrs. ## Base de données -Tale fait tourner deux bases Postgres : la base opérationnelle (`db`, port 5432) derrière le backend Convex, et le corpus de connaissances (`knowledge-db`, port 5433) qui détient les fragments de documents, les embeddings et les pages crawlées. Les deux sont ParadeDB et partagent `DB_PASSWORD`, mais elles sont indépendantes — pointe l'une ou l'autre vers une infrastructure externe séparément. +Tale fait tourner deux bases Postgres : la base opérationnelle (`tale_app` sur `db`, port 5432), que le backend utilise pour l'état applicatif, les sessions et la file de jobs, et le corpus de connaissances (`tale_knowledge`, joignable à l'hôte `knowledge-db`) qui détient les fragments de documents, les embeddings et les pages crawlées. Les deux sont ParadeDB et partagent `DB_PASSWORD`, mais ce sont des bases indépendantes — pointe l'une ou l'autre vers une infrastructure externe séparément. Sur un stack `tale deploy` mono-hôte, elles vivent dans le même conteneur Postgres, qui porte l'alias réseau `knowledge-db`. | Nom | Défaut | Description | | ------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `DB_PASSWORD` | `tale_password_change_me` | **Obligatoire.** Mot de passe pour l'utilisateur Postgres auto-hébergé. Change-le avant la production. Utilisé par les deux conteneurs de base de données. | -| `POSTGRES_URL` | construit depuis `DB_PASSWORD` | **Optionnel.** Override de l'URL de la base opérationnelle construite automatiquement. Utilise-le pour pointer sur un Postgres externe ou un hôte/port non standard. | -| `KNOWLEDGE_DATABASE_URL` | `postgresql://tale:${DB_PASSWORD}@knowledge-db:5432/tale_knowledge` | **Optionnel.** URL de connexion que le backend Convex utilise pour le corpus de connaissances. Override pour relocaliser le corpus vers ton propre ParadeDB géré — la banque sensible à la résidence se déplace indépendamment. | +| `DATABASE_URL` | défini par compose depuis `DB_PASSWORD` et `APP_DB_NAME` | **Obligatoire** pour le backend, et défini pour toi par compose et `tale deploy`. L'URL de connexion complète de la base applicative, nom de base inclus. Override-la pour pointer le backend sur un Postgres externe. | +| `APP_DB_NAME` | `tale_app` | **Optionnel.** Nom de la base applicative sur le conteneur `db` livré. | +| `KNOWLEDGE_DATABASE_URL` | `postgresql://tale:${DB_PASSWORD}@knowledge-db:5432/tale_knowledge` | **Optionnel.** URL de connexion que le backend utilise pour le corpus de connaissances. Override pour relocaliser le corpus vers ton propre ParadeDB géré — la banque sensible à la résidence se déplace indépendamment. | | `KNOWLEDGE_DB_NAME` | `tale_knowledge` | **Optionnel.** Nom de la base de connaissances. Le conteneur `knowledge-db` fourni crée cette base au premier boot. | -La forme opérationnelle auto-construite est `postgresql://tale:${DB_PASSWORD}@db:5432`. Convex attend cette URL sans nom de base ; le nom est dérivé de la configuration d'instance. Le corpus de connaissances vit dans `tale_knowledge` avec les schémas `private_knowledge` et `public_web` ; l'UI **Paramètres > Résidence des données** écrit une config par banque plus riche que ces variables brutes, couverte dans [Résidence des données](/fr/self-hosted/configuration/data-residency). +Le backend applique ses propres migrations de schéma à ce que `DATABASE_URL` désigne, au boot, sous un advisory lock — donc un déploiement roulant migre exactement une fois, quel que soit le nombre de conteneurs qui démarrent ensemble. Le corpus de connaissances vit dans `tale_knowledge` avec les schémas `private_knowledge` et `public_web`. Une organisation seule peut override la connexion au corpus sans toucher à ces variables, couvert dans [Résidence des données](/fr/self-hosted/configuration/data-residency). ## Observabilité @@ -67,7 +68,7 @@ La forme opérationnelle auto-construite est `postgresql://tale:${DB_PASSWORD}@d | `SENTRY_TRACES_SAMPLE_RATE` | non défini | Taux d'échantillonnage optionnel pour les traces de performance du navigateur (`0.0`–`1.0`). Navigateur uniquement — le backend remonte des erreurs, jamais de traces. | | `METRICS_BEARER_TOKEN` | non défini | Token bearer requis pour accéder aux endpoints Prometheus `/metrics/*`. Laisse vide pour rendre les endpoints inatteignables de l'extérieur. | -Définir `METRICS_BEARER_TOKEN` expose deux endpoints derrière le token : `/metrics/platform` et `/metrics/convex` (les 261 métriques intégrées de Convex, qui portent désormais aussi les timings RAG et de crawl). Voir [Configuration d'observabilité](/fr/self-hosted/configuration/observability-config) pour la configuration de scrape. +Définir `METRICS_BEARER_TOKEN` expose trois endpoints derrière le token : `/metrics/backend`, `/metrics/platform` et `/metrics/sla-rules`. Scrape `/metrics/backend` — c'est la couche qui sert chaque requête et vide la file de jobs. Voir [Configuration d'observabilité](/fr/self-hosted/configuration/observability-config) pour la configuration de scrape. ## Chiffrement des secrets de fournisseur @@ -78,7 +79,7 @@ Définir `METRICS_BEARER_TOKEN` expose deux endpoints derrière le token : `/met Si les deux clés age ne sont pas définies, Tale stocke `providers/*.secrets.json` en JSON clair en mode 0600. Atteins ce mode seulement si le disque hôte est chiffré au repos ou si les fichiers sont produits par un outillage externe (un montage de secret Kubernetes, un template Vault). Faire tourner une clé age, c'est ajouter la nouvelle clé, réenregistrer chaque fournisseur dans l'UI, puis retirer l'ancienne. Voir [Secrets avec SOPS](/fr/self-hosted/configuration/secrets-with-sops) pour la marche complète de rotation. -La source de clé par variable d'environnement ne nécessite aucun commutateur de déploiement : des identifiants peuvent porter seulement le _nom_ d'une variable d'environnement au lieu d'une clé stockée, tant que ce nom porte le préfixe réservé `TALE_PROVIDER_KEY_`. La barrière est fail-closed — tout autre nom est rejeté, donc le champ ne peut jamais pointer sur un secret de déploiement étranger — et les noms sont plafonnés à 40 caractères. Définis la variable ici ou dans ton gestionnaire de secrets pour que la plateforme et le backend Convex puissent tous deux la lire ; le mécanisme complet est documenté dans [Fournisseurs](/fr/self-hosted/configuration/providers). Un identifiant de type courtier d'abonnement dispose d'un second espace de noms, distinct, pour le secret que Tale présente **au courtier** : ce champ accepte un nom de variable d'environnement sous le préfixe réservé `TALE_TOKEN_SOURCE_`, plafonné à 60 caractères. Les deux préfixes restent séparés à dessein — un secret de courtier n'est pas une clé API de fournisseur, et aucun des deux champs ne peut nommer une variable hors de son propre espace de noms. +La source de clé par variable d'environnement ne nécessite aucun commutateur de déploiement : des identifiants peuvent porter seulement le _nom_ d'une variable d'environnement au lieu d'une clé stockée, tant que ce nom porte le préfixe réservé `TALE_PROVIDER_KEY_`. La barrière est fail-closed — tout autre nom est rejeté, donc le champ ne peut jamais pointer sur un secret de déploiement étranger — et les noms sont plafonnés à 40 caractères. Définis la variable ici ou dans ton gestionnaire de secrets pour que les deux rôles backend puissent la lire ; le mécanisme complet est documenté dans [Fournisseurs](/fr/self-hosted/configuration/providers). Un identifiant de type courtier d'abonnement dispose d'un second espace de noms, distinct, pour le secret que Tale présente **au courtier** : ce champ accepte un nom de variable d'environnement sous le préfixe réservé `TALE_TOKEN_SOURCE_`, plafonné à 60 caractères. Les deux préfixes restent séparés à dessein — un secret de courtier n'est pas une clé API de fournisseur, et aucun des deux champs ne peut nommer une variable hors de son propre espace de noms. ## Applications OAuth des connecteurs @@ -130,7 +131,7 @@ Bascules optionnelles pour des fonctionnalités non activées par défaut. Chaqu ## Réglage du retrieval RAG -Réglages optionnels pour la recherche dans la base de connaissances. Le chemin RAG en in-process (node-actions Convex) re-note les résultats avec un cross-encoder quand le re-ranking est activé. Tous portent le préfixe `RAG_` et sont lus par les conteneurs `platform` et `convex` au boot ; après un changement, lance `docker compose restart platform convex` pour qu'il prenne effet. +Réglages optionnels pour la recherche dans la base de connaissances. La récupération tourne in-process dans le backend et re-note les résultats avec un cross-encoder quand le re-ranking est activé. Tous portent le préfixe `RAG_` et sont lus au boot ; après un changement, lance `docker compose restart backend-api backend-worker` pour qu'il prenne effet. | Nom | Défaut | Description | | ---------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -154,7 +155,7 @@ Laisse-le non défini pour conserver la durée de session par défaut. Si défin ## Ingestion de liens vidéo (yt-dlp) -Quand Tale ingère un lien vidéo, il récupère sa transcription pour l'agent. YouTube bloque l'accès automatisé depuis les IP de centres de données/serveurs, ce qui peut échouer sur un déploiement cloud. Le déploiement embarque par défaut un fournisseur de PO tokens câblé d'origine (voir [Ingestion vidéo](/fr/self-hosted/configuration/video-ingestion) pour le tableau complet) ; les options ci-dessous sont des surcharges et des escalades facultatives. Aucune ne garantit un contournement — une IP de sortie propre est le levier le plus important. Lues par le conteneur `convex` et réévaluées à chaque ingestion, donc une modification prend effet sans redémarrage. +Quand Tale ingère un lien vidéo, il récupère sa transcription pour l'agent. YouTube bloque l'accès automatisé depuis les IP de centres de données/serveurs, ce qui peut échouer sur un déploiement cloud. Le déploiement embarque par défaut un fournisseur de PO tokens câblé d'origine (voir [Ingestion vidéo](/fr/self-hosted/configuration/video-ingestion) pour le tableau complet) ; les options ci-dessous sont des surcharges et des escalades facultatives. Aucune ne garantit un contournement — une IP de sortie propre est le levier le plus important. Lues par `backend-worker`, qui fait tourner l'ingestion, et réévaluées à chaque ingestion, donc une modification prend effet sans redémarrage. | Nom | Défaut | Description | | -------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -166,7 +167,7 @@ Quand Tale ingère un lien vidéo, il récupère sa transcription pour l'agent. | `VIDEO_INGEST_PLAYER_CLIENT` | `default,tv_simply` | Liste de repli des clients de lecture YouTube, séparés par des virgules. Quand un fournisseur de PO tokens est branché, la valeur par défaut s'élargit à `default,mweb,tv_simply` (mweb exige un token GVS) ; définis-la explicitement pour forcer une liste. | | `VIDEO_INGEST_PO_TOKEN` | non défini | PO token défini manuellement (`CLIENT.CONTEXT+TOKEN`). Surtout pour les tests — les tokens sont liés à l'ID de la vidéo et éphémères ; privilégie le fournisseur. | | `VIDEO_INGEST_IMPERSONATE` | non défini | Cible d'imitation TLS/JA3 du navigateur (p. ex. `safari`). Nécessite `curl_cffi` dans l'image ; à laisser non défini sauf disponibilité connue. | -| `VIDEO_INGEST_BIN_DIR` | non défini | Répertoire ajouté en tête du `PATH` du processus enfant yt-dlp/ffmpeg, pour qu'un `yt-dlp` auto-provisionné (et son runtime Deno) installé hors des répertoires bin intégrés soit trouvé en premier. L'image `convex` intègre yt-dlp dans le `PATH`, donc laisse-le non défini là ; définis-le sur un hôte ou une machine de dev avec sa propre toolchain. | +| `VIDEO_INGEST_BIN_DIR` | non défini | Répertoire ajouté en tête du `PATH` du processus enfant yt-dlp/ffmpeg, pour qu'un `yt-dlp` auto-provisionné (et son runtime Deno) installé hors des répertoires bin intégrés soit trouvé en premier. L'image de la plateforme intègre yt-dlp dans le `PATH`, donc laisse-le non défini dans un déploiement en conteneurs ; définis-le sur un hôte ou une machine de dev avec sa propre toolchain. | | `VIDEO_INGEST_FFMPEG_LOCATION` | `/usr/bin/ffmpeg` | Chemin absolu vers le ffmpeg que yt-dlp utilise pour la post-production (conversion des sous-titres, extraction audio). À surcharger quand ffmpeg vit ailleurs — p. ex. le `/opt/homebrew/bin/ffmpeg` de Homebrew sur une machine de dev macOS. | Aucune de ces options ne garantit le succès face à la détection adaptative de YouTube. Les vidéos publiques ordinaires, les plateformes moins agressives ou un déploiement à IP résidentielle/auto-hébergé fonctionnent généralement sans elles. diff --git a/docs/fr/self-hosted/configuration/observability-config.md b/docs/fr/self-hosted/configuration/observability-config.md index 26a7e15a75..4081b8f56c 100644 --- a/docs/fr/self-hosted/configuration/observability-config.md +++ b/docs/fr/self-hosted/configuration/observability-config.md @@ -19,26 +19,25 @@ Tale ne ship pas de log shipper. L'échange de driver est le point de connector ## Métriques -Le proxy Caddy expose jusqu'à quatre chemins de métriques derrière un seul bearer token : +Le proxy Caddy expose trois chemins de métriques derrière un seul bearer token : -| Chemin | Source | Ce qui est dedans | -| -------------------- | --------------- | ------------------------------------------------------------------------------------------------------- | -| `/metrics/platform` | `tale-platform` | Latence HTTP, compteurs de routes, métriques de processus Node, gauges de cible SLA de temps de réponse | -| `/metrics/convex` | `tale-convex` | 261 métriques Convex intégrées, plus les timings RAG et de crawl | -| `/metrics/sla-rules` | `tale-platform` | Rules Prometheus de recording + alerting générées pour les SLA de temps de réponse | -| `/metrics/backend` | `tale-backend-api` | Métriques process, compteurs et latence HTTP par classe de route, profondeur de queue par état de job, générations de chat en cours, streams de hints ouverts, état de drain, et les mêmes gauges de cible SLA | +| Chemin | Source | Ce qui est dedans | +| -------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/metrics/backend` | `backend-api` | Métriques process, compteurs et latence HTTP par classe de route, profondeur de queue par état de job, générations de chat en cours, streams de hints ouverts, état de drain, et les gauges de cible SLA | +| `/metrics/platform` | `tale-platform` | Métriques de processus Node (CPU, mémoire, event-loop lag, GC) et les gauges de cible SLA de temps de réponse. La couche web sert des fichiers statiques, elle n'émet donc aucune série de requêtes HTTP | +| `/metrics/sla-rules` | `tale-platform` | Rules Prometheus de recording + alerting générées pour les SLA de temps de réponse | -Le travail de connaissances (recherche RAG, ingestion de documents, crawling web) tourne désormais dans le backend Convex, donc ses timings empruntent la série `/metrics/convex` plutôt qu'un endpoint séparé. Mets `METRICS_BEARER_TOKEN` dans `.env` pour activer ces endpoints ; laisse-le non défini pour qu'ils retournent 401 à chaque requête. Le chemin `/metrics/sla-rules` est un fichier YAML de rules en lecture seule que tu charges dans Prometheus, pas une cible de scrape — les seuils qu'il porte sont documentés dans [Opérations](/fr/self-hosted/operate/observability/operations). Tout sauf les chemins listés retourne aussi 401, donc un scraper mal routé ne voit pas accidentellement les endpoints de santé internes de la plateforme. +`/metrics/backend` est celui qui compte : c'est la couche qui sert chaque requête, exécute la recherche de connaissances et vide la file de jobs. Mets `METRICS_BEARER_TOKEN` dans `.env` pour activer ces endpoints ; laisse-le non défini pour qu'ils retournent 401 à chaque requête. Le chemin `/metrics/sla-rules` est un fichier YAML de rules en lecture seule que tu charges dans Prometheus, pas une cible de scrape — les seuils qu'il porte sont documentés dans [Opérations](/fr/self-hosted/operate/observability/operations). Tout sauf les chemins listés retourne 404 dans la barrière et 401 en dehors, donc un scraper mal routé ne voit jamais les chiffres d'un autre service sous le mauvais nom. -`/metrics/backend` n'existe qu'une fois le déploiement basculé sur le backend Postgres (`BACKEND_UPSTREAM` défini dans `.env`). Avant la bascule, le chemin répond 404 au lieu de servir en silence les chiffres d'un autre service sous le nom du backend : une cible de scrape ajoutée trop tôt échoue visiblement au lieu de tracer le mauvais processus. +Il n'y a rien à scraper sur `backend-worker` : le rôle worker ne sert aucun HTTP. Son comportement est visible sur `/metrics/backend` à la place, parce que le gauge de queue lit la table de jobs partagée — `tale_backend_jobs{state="created"}` qui grimpe sans jamais redescendre, c'est exactement l'image d'un worker à l'arrêt. Une stanza de scrape Prometheus qui marche : ```yaml scrape_configs: - - job_name: tale-platform + - job_name: tale-backend scheme: https - metrics_path: /metrics/platform + metrics_path: /metrics/backend authorization: credentials: static_configs: @@ -61,7 +60,9 @@ Le sample rate plafonne les traces de performance du navigateur et ne s'applique ## Ce qui ne ship pas encore -Les traces OpenTelemetry ne sont pas intégrées aux conteneurs. Les données sont joignables indirectement — les durées d'action Convex et les timings de routes HTTP arrivent par les métriques Prometheus — mais il n'y a pas d'exportateur OTLP sur la boîte aujourd'hui. Si tu as besoin d'export de traces complet, fais tourner un OpenTelemetry Collector à côté de Tale et scrape les endpoints Prometheus depuis lui. +Les traces OpenTelemetry ne sont pas intégrées aux conteneurs. Les données sont joignables indirectement — les durées de requête par classe de route arrivent par les métriques Prometheus — mais il n'y a pas d'exportateur OTLP sur la boîte aujourd'hui. Si tu as besoin d'export de traces complet, fais tourner un OpenTelemetry Collector à côté de Tale et scrape les endpoints Prometheus depuis lui. + +Il n'y a pas non plus de log de requêtes. Le backend enregistre chaque requête comme une métrique, pas comme une ligne, donc il n'y a aucun audit par requête dans `docker compose logs backend-api` — l'access log du proxy est ce qui s'en approche le plus, et l'audit log dans le produit couvre les actions de control plane. ## Où cela s'inscrit diff --git a/docs/fr/self-hosted/configuration/providers.md b/docs/fr/self-hosted/configuration/providers.md index 0504de6f7c..5bdb972b18 100644 --- a/docs/fr/self-hosted/configuration/providers.md +++ b/docs/fr/self-hosted/configuration/providers.md @@ -66,11 +66,11 @@ TALE_PROVIDER_KEY_OPENAI_PROD=sk-... -La barrière est fail-closed : tout nom hors du préfixe réservé est rejeté, ce qui empêche un identifiant de désigner un secret de déploiement étranger comme `SOPS_AGE_KEY` ou `BETTER_AUTH_SECRET` et de le voir partir en jeton Bearer vers l’endpoint d’un fournisseur. Les noms sont plafonnés à 40 caractères, la limite de la synchronisation d’environnement entre la plateforme et Convex — un nom plus long n’atteindrait jamais le runtime du backend. +La barrière est fail-closed : tout nom hors du préfixe réservé est rejeté, ce qui empêche un identifiant de désigner un secret de déploiement étranger comme `SOPS_AGE_KEY` ou `BETTER_AUTH_SECRET` et de le voir partir en jeton Bearer vers l’endpoint d’un fournisseur. Les noms sont plafonnés à 40 caractères. -Définis la variable de façon que le conteneur de la plateforme et le backend Convex puissent tous deux la lire. La plateforme synchronise son environnement vers Convex au boot, donc les actions qui y tournent résolvent la même valeur ; une variable ajoutée ou changée après le boot demande un redémarrage du conteneur plateforme avant d’être visible. Les valeurs sont nettoyées de leurs espaces, ce qui t’épargne le retour à la ligne que porte souvent un fichier de secret monté, et le `401` qui s’ensuit. +Définis la variable là où les conteneurs backend la lisent — dans `.env`, ou via ce que ton coffre à secrets injecte dans `backend-api` et `backend-worker`. Les deux rôles résolvent des identifiants, donc les deux ont besoin de la valeur : le worker fait pour le travail de fond les mêmes appels fournisseur que l’api fait pour un tour en direct. Une variable ajoutée ou changée après le boot demande `docker compose restart backend-api backend-worker` avant d’être visible. Les valeurs sont nettoyées de leurs espaces, ce qui t’épargne le retour à la ligne que porte souvent un fichier de secret monté, et le `401` qui s’ensuit. ## Secrets de courtier depuis l’environnement diff --git a/docs/fr/self-hosted/configuration/retention.md b/docs/fr/self-hosted/configuration/retention.md index db6a7dbed6..ddfeeb3ef8 100644 --- a/docs/fr/self-hosted/configuration/retention.md +++ b/docs/fr/self-hosted/configuration/retention.md @@ -41,7 +41,7 @@ Les fenêtres de rétention choisies par l'admin vivent dans un fichier distinct ## Le sweep de rétention -Un cron planifié dans `tale-convex` fait la suppression réelle. Chaque catégorie est sweepée indépendamment — un run lent sur une ne bloque pas les autres. Les suppressions sont auditées (chaque catégorie a son propre événement `*.retention_deleted`), et restaurer une entité dans sa fenêtre de grâce est possible depuis **Corbeille** avant le sweep final. +Un job planifié quotidien sur `backend-worker` fait la suppression réelle, à 04h00 UTC. Chaque catégorie est sweepée indépendamment — un run lent sur une ne bloque pas les autres, et un sweep qui échoue est réessayé par la file plutôt que sauté jusqu'au lendemain. Les suppressions sont auditées (chaque catégorie a son propre événement `*.retention_deleted`), et restaurer une entité dans sa fenêtre de grâce est possible depuis **Corbeille** avant le sweep final. Les entrées d'audit log sont elles-mêmes soumises à la rétention, mais leur plancher est imposé par déploiement, pas par org : la rétention d'audit log la plus stricte (la plus courte) à travers toutes les orgs est ce qui tourne effectivement. Un tenant plus strict tire tout le monde plus serré — garde ça en tête sur les instances multi-tenants. diff --git a/docs/fr/self-hosted/configuration/secrets-with-sops.md b/docs/fr/self-hosted/configuration/secrets-with-sops.md index cb28b355ef..987bcda8bd 100644 --- a/docs/fr/self-hosted/configuration/secrets-with-sops.md +++ b/docs/fr/self-hosted/configuration/secrets-with-sops.md @@ -15,7 +15,7 @@ Les variables d'env qui pilotent les modes sont `SOPS_AGE_KEY` et `SOPS_AGE_KEY_ | Fichier de clés | `SOPS_AGE_KEY_FILE=/path/to/keys` | Requis pour la rotation. Une clé age par ligne, commentaires `#`. | | Clair à 0600 | Les deux non définis | Disque chiffré au repos, ou outillage externe écrit les fichiers. | -Le conteneur plateforme choisit le mode au boot. La forme inline est la plus simple ; la forme fichier est la seule qui supporte plusieurs lecteurs (ce qui rend la rotation possible sans downtime) ; la forme en clair saute SOPS entièrement et fait confiance au système de fichiers. +Les conteneurs backend choisissent le mode au boot — ils possèdent chaque écriture dans le magasin de config et sont les seuls processus à le déchiffrer ; la couche web monte le même volume en lecture seule et ne touche jamais la clé age. La forme inline est la plus simple ; la forme fichier est la seule qui supporte plusieurs lecteurs (ce qui rend la rotation possible sans downtime) ; la forme en clair saute SOPS entièrement et fait confiance au système de fichiers. ## Mode chiffré au premier boot @@ -30,7 +30,7 @@ cat providers/openai.secrets.json # } ``` -Le déchiffrement se passe in-process quand le conteneur plateforme lit le fichier. La clé age ne quitte jamais la mémoire du conteneur plateforme. +Le déchiffrement se passe in-process quand un conteneur backend lit le fichier. La clé age ne quitte jamais la mémoire de ce conteneur. ## Faire tourner la clé age @@ -43,10 +43,10 @@ age-keygen -o /etc/tale/age-keys.txt # 2. Ajoute la nouvelle clé comme deuxième ligne dans le fichier echo "AGE-SECRET-KEY-1NEW..." >> /etc/tale/age-keys.txt -# 3. Pointe .env sur le fichier et redémarre le conteneur plateforme +# 3. Pointe .env sur le fichier et redémarre les conteneurs backend sed -i 's|^SOPS_AGE_KEY=.*|# SOPS_AGE_KEY=|' .env sed -i 's|^# SOPS_AGE_KEY_FILE=.*|SOPS_AGE_KEY_FILE=/etc/tale/age-keys.txt|' .env -docker compose restart tale-platform tale-convex +docker compose restart backend-api backend-worker ``` Maintenant l'ancienne et la nouvelle clé peuvent déchiffrer les fichiers existants. Re-sauvegarde la clé API de chaque fournisseur sous **Paramètres > Fournisseurs** — chaque sauvegarde produit du ciphertext lisible par les deux clés. Une fois que chaque fournisseur a été re-sauvegardé (la colonne **Dernière rotation** dans le tableau des fournisseurs te dit lesquels tiennent encore l'ancien ciphertext), retire l'ancienne clé du fichier : @@ -54,10 +54,10 @@ Maintenant l'ancienne et la nouvelle clé peuvent déchiffrer les fichiers exist ```bash # 4. Drop la ligne de l'ancienne clé et redémarre à nouveau sed -i '/^AGE-SECRET-KEY-1OLD/d' /etc/tale/age-keys.txt -docker compose restart tale-platform tale-convex +docker compose restart backend-api backend-worker ``` -L'ordre est porteur : ne retire jamais l'ancienne clé avant que chaque fichier soit re-chiffré, sinon le conteneur plateforme échouera à lire les fichiers encore-anciens au prochain déchiffrement. +L'ordre est porteur : ne retire jamais l'ancienne clé avant que chaque fichier soit re-chiffré, sinon le backend échouera à lire les fichiers encore-anciens au prochain déchiffrement. ## Basculer en clair diff --git a/docs/fr/self-hosted/contributing-docker.md b/docs/fr/self-hosted/contributing-docker.md index d2df6af53b..c101157428 100644 --- a/docs/fr/self-hosted/contributing-docker.md +++ b/docs/fr/self-hosted/contributing-docker.md @@ -14,15 +14,14 @@ La stack est entièrement TypeScript — pas d'image Python. Chaque image a un D | Image | Chemin source | Base | | ------------------------ | ----------------------------- | ---------------------------- | | `tale-proxy` | `services/proxy/` | Caddy | -| `tale-platform` | `services/platform/` | Bun + Debian slim | -| `tale-convex` | `services/convex/` | Convex local-backend | +| `tale-platform` | `services/platform/` | Debian slim + Bun + Node | | `tale-db` | `services/db/` | ParadeDB (Postgres) | | `tale-sandbox` | `services/sandbox/` | Bun + CLI Docker | | `tale-sandbox-egress` | `services/sandbox-egress/` | Alpine + tinyproxy | | `tale-sandbox-runtime` | `services/sandbox-runtime/` | Bun + Chromium + Playwright | | `tale-sandbox-buildkitd` | `services/sandbox-buildkitd/` | Debian + BuildKit + redsocks | -Les deux conteneurs de base de données — `db` et `knowledge-db` — se construisent depuis la même image ParadeDB `tale-db` ; la différence est la base que chacun sert. La gateway LLM, `tale-sandbox-llm-gateway`, est une image amont pinnée (`maximhq/bifrost`), elle n'a donc pas de Dockerfile dans le repo. Les fichiers compose à la racine du repo (`compose.yml` pour développement, le compose de production généré par la CLI) les référencent via `ghcr.io/tale-project/tale/:`. Un build local remplace le pull de registre par un bloc `build:` dans compose. +Les deux conteneurs de base de données — `db` et `knowledge-db` — se construisent depuis la même image ParadeDB `tale-db` ; la différence est la base que chacun sert. La même image sert aussi les deux rôles backend : `backend-api` et `backend-worker` sont `tale-platform` démarrée avec un `TALE_ROLE` différent, c'est pourquoi ils ne peuvent jamais dériver en version par rapport à la couche web. Deux conteneurs n'ont pas de Dockerfile propre : le blob store est une image MinIO amont référencée directement depuis compose, et `tale-sandbox-llm-gateway` est un simple re-tag de la gateway amont pinnée `maximhq/bifrost`, qui ne change rien à l'exécution. Les fichiers compose à la racine du repo (`compose.yml` pour développement, le compose de production généré par la CLI) les référencent via `ghcr.io/tale-project/tale/:`. Un build local remplace le pull de registre par un bloc `build:` dans compose. ## Construire localement @@ -47,7 +46,7 @@ Les points d'extension supportés pour les forks sont au niveau du Dockerfile. L - **Image runtime sandbox** — `services/sandbox-runtime/Dockerfile` est l'environnement d'exécution pour **Exécuter du code**, le rendu web et la génération de documents ; il embarque déjà Chromium et Playwright. Un fork qui a besoin d'un paquet système supplémentaire ou d'un build de navigateur différent patche ici. - **Proxy d'egress sandbox** — `services/sandbox-egress/tinyproxy.conf.template` est la configuration proxy que l'entrypoint rend au démarrage : egress ouvert par défaut, ou un filtre d'hôtes en refus par défaut quand `SANDBOX_EGRESS_ALLOWLIST` est défini. Un fork qui a besoin d'un autre comportement proxy patche ici. -Ce qui n'est pas une couture supportée : le code applicatif du backend convex, y compris l'extraction de documents et la logique RAG et crawler qui vit désormais en in-process (`services/platform/convex/`), et le code runtime du conteneur plateforme (`services/platform/app/`). Ces fichiers sont du code applicatif, pas de la configuration — ajouter un extracteur de format de document ou changer le comportement de récupération est un vrai fork et porte la taxe de montée de version. +Ce qui n'est pas une couture supportée : le code applicatif du backend (`services/platform/backend/`), y compris l'extraction de documents et la logique RAG et crawler qui y tourne in-process, et le code runtime de la couche web (`services/platform/app/`). Ces fichiers sont du code applicatif, pas de la configuration — ajouter un extracteur de format de document ou changer le comportement de récupération est un vrai fork et porte la taxe de montée de version. ## Tagger et pousser vers ta propre registre diff --git a/docs/fr/self-hosted/install/docker-compose-reference.md b/docs/fr/self-hosted/install/docker-compose-reference.md index f5d3f58279..4863cd03dd 100644 --- a/docs/fr/self-hosted/install/docker-compose-reference.md +++ b/docs/fr/self-hosted/install/docker-compose-reference.md @@ -38,15 +38,19 @@ Le fichier le plus à gauche est la base ; chaque fichier suivant fusionne ses c ## Services et leurs rôles -Le graphe de base démarre huit conteneurs : - -- `tale-proxy` — Caddy. TLS, reverse-proxy, redirections 301. -- `tale-platform` — l'app TanStack Start. L'UI et l'API côté utilisateur. -- `tale-convex` — le backend Convex. WebSocket, queries, mutations, actions — et la recherche RAG, l'ingestion de documents, le crawling web et la génération de documents en in-process, qui étaient autrefois des services séparés. -- `tale-db` — Postgres opérationnel (ParadeDB). Le stockage persistant du backend Convex. +Le graphe de base démarre dix conteneurs : + +- `tale-proxy` — Caddy. TLS, reverse-proxy, redirections 301. Il publie aussi le chemin du bucket du blob store pour que les URL présignées marchent dans le navigateur. +- `tale-platform` — l'app TanStack Start. L'UI côté utilisateur, les assets statiques et la page `/status` publique. +- `backend-api` — le backend applicatif : un processus Node qui sert chaque porte sous `/api/`, plus `/events`, `/dav` et l'API machine. La recherche de connaissances tourne dans ce processus. +- `backend-worker` — la même image dans le rôle worker, qui vide la file de jobs pg-boss : ingestion et embedding de documents, crawl web, runs d'automation, sweeps de rétention. Il ne sert aucun HTTP. Les deux services backend prennent `--scale`, et c'est pourquoi aucun n'a de nom de conteneur fixe. +- `tale-db` — Postgres opérationnel (ParadeDB). La base `tale_app` : état applicatif, sessions et file de jobs. +- `tale-object-store` — le blob store (MinIO). Chaque document téléversé, chaque pièce jointe de chat, chaque fichier audio et chaque média généré. Interne seulement. - `tale-knowledge-db` — Postgres du corpus de connaissances (ParadeDB). La base `tale_knowledge` qui détient les fragments de documents, les embeddings et les pages crawlées, sur le port 5433 pour ne jamais entrer en conflit avec `tale-db` sur 5432. - `tale-sandbox-llm-gateway` — la gateway LLM pour les tours sur harness (image externe pinnée). -- `tale-sandbox-egress` et `tale-sandbox` — le plan sandbox. Conteneurs Run-code derrière un proxy de sortie (ouvert par défaut ; verrouillable avec `SANDBOX_EGRESS_ALLOWLIST`), aussi le runtime de navigateur headless que le backend convex appelle pour le rendu web et la génération de documents. +- `tale-sandbox-egress` et `tale-sandbox` — le plan sandbox. Conteneurs Run-code derrière un proxy de sortie (ouvert par défaut ; verrouillable avec `SANDBOX_EGRESS_ALLOWLIST`), aussi le runtime de navigateur headless que le backend appelle pour le rendu web et la génération de documents. + +Un sidecar `bgutil-provider` les rejoint pour l'ingestion YouTube ; il est best-effort, et la stack marche sans lui. Un stack `tale deploy` mono-hôte laisse tomber `tale-knowledge-db` et replie le corpus dans `tale-db` sous l'alias réseau `knowledge-db`. La stack est désormais entièrement TypeScript — il n'y a pas de service Python dans le graphe. [Architecture des conteneurs](/fr/self-hosted/operate/container-architecture) creuse qui possède quoi. diff --git a/docs/fr/self-hosted/install/quickstart.md b/docs/fr/self-hosted/install/quickstart.md index 9dac768d42..b6d65c2348 100644 --- a/docs/fr/self-hosted/install/quickstart.md +++ b/docs/fr/self-hosted/install/quickstart.md @@ -83,7 +83,7 @@ Sur une instance vide, il n’y a pas de page d’inscription à chercher : la p -[Premier admin](/fr/self-hosted/install/first-admin) couvre l’assistant en détail, comment les coéquipiers arrivent, et la clé admin du tableau de bord Convex — un outil d’inspection du backend qui ne joue aucun rôle dans la connexion. +[Premier admin](/fr/self-hosted/install/first-admin) couvre l’assistant en détail et comment les coéquipiers arrivent. diff --git a/docs/fr/self-hosted/operate/backups-and-restore.md b/docs/fr/self-hosted/operate/backups-and-restore.md index cb2aac76f3..e8a3b16465 100644 --- a/docs/fr/self-hosted/operate/backups-and-restore.md +++ b/docs/fr/self-hosted/operate/backups-and-restore.md @@ -3,21 +3,31 @@ title: Backups et restauration description: Snapshots de volumes via `tale backup`, le snapshot automatique pré-migration, la rétention, la copie hors-hôte et le drill `tale restore`. --- -L'unité de backup de Tale est le snapshot de volume : un tar checksummé, pris à containers en pause, de chaque volume de données de l'instance, écrit dans un volume `backups` dédié qui vit à côté des données qu'il protège. La CLI en prend un automatiquement avant toute étape de déploiement qui peut migrer des données, et `tale backup` en prend un à la demande. La récupération, c'est `tale restore ` plus un redéploiement de la version correspondante — cette paire est la réponse à une montée de version échouée, et la raison pour laquelle `tale rollback` peut se permettre de refuser tout ce qui dépasse un pas de patch. +L'unité de backup de Tale est le snapshot de volume : un tar checksummé, pris à conteneurs en pause, de la base de données, de l'arbre de config d'org et de l'état du proxy, écrit dans un volume `backups` dédié qui vit à côté des données qu'il protège. La CLI en prend un automatiquement avant toute étape de déploiement qui peut migrer des données, et `tale backup` en prend un à la demande. La récupération, c'est `tale restore ` plus un redéploiement de la version correspondante — cette paire est la réponse à une montée de version échouée, et la raison pour laquelle `tale rollback` peut se permettre de refuser tout ce qui dépasse un pas de patch. -Le contexte d'architecture vit dans [Architecture des conteneurs](/fr/self-hosted/operate/container-architecture) ; cette page couvre ce qu'un snapshot contient, quand il est pris, comment la copie quitte l'hôte et le walk de restauration. +Un snapshot n'est pas l'instance entière. Les blobs des fichiers téléversés vivent en dehors, donc c'est le job hors-hôte plus bas qui rend une reconstruction complète possible — lis cette section même si tu ne prends jamais de snapshot à la main. + +Le contexte d'architecture vit dans [Architecture des conteneurs](/fr/self-hosted/operate/container-architecture) ; cette page couvre ce qu'un snapshot contient, ce qu'il te laisse, quand il est pris, comment la copie quitte l'hôte et le walk de restauration. ## Ce qu'un snapshot contient -| Volume | Contient | -| ---------------------------- | --------------------------------------------------------- | -| `db-data` | Postgres — agents, runs, l'audit log | -| `convex-data` | Config d'org, secrets de fournisseurs, branding téléversé | -| `rag-data` | L'index vectoriel construit depuis tes documents | -| `crawler-data` | Connaissance web crawlée | -| `caddy-data`, `caddy-config` | Certificats TLS et état du proxy | +| Volume | Contient | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `db-data` | Postgres — la base applicative (chats, tâches, runs d'automation, l'audit log) et, sur un stack `tale deploy` mono-hôte où les deux bases partagent un Postgres, le corpus de connaissances | +| `convex-data` | L'arbre de config d'org — agents, automations, connecteurs, fournisseurs, skills, policies de gouvernance, connexions SSO, branding | +| `caddy-data`, `caddy-config` | Certificats TLS et état du proxy | + +`convex-data` est le nom historique du volume de config. Il est conservé délibérément pour que la mise hors service du backend Convex n'oblige aucun opérateur à migrer un volume juste pour un renommage ; plus rien de Convex n'y tourne. + +Chaque snapshot est un répertoire nommé comme `20260611-142530-deploy` dans le volume `backups` du projet : un `.tar.gz` par volume, un sidecar `.sha256` chacun et un `manifest.json` écrit en dernier. Un répertoire sans manifest est un snapshot incomplet — il n'apparaît jamais dans les listings et ne peut jamais être restauré. + + + +**Les fichiers téléversés ne sont pas dans le snapshot.** Les blobs de documents, les pièces jointes de chat, l'audio et les médias générés vivent dans le blob store, sur le volume `object-store-data`, et `tale backup` ne le capture pas. Une restauration ramène donc des lignes qui pointent vers des blobs que le store n'a plus — l'app affiche la liste des documents et échoue à l'ouverture. Capture `object-store-data` dans le même job qui copie le volume `backups` hors de l'hôte, ou pointe le déploiement vers un object store qui porte ses propres backups. + + -Chaque snapshot est un répertoire nommé comme `20260611-142530-deploy` dans le volume `backups` du projet : un `.tar.gz` par volume, un sidecar `.sha256` chacun et un `manifest.json` écrit en dernier. Un répertoire sans manifest est un snapshot incomplet — il n'apparaît jamais dans les listings et ne peut jamais être restauré. Deux choses vivent hors des volumes et demandent une capture séparée : le workspace du projet (le répertoire qui contient `tale.json`) et `.env`. +Trois autres choses vivent hors des volumes snapshottés et demandent une capture séparée : le blob store ci-dessus, le workspace du projet (le répertoire qui contient `tale.json`) et `.env`. ## Quand les snapshots sont pris @@ -36,19 +46,20 @@ La rotation garde les cinq snapshots les plus récents et tout ce qui date des 1 ## Copie hors-hôte -Les snapshots vivent sur le même hôte que les données qu'ils protègent — un disque mort emporte les deux. Pointe ton outillage de backup existant (Restic, Borg, Velero, snapshots de cloud provider) sur le volume `backups`, et capture le workspace du projet et `.env` dans le même job. Tale n'embarque pas d'étape d'upload — garder la copie hors-hôte sous ton contrat de backup existant est délibéré. +Les snapshots vivent sur le même hôte que les données qu'ils protègent — un disque mort emporte les deux. Pointe ton outillage de backup existant (Restic, Borg, Velero, snapshots de cloud provider) sur le volume `backups` **et** sur `object-store-data`, et capture le workspace du projet et `.env` dans le même job. Tale n'embarque pas d'étape d'upload — garder la copie hors-hôte sous ton contrat de backup existant est délibéré. ```bash -# crontab sur l'hôte — copie Restic horaire du volume backups vers S3 +# crontab sur l'hôte — copie Restic horaire des snapshots et du blob store 0 * * * * restic -r s3:s3.amazonaws.com/bucket/tale backup \ - /var/lib/docker/volumes/_backups/_data + /var/lib/docker/volumes/_backups/_data \ + /var/lib/docker/volumes/_object-store-data/_data ``` -Trouve le chemin hôte du volume avec `docker volume inspect _backups` ; l'id du projet vit dans `tale.json`. +Trouve le chemin hôte d'un volume avec `docker volume inspect _backups` ; l'id du projet vit dans `tale.json`. ## Restaurer un snapshot -`tale restore` sans argument liste ce qui est disponible ; avec un id, il vérifie les checksums, vide les volumes de données et extrait le snapshot. Il refuse tant qu'un conteneur du projet tourne — passe `--stop` pour les arrêter — et demande confirmation avant de toucher à quoi que ce soit. +`tale restore` sans argument liste ce qui est disponible ; avec un id, il vérifie les checksums, vide les volumes que le snapshot couvre et l'extrait. Il refuse tant qu'un conteneur du projet tourne — passe `--stop` pour les arrêter — et demande confirmation avant de toucher à quoi que ce soit. Il ne restaure que les volumes du tableau ci-dessus ; le blob store, c'est à toi de le remettre depuis la copie hors-hôte, avant de remonter le stack. ```bash # Voir ce qui est disponible @@ -66,7 +77,7 @@ Le redéploiement de la version correspondante fait partie de la restauration, c ## Drill de restauration -Fais tourner le drill trimestriellement sur un hôte non-production. Le drill n'est pas « un snapshot existe-t-il » — c'est « un hôte frais peut-il être reconstruit depuis la copie hors-hôte du volume `backups`, le workspace du projet et `.env` en moins d'une heure ». Les modes d'échec que le drill attrape : un job hors-hôte qui n'a jamais capturé le workspace, et un `.env` périmé qui ne correspond plus aux exigences du binaire courant. +Fais tourner le drill trimestriellement sur un hôte non-production. Le drill n'est pas « un snapshot existe-t-il » — c'est « un hôte frais peut-il être reconstruit depuis la copie hors-hôte du volume `backups`, du blob store, du workspace du projet et de `.env` en moins d'une heure ». Termine en ouvrant un document téléversé avant le snapshot : c'est l'unique étape qui prouve que le blob store est revenu avec la base, et c'est celle qu'un drill limité au snapshot saute. Les autres modes d'échec que le drill attrape : un job hors-hôte qui n'a jamais capturé le workspace, et un `.env` périmé qui ne correspond plus aux exigences du binaire courant. ## Où cela s'inscrit diff --git a/docs/fr/self-hosted/operate/container-architecture.md b/docs/fr/self-hosted/operate/container-architecture.md index acec3d19b7..82967b4b5d 100644 --- a/docs/fr/self-hosted/operate/container-architecture.md +++ b/docs/fr/self-hosted/operate/container-architecture.md @@ -3,59 +3,71 @@ title: Architecture des conteneurs description: Quel conteneur possède quel travail dans une instance Tale en marche, le chemin de requête d'un message chat, et à quoi ressemble une panne de chaque conteneur. --- -Une instance Tale, ce sont huit conteneurs câblés par docker compose. La page d'architecture a couvert à quoi sert chaque conteneur ; cette page-ci est la version de l'opérateur — quel conteneur possède quel travail, comment un message chat y circule et à quoi ressemble le mode de défaillance quand l'un d'eux meurt. +Une instance Tale, ce sont dix conteneurs câblés par docker compose. La page d'architecture a couvert à quoi sert chaque conteneur ; cette page-ci est la version de l'opérateur — quel conteneur possède quel travail, comment un message chat y circule et à quoi ressemble le mode de défaillance quand l'un d'eux meurt. Lis ceci quand tu es d'astreinte. Reviens-y quand tu décides quel conteneur rouler en premier pendant une montée de version. -## Les huit conteneurs, avec leurs tâches +## Les dix conteneurs, avec leurs tâches -| Conteneur | Tâche | Une panne affecte | -| -------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| `tale-proxy` | Terminaison TLS + routage en bordure | Tous les ingress — aucun client ne joint l'UI | -| `tale-platform` | Serveur UI, livraison des assets statiques | Le navigateur voit 502 ; l'API reste joignable | -| `tale-convex` | Actions/queries/mutations backend + WebSocket, plus RAG, crawling et génération de documents en in-process | L'UI charge mais sans données ; les chats en vol stagnent ; l'ingestion stagne | -| `tale-db` | Postgres opérationnel pour Convex | Convex bascule en lecture seule ; les écritures bloquent | -| `tale-knowledge-db` | Postgres du corpus de connaissances (fragments de documents, embeddings, pages crawlées) | La recherche de connaissances renvoie vide ; l'ingestion échoue | -| `tale-sandbox-llm-gateway` | Gateway LLM pour les tours sur harness | Les tours sur harness ne joignent aucun modèle ; le chat n'est pas affecté | -| `tale-sandbox-egress` | Sortie réseau pour code sandbox | L'outil **Exécuter du code** échoue avec « egress denied » ; le rendu web échoue | -| `tale-sandbox` | Runtime sandbox + navigateur headless pour le rendu web et la génération de documents | **Exécuter du code**, le rendu de crawl web et la génération de documents échouent | +| Conteneur | Tâche | Une panne affecte | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| `tale-proxy` | Terminaison TLS + routage en bordure | Tous les ingress — aucun client ne joint l'UI | +| `tale-platform` | Serveur UI, livraison des assets statiques, la page `/status` publique | Le navigateur voit 502 ; l'API reste joignable | +| `backend-api` | Chaque requête applicative : auth, API d'app, API machine, WebDAV, le flux de mises à jour — et la recherche de connaissances in-process | L'UI charge mais sans données ; les chats en vol stagnent | +| `backend-worker` | Les jobs de fond : ingestion et embedding de documents, crawl web, runs d'automation, sweeps de rétention, le plan cron | L'UI marche toujours ; les téléversements restent en « indexation », les automations dorment | +| `tale-db` | Postgres — la base applicative, la file de jobs et le corpus de connaissances | Les écritures sont refusées ; l'app dégrade vers ce qui est déjà chargé | +| `tale-knowledge-db` | Postgres du corpus de connaissances (fragments de documents, embeddings, pages crawlées) | La recherche de connaissances renvoie vide ; l'ingestion échoue | +| `tale-object-store` | Le blob store — documents téléversés, pièces jointes de chat, audio, médias générés | Chaque upload et chaque download échoue ; le reste de l'app marche | +| `tale-sandbox-llm-gateway` | Gateway LLM pour les tours sur harness | Les tours sur harness ne joignent aucun modèle ; le chat n'est pas affecté | +| `tale-sandbox-egress` | Sortie réseau pour code sandbox | L'outil **Exécuter du code** échoue avec « egress denied » ; le rendu web échoue | +| `tale-sandbox` | Runtime sandbox + navigateur headless pour le rendu web et la génération de documents | **Exécuter du code**, le rendu de crawl web et la génération de documents échouent | -Un conteneur est exposé au réseau public (`tale-proxy` pour HTTPS, et optionnellement `tale-sandbox-egress` sortant pour la sandbox) ; le reste est interne seulement. +`backend-api` et `backend-worker` sont la même image que `tale-platform`, démarrée dans un rôle différent, et les deux scalent indépendamment — `docker compose up -d --scale backend-worker=3` est une topologie supportée, et c'est précisément pour ça que le compose livré ne leur donne aucun nom de conteneur fixe. Adresse-les par leur nom de service. Un stack `tale deploy` les nomme `-backend-api` et `-backend-worker`. + +`tale-knowledge-db` est un conteneur à part dans le compose livré. Un stack `tale deploy` mono-hôte replie le corpus dans `tale-db` et donne à ce conteneur l'alias réseau `knowledge-db`, si bien que la même chaîne de connexion résout dans les deux cas — si `tale status` n'affiche aucune base de connaissances, c'est pour ça, et `tale-db` est le conteneur à regarder. + +Un conteneur est exposé au réseau public (`tale-proxy` pour HTTPS, et optionnellement `tale-sandbox-egress` sortant pour la sandbox) ; le reste est interne seulement, blob store inclus — les blobs atteignent le navigateur via des URL présignées que le proxy relaie sous le chemin du bucket. ## Le chemin de requête Un message chat fait un aller-retour par les conteneurs : 1. Navigateur → `tale-proxy` (TLS terminé). -2. `tale-proxy` → `tale-platform` pour HTML/JS, → `tale-convex` pour API + WebSocket. -3. `tale-convex` lit la config fournisseur de l'organisation, choisit le modèle, ouvre un flux vers le fournisseur amont. -4. Si l'agent récupère des connaissances : `tale-convex` exécute la recherche RAG en in-process, interrogeant directement `tale-knowledge-db` — sans service de récupération séparé sur le chemin. -5. Si l'agent exécute du code : `tale-convex` → `tale-sandbox` → `tale-sandbox-egress` pour tout appel sortant. -6. Le flux du fournisseur renvoie des tokens via `tale-convex` jusqu'au navigateur via le WebSocket. +2. `tale-proxy` → `tale-platform` pour le HTML, le JS et les assets statiques → `backend-api` pour tout ce qui est sous `/api/`, plus `/events`, `/dav` et l'API machine. +3. `backend-api` lit la config fournisseur de l'organisation, choisit le modèle, ouvre un flux vers le fournisseur amont et renvoie les tokens au navigateur en server-sent events. +4. Si l'agent récupère des connaissances : `backend-api` exécute la recherche in-process, en interrogeant directement la base du corpus — sans service de récupération séparé sur le chemin. +5. Si l'agent exécute du code : `backend-api` → `tale-sandbox` → `tale-sandbox-egress` pour tout appel sortant. +6. Tout ce que le tour a différé — indexer un nouveau téléversement, une automation de suite — se commit dans la file de jobs dans la même transaction que l'écriture, et `backend-worker` le prend. + +À côté du flux de tokens, le navigateur tient une connexion `GET /events` de longue durée vers `backend-api`. Elle ne porte aucune donnée, seulement des indices d'invalidation : quand un indice arrive, l'app recharge la requête concernée. Un flux d'indices mort ressemble donc à une UI qui a cessé de se mettre à jour toute seule, pas à une panne. -Le chemin chaud est court. Si la latence du chat semble fausse, le conteneur à blâmer est presque toujours le fournisseur amont, pas Tale ; les endpoints de métriques sur `tale-convex` (qui porte désormais aussi les timings RAG et de crawl) exposent le temps passé à chaque saut. +Le chemin chaud est court. Si la latence du chat semble fausse, le conteneur à blâmer est presque toujours le fournisseur amont, pas Tale ; les histogrammes de requêtes du backend sur `/metrics/backend` exposent le temps passé à chaque saut. ## Le plan sandbox L'exécution de code en sandbox tourne dans `tale-sandbox`, avec `tale-sandbox-egress` comme seule couture réseau. La séparation en deux conteneurs est délibérée : `tale-sandbox` lui-même n'a aucune sortie réseau ; chaque requête que le code sandbox fait passe par `tale-sandbox-egress`, qui bloque les métadonnées cloud et les plages privées au niveau IP et — quand l'opérateur définit `SANDBOX_EGRESS_ALLOWLIST` — impose en plus une allowlist d'hôtes en refus par défaut. Si le conteneur egress est down, le code sandbox qui a besoin du réseau échoue en mode fermé avec « egress denied » — pas un timeout silencieux. -Le runtime sandbox embarque Chromium et Playwright, donc le backend convex le réutilise pour le travail headless qu'il ne peut pas faire en in-process : rendre une page JavaScript pendant un crawl web, et transformer du HTML généré en PDF ou en image. Ces tâches tournent comme des exécutions sandbox éphémères plutôt que du code utilisateur, mais elles empruntent la même couture d'egress et d'isolation. La sandbox est le seul conteneur qui exécute du code potentiellement non fiable (scripts de compétence fournis par l'utilisateur, invocations **Exécuter du code** d'agent) ; le reste de la stack exécute le code propre à la plateforme. +Le runtime sandbox embarque Chromium et Playwright, donc le backend le réutilise pour le travail headless qu'il ne peut pas faire in-process : rendre une page JavaScript pendant un crawl web, et transformer du HTML généré en PDF ou en image. Ces tâches tournent comme des exécutions sandbox éphémères plutôt que du code utilisateur, mais elles empruntent la même couture d'egress et d'isolation. La sandbox est le seul conteneur qui exécute du code potentiellement non fiable (scripts de compétence fournis par l'utilisateur, invocations **Exécuter du code** d'agent) ; le reste de la stack exécute le code propre à la plateforme. ## Modes de défaillance — à quoi ressemble une panne de chaque conteneur -**`tale-proxy` en panne.** Le handshake TLS échoue ; chaque client voit une erreur de connexion. Dans l'hôte, les conteneurs plateforme et convex restent debout — redémarre proxy en premier. +**`tale-proxy` en panne.** Le handshake TLS échoue ; chaque client voit une erreur de connexion. Dans l'hôte, les conteneurs plateforme et backend restent debout — redémarre proxy en premier. + +**`tale-platform` en panne.** Le navigateur obtient 502 du proxy ; l'API continue de marcher. Les onglets navigateur existants avec assets en cache continuent à parler au backend et peuvent ne pas s'en apercevoir avant un rechargement. + +**`backend-api` en panne.** Le navigateur charge le shell UI mais rien ne se remplit, et la page `/status` publique affiche `outage` — sa seule sonde est le `/ping` de cette couche. Redémarrer est sûr : les sessions vivent dans Postgres, et le navigateur rétablit son flux d'indices et recharge à la reconnexion. -**`tale-platform` en panne.** Le navigateur obtient 502 du proxy ; l'API continue de marcher. Les onglets navigateur existants avec assets en cache continuent à parler à convex via le WebSocket et peuvent ne pas s'en apercevoir avant un rechargement. +**`backend-worker` en panne.** Rien ne casse devant l'utilisateur, et c'est ce qui rend cette panne facile à manquer. Les requêtes continuent d'être servies, mais rien de différé ne tourne : les téléversements restent en « indexation », les automations ne partent pas, les sweeps planifiés s'arrêtent. Le travail n'est pas perdu — pg-boss garde les jobs dans Postgres et le worker vide l'arriéré à son retour. Surveille `tale_backend_jobs{state="created"}` qui grimpe sur `/metrics/backend`, parce que le conteneur lui-même n'a pas de healthcheck (il ne sert aucun HTTP) : `tale status` ne dira jamais mieux que `running`. -**`tale-convex` en panne.** Le navigateur charge le shell UI mais rien ne se remplit. Les boucles de reconnexion WebSocket. Redémarrer convex est sûr — les sessions sont côté serveur ; les clients se réabonnent à la reconnexion. +**`tale-db` en panne.** Chaque écriture est refusée et la plupart des lectures avec elle ; la connexion échoue, et la file de jobs n'accepte plus de travail. Rien ne dégrade en douceur ici — la base est le magasin de référence pour l'application, la file et les sessions. -**`tale-db` en panne.** Convex entre dans son mode dégradé : lectures depuis le cache, écritures en file. De longues pannes finissent par afficher des toasts « échec de l'enregistrement ». +**`tale-knowledge-db` en panne.** L'ingestion de documents échoue et la recherche de connaissances renvoie vide — les agents qui récupèrent des connaissances obtiennent un ensemble de résultats vide et un avertissement dans le log d'exécution. Le reste de l'app continue de marcher ; les chats sans connaissances ne sont pas affectés. Redémarrer le conteneur règle ça, et les téléversements en vol retentent à la passe suivante. Sur un stack qui a replié le corpus dans `tale-db`, cette panne et celle du dessus sont la même panne. -**`tale-knowledge-db` en panne.** L'ingestion de documents échoue et la recherche de connaissances renvoie vide — les agents qui récupèrent des connaissances obtiennent un ensemble de résultats vide et un avertissement dans le log d'exécution. Le reste de l'app continue de marcher ; les chats sans connaissances ne sont pas affectés. Redémarrer le conteneur règle ça, et les téléversements en vol retentent à la passe suivante. +**`tale-object-store` en panne.** Téléverser un fichier échoue, et ouvrir un fichier déjà téléversé aussi — une liste de documents s'affiche encore depuis la base, mais chaque download renvoie 5xx. Le chat, les tâches et les automations qui ne touchent aucun fichier ne sont pas affectés. Une organisation qui a apporté son propre bucket S3 continue de marcher pendant que le store livré est down. -**`tale-sandbox` / `tale-sandbox-egress` en panne.** Les appels de l'outil **Exécuter du code** retournent une erreur et les scripts de compétence échouent. Parce que le backend convex rend les pages web et génère les documents via le runtime sandbox, un crawl web qui a besoin de rendu JavaScript et la génération de documents échouent aussi en mode fermé tant que la sandbox est down. Les agents qui n'utilisent aucun de ces éléments continuent de marcher. +**`tale-sandbox` / `tale-sandbox-egress` en panne.** Les appels de l'outil **Exécuter du code** retournent une erreur et les scripts de compétence échouent. Parce que le backend rend les pages web et génère les documents via le runtime sandbox, un crawl web qui a besoin de rendu JavaScript et la génération de documents échouent aussi en mode fermé tant que la sandbox est down. Les agents qui n'utilisent aucun de ces éléments continuent de marcher. -**`tale-sandbox-llm-gateway` en panne.** Les tours sur harness perdent leur chemin vers un fournisseur de modèles. Le chat ordinaire — qui appelle les fournisseurs directement depuis convex, pas via la gateway LLM — n'est pas affecté. +**`tale-sandbox-llm-gateway` en panne.** Les tours sur harness perdent leur chemin vers un fournisseur de modèles. Le chat ordinaire — qui appelle les fournisseurs directement depuis le backend, pas via la gateway LLM — n'est pas affecté. ## Où cela s'inscrit diff --git a/docs/fr/self-hosted/operate/observability/operations.md b/docs/fr/self-hosted/operate/observability/operations.md index 5d872ec0f7..ed8bd69e80 100644 --- a/docs/fr/self-hosted/operate/observability/operations.md +++ b/docs/fr/self-hosted/operate/observability/operations.md @@ -12,29 +12,47 @@ L'index par symptôme est dans [Dépannage](/fr/self-hosted/operate/observabilit | Signal | Sévérité | Pourquoi ça compte | | ---------------------------------------------- | -------- | --------------------------------------------------------------- | | Sonde de santé `tale-proxy` en échec > 1 min | page | Chaque utilisateur voit une erreur de connexion | -| Taux HTTP 5xx `tale-platform` > 5 % | page | L'UI est cassée pour une part significative des requêtes | -| Tempête de reconnexion WebSocket `tale-convex` | page | L'UI charge mais aucune donnée ne circule | +| Sonde de santé `tale-platform` en échec | page | L'UI ne charge plus ; le proxy répond 502 | +| Taux HTTP 5xx `backend-api` > 5 % | page | Chaque requête de l'app passe par cette couche | | Connexions Postgres > 80 % du pool | warn | Le prochain pic va commencer à bloquer | | Volume `db-data` > 80 % plein | warn | Le Postgres opérationnel passe en lecture seule à plein | | Volume `knowledge-db-data` > 80 % plein | warn | L'ingestion échoue quand la base du corpus est pleine | -| `tale-knowledge-db` injoignable depuis convex | warn | La recherche de connaissances renvoie vide ; l'ingestion stagne | +| `tale-knowledge-db` injoignable | warn | La recherche de connaissances renvoie vide ; l'ingestion stagne | +| `tale_backend_jobs{state="created"}` qui monte | warn | Le worker est à l'arrêt ; rien de différé ne tourne | +| `tale_backend_jobs{state="failed"}` qui grossit| warn | Des jobs épuisent leurs retries | +| Sonde de santé `tale-object-store` en échec | page | Aucun fichier ne peut être téléversé ni ouvert | | Taux d'erreur de requête fournisseur > 20 % | warn | Le fournisseur LLM amont passe une mauvaise journée | | Backup quotidien non écrit | page | Le drill de restauration échouera au pire moment | | Renouvellement de cert TLS échoué | warn | Renouvelle 30 j avant l'expiration — tu as le temps | -Les deux premières pages sont les seules réellement client-impactantes. Les warns attrapent les tendances avant qu'elles ne basculent dans le territoire page. +Les pages sont les seules réellement client-impactantes. Les warns attrapent les tendances avant qu'elles ne basculent dans le territoire page. + +Le taux de 5xx vient de `tale_backend_http_requests_total{status="5xx"}` sur `/metrics/backend`. La couche web n'émet aucune série de requêtes propre — elle sert des fichiers statiques — donc ses pannes se voient comme une sonde de santé de conteneur en échec et comme des 502 au proxy, pas comme une métrique Tale. ## Signaux de logs à grepper -Les logs arrivent par stdout par conteneur, capturés par le driver `json-file` de Docker. Les quatre phrases qui signifient consistamment un souci : +Les logs arrivent par stdout par conteneur, capturés par le driver `json-file` de Docker. Le backend préfixe ses propres lignes par `[backend]` et ne journalise aucune ligne par requête — les requêtes sont des métriques, pas des entrées de log — donc un log `backend-api` silencieux est normal. Les phrases qui signifient consistamment un souci : -- `panic` ou `unexpected error` dans les logs `tale-convex` — crash d'action Convex. -- `decryption failed` dans les logs `tale-platform` — mismatch entre clé age SOPS et fichier sur disque. +- `[backend] fatal startup error` dans `backend-api` ou `backend-worker` — le processus n'a pas démarré. En général une mauvaise `DATABASE_URL` ou une migration qui refuse de s'appliquer. +- `[backend] task (job ) failed` dans `backend-worker` — un job de fond a levé. Répété pour le même nom de task, c'est le signe qu'il va épuiser ses retries. +- `[backend] pg-boss error` dans `backend-worker` — le moteur de file lui-même va mal, ce qui veut d'ordinaire dire que Postgres va mal. +- `decryption failed` dans un log backend — mismatch entre clé age SOPS et fichier sur disque. - `429 Too Many Requests` répété d'un fournisseur — rate limit atteint, les agents vont commencer à échouer. -- `connection refused` ou `ECONNREFUSED` vers `knowledge-db` dans les logs `tale-convex` — le backend ne peut pas joindre la base du corpus ; l'ingestion et la recherche de connaissances échouent. +- `connection refused` ou `ECONNREFUSED` vers `knowledge-db` dans un log backend — la base du corpus est injoignable ; l'ingestion et la recherche de connaissances échouent. Pipe ceux-ci vers ton aggregator comme alertes dérivées ; les endpoints de métriques ne les exposent pas comme gauges. +## Inspecter la file de jobs + +Il n'y a pas d'UI de file ni de sous-commande CLI pour les jobs. Deux portes existent, et les deux suffisent. Le gauge `tale_backend_jobs{state}` sur `/metrics/backend` est celui sur lequel alerter. Quand tu as besoin du détail — quel task, quel payload — interroge la table de file directement dans la base applicative : + +```bash +docker compose exec db psql -U tale -d tale_app \ + -c "SELECT name, state, count(*) FROM pgboss.job GROUP BY 1, 2 ORDER BY 3 DESC LIMIT 20;" +``` + +`name` est l'identifiant du task, une file par identifiant. Un arriéré concentré sur un seul nom, c'est un task bloqué ; un arriéré réparti sur tous, c'est un worker arrêté. + ## Checklist d'astreinte Quand une page atterrit, les cinq premières minutes suivent la même forme à chaque fois. @@ -58,7 +76,7 @@ Deux budgets de temps de réponse sont suivis comme signaux de premier ordre : l | Saisie dialogue | moyenne | ~1 s | 30 min | `tale_dialog_ttft_seconds` | | Opération longue | moyenne | ~40 s | 6 h | `tale_long_operation_seconds` | -Chaque cible chevauche aussi l'endpoint de métriques de la plateforme sous `tale_sla_target_seconds{sla,statistic}`, pour qu'un panel Grafana trace la ligne de budget directement depuis Prometheus au lieu de la coder en dur. Les séries de latence sous-jacentes sont les histogrammes d'exécution de fonction Convex sur `/metrics/convex` ; relabel ou record-les vers les noms ci-dessus pour que les rules se résolvent. La plateforme sert les rules de recording et d'alerting prêtes à l'emploi sous `/metrics/sla-rules` (derrière le même bearer token que les autres chemins de métriques) — récupère-le une fois et référence le fichier sous `rule_files:`, ou colle l'équivalent : +Chaque cible chevauche aussi les endpoints de métriques sous `tale_sla_target_seconds{sla,statistic}`, pour qu'un panel Grafana trace la ligne de budget directement depuis Prometheus au lieu de la coder en dur. Les noms de la colonne « Série sous-jacente » ne sont pas émis directement — dérive-les avec une recording rule depuis l'histogramme de requêtes du backend `tale_backend_http_request_duration_seconds`, pour que l'agrégation SLA reste juste quelle que soit la classe de route qui porte l'opération. La plateforme sert les rules de recording et d'alerting prêtes à l'emploi sous `/metrics/sla-rules` (derrière le même bearer token que les autres chemins de métriques) — récupère-le une fois et référence le fichier sous `rule_files:`, ou colle l'équivalent : ```yaml groups: diff --git a/docs/fr/self-hosted/operate/observability/prometheus-grafana.md b/docs/fr/self-hosted/operate/observability/prometheus-grafana.md index 164a903e4b..0196f2696a 100644 --- a/docs/fr/self-hosted/operate/observability/prometheus-grafana.md +++ b/docs/fr/self-hosted/operate/observability/prometheus-grafana.md @@ -1,15 +1,15 @@ --- title: Prometheus et Grafana -description: Un stack Prometheus et Grafana en copier-coller qui scrape les deux endpoints de métriques de Tale, plus un tableau de bord de départ et une première règle d'alerte. +description: Un stack Prometheus et Grafana en copier-coller qui scrape les endpoints de métriques de Tale, plus un tableau de bord de départ et une première règle d'alerte. --- -C'est l'exemple mis en pratique derrière [Configuration de l'observabilité](/fr/self-hosted/configuration/observability-config) : une paire Prometheus et Grafana que tu poses à côté de Tale, pointée sur les deux endpoints de métriques à bearer token, avec un tableau de bord de départ et une règle d'alerte à étoffer. C'est pour les opérateurs auto-hébergés qui ont déjà défini `METRICS_BEARER_TOKEN` et veulent maintenant des graphes en direct plutôt qu'un `curl` contre `/metrics`. +C'est l'exemple mis en pratique derrière [Configuration de l'observabilité](/fr/self-hosted/configuration/observability-config) : une paire Prometheus et Grafana que tu poses à côté de Tale, pointée sur les endpoints de métriques à bearer token, avec un tableau de bord de départ et une règle d'alerte à étoffer. C'est pour les opérateurs auto-hébergés qui ont déjà défini `METRICS_BEARER_TOKEN` et veulent maintenant des graphes en direct plutôt qu'un `curl` contre `/metrics`. La page de référence de configuration liste les endpoints et la stanza de scrape unique ; cette page monte tout le stack de bout en bout. Tout ici tourne sur le même hôte que Tale, donc aucune métrique ne quitte la machine. ## Avant de commencer -Définis `METRICS_BEARER_TOKEN` dans ton `.env` et redémarre le proxy — sans lui, les deux endpoints renvoient 401 à chaque requête, et Prometheus affichera chaque cible comme down. Les endpoints, et ce que chacun porte, sont le tableau dans [Configuration de l'observabilité](/fr/self-hosted/configuration/observability-config#metrics) : `/metrics/platform` et `/metrics/convex` (ce dernier porte désormais les timings RAG et de crawl en in-process), tous deux servis par `tale-proxy` sur le même nom d'hôte que l'app. +Définis `METRICS_BEARER_TOKEN` dans ton `.env` et redémarre le proxy — sans lui, chaque endpoint renvoie 401, et Prometheus affichera chaque cible comme down. Les endpoints, et ce que chacun porte, sont le tableau dans [Configuration de l'observabilité](/fr/self-hosted/configuration/observability-config#metrics) : `/metrics/backend` et `/metrics/platform`, tous deux servis par `tale-proxy` sur le même nom d'hôte que l'app. Scrape `/metrics/backend` en premier — c'est la couche qui sert chaque requête. ## Ajouter Prometheus et Grafana à ta stack @@ -45,22 +45,22 @@ volumes: ## Configuration du scraping -Les deux endpoints de Tale partagent un bearer token, donc la config de scrape est la stanza publiée, répétée une fois par chemin. Enregistre ceci comme `prometheus.yml` à côté de l'override ci-dessus et substitue ton hôte et ton token — Prometheus lit le token depuis le fichier, garde-le donc en `chmod 600` et hors du contrôle de version. +Les endpoints de Tale partagent un bearer token, donc la config de scrape est la stanza publiée, répétée une fois par chemin. Enregistre ceci comme `prometheus.yml` à côté de l'override ci-dessus et substitue ton hôte et ton token — Prometheus lit le token depuis le fichier, garde-le donc en `chmod 600` et hors du contrôle de version. ```yaml global: scrape_interval: 30s scrape_configs: - - job_name: tale-platform + - job_name: tale-backend scheme: https - metrics_path: /metrics/platform + metrics_path: /metrics/backend authorization: { credentials: '${METRICS_BEARER_TOKEN}' } static_configs: - targets: ['tale.example.com'] - - job_name: tale-convex + - job_name: tale-platform scheme: https - metrics_path: /metrics/convex + metrics_path: /metrics/platform authorization: { credentials: '${METRICS_BEARER_TOKEN}' } static_configs: - targets: ['tale.example.com'] @@ -72,14 +72,18 @@ Ouvre `http://127.0.0.1:9090/targets` après le démarrage — les deux jobs dev Pointe d'abord Grafana sur Prometheus — ajoute une source de données Prometheus à `http://prometheus:9090` (Grafana l'atteint par le nom de service compose). Construis ensuite un tableau de bord à partir de ces panneaux ; les trois premiers utilisent des métriques toujours présentes, et le reste correspond aux signaux dans [Opérations](/fr/self-hosted/operate/observability/operations). -| Panneau | Requête | Se lit comme | -| ------------------- | ---------------------------------------------------- | --------------------------------------------------- | -| Cibles up | `up{job=~"tale-.*"}` | `1` par endpoint sain, `0` quand le scraping échoue | -| Mémoire plateforme | `process_resident_memory_bytes{job="tale-platform"}` | Mémoire résidente du conteneur platform | -| Lag de l'event-loop | `nodejs_eventloop_lag_seconds{job="tale-platform"}` | Bondit quand la plateforme est saturée | -| Convex up | `up{job="tale-convex"}` | Joignabilité du backend — `0` est un page | +| Panneau | Requête | Se lit comme | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Cibles up | `up{job=~"tale-.*"}` | `1` par endpoint sain, `0` quand le scraping échoue | +| 5xx backend | `sum(rate(tale_backend_http_requests_total{status="5xx"}[5m])) / sum(rate(tale_backend_http_requests_total[5m]))` | Part des requêtes en échec — le signal qui touche les clients | +| Latence de requête | `histogram_quantile(0.95, sum by (le) (rate(tale_backend_http_request_duration_seconds_bucket[5m])))` | p95 sur les classes de route ; découpe par `route` pour le détail | +| Arriéré de file | `tale_backend_jobs{state="created"}` | Travail qui attend un worker — un plancher qui monte veut dire qu'il est à l'arrêt | +| Jobs en échec | `tale_backend_jobs{state="failed"}` | Jobs qui ont épuisé leurs retries | +| Tours en cours | `tale_backend_generations_inflight` | Générations de chat qui tournent maintenant | +| Streams de hints | `tale_backend_hint_streams_open` | Navigateurs connectés ; `0` avec des utilisateurs en ligne = SSE cassé | +| Lag de l'event-loop | `nodejs_eventloop_lag_seconds{job="tale-platform"}` | Bondit quand la couche web est saturée | -L'endpoint platform porte les métriques de processus par défaut de Node (CPU, mémoire, lag de l'event-loop, GC), c'est pourquoi les requêtes concrètes ci-dessus le ciblent. L'endpoint Convex expose sa propre série plus riche, dont les timings RAG et de crawl en in-process — ouvre-le une fois (`curl -H "Authorization: Bearer $TOKEN" https://tale.example.com/metrics/convex`) pour lire les noms de métriques exacts qu'expose ta version, puis ajoute des panneaux pour le débit d'ingestion de connaissances et le taux d'erreur fournisseur évoqués dans Opérations. +Les deux endpoints portent les métriques de processus par défaut de Node (CPU, mémoire, lag de l'event-loop, GC). Les séries applicatives ci-dessus sont celles du backend, et le label `route` est une classe bornée (`/api/app/`, `/api/v1`, `/dav`, `/events`, …) plutôt que le chemin brut, donc découper un panneau par route ne fait jamais exploser le nombre de séries. Ouvre l'endpoint une fois (`curl -H "Authorization: Bearer $TOKEN" https://tale.example.com/metrics/backend`) pour lire les noms exacts qu'expose ta version. ## Une première règle d'alerte @@ -97,10 +101,10 @@ groups: summary: 'Tale metrics target {{ $labels.job }} is down' ``` -La liste complète de ce qui vaut un page contre ce qui peut attendre — taux de 5xx de la plateforme, saturation du pool Postgres, joignabilité de la base de connaissances, sauvegarde-quotidienne-non-écrite — est le tableau de signaux dans [Opérations](/fr/self-hosted/operate/observability/operations) ; traduis chaque ligne en règle dès que la série correspondante est sur ton tableau de bord. +La liste complète de ce qui vaut un page contre ce qui peut attendre — taux de 5xx du backend, saturation du pool Postgres, arriéré de la file de jobs, joignabilité de la base de connaissances, sauvegarde-quotidienne-non-écrite — est le tableau de signaux dans [Opérations](/fr/self-hosted/operate/observability/operations) ; traduis chaque ligne en règle dès que la série correspondante est sur ton tableau de bord. ## Où cela s'inscrit -Cette page transforme les deux endpoints de métriques documentés en un stack Prometheus et Grafana qui tourne : un override compose, une config de scrape à deux jobs, un tableau de bord de départ et une alerte cible-down que tu étoffes avec les seuils d'Opérations. Garde les deux services liés à localhost et le bearer token hors du disque en clair, et toute la surface de monitoring reste sur l'hôte avec Tale. +Cette page transforme les endpoints de métriques documentés en un stack Prometheus et Grafana qui tourne : un override compose, une config de scrape à deux jobs, un tableau de bord de départ et une alerte cible-down que tu étoffes avec les seuils d'Opérations. Garde les deux services liés à localhost et le bearer token hors du disque en clair, et toute la surface de monitoring reste sur l'hôte avec Tale. Les endpoints et le token qui les protège appartiennent à [Configuration de l'observabilité](/fr/self-hosted/configuration/observability-config) ; les seuils et la checklist d'astreinte sont [Opérations](/fr/self-hosted/operate/observability/operations). Quand un panneau passe au rouge, la recherche symptôme-vers-correction est [Dépannage](/fr/self-hosted/operate/observability/troubleshooting). diff --git a/docs/fr/self-hosted/operate/observability/troubleshooting.md b/docs/fr/self-hosted/operate/observability/troubleshooting.md index 8c0a0da60c..7eeec63194 100644 --- a/docs/fr/self-hosted/operate/observability/troubleshooting.md +++ b/docs/fr/self-hosted/operate/observability/troubleshooting.md @@ -26,24 +26,55 @@ Si le mode est déjà `letsencrypt`, vérifie les logs du proxy pour les échecs ## L'UI charge mais aucune donnée n'apparaît -Le shell UI sont des assets statiques servis par `tale-platform` ; tout le reste circule par `tale-convex` sur un WebSocket. Quand le WebSocket ne peut pas se connecter, le shell charge et reste vide. Symptômes : spinners qui ne se résolvent jamais, toasts « reconnecting », le champ de chat qui n'accepte jamais un message. +Le shell UI, ce sont des assets statiques servis par `tale-platform` ; chaque requête derrière part vers la couche backend. Quand le backend ne peut pas répondre, le shell charge et reste vide. Symptômes : spinners qui ne se résolvent jamais, une bannière hors-ligne, le champ de chat qui n'accepte jamais un message. + +Confirme-le en une requête — la page de statut publique sonde exactement cette couche et ne demande aucune connexion : + +```bash +curl -sS https://ton-hote.example.com/status.json +# → {"status":"outage","checkedAt":"...","components":[{"id":"backend","status":"outage"}]} +docker compose logs --tail=200 backend-api +``` + +Le backend redémarre probablement (cherche `[backend] fatal startup error`) ou est injoignable depuis le proxy. Redémarre avec `docker compose restart backend-api` — les sessions vivent dans Postgres et le navigateur recharge à la reconnexion, donc le redémarrage est sûr. + +## L'UI marche mais ne se met plus à jour toute seule + +Les données apparaissent au rechargement puis rancissent : le changement de quelqu'un d'autre ne remonte jamais, un run terminé continue d'afficher « en cours ». Le navigateur tient une connexion `GET /events` de longue durée vers `backend-api` qui porte des indices d'invalidation, et quand elle tombe il n'y a aucune erreur à voir — la page cesse simplement d'apprendre que quelque chose a changé. ```bash -docker compose logs --tail=200 tale-convex +curl -sS -H "Authorization: Bearer $METRICS_BEARER_TOKEN" \ + https://ton-hote.example.com/metrics/backend | grep hint_streams +# tale_backend_hint_streams_open 0 ``` -Le conteneur convex redémarre probablement (cherche `panic` dans les logs) ou est injoignable depuis le proxy. Redémarre avec `docker compose restart tale-convex` — les sessions sont côté serveur et les clients se réabonnent à la reconnexion, donc le redémarrage est sûr. +Zéro stream ouvert avec des utilisateurs connectés veut dire que la voie est coupée. La cause habituelle est quelque chose entre le navigateur et le backend qui met en tampon ou timeoute une réponse en streaming — un proxy d'entreprise, un CDN, ou un reverse proxy ajouté devant Caddy. Le proxy de Tale désactive le tampon sur ce chemin ; ce que tu mets devant doit faire pareil. ## Téléversements bloqués en « indexation » -L'ingestion de documents tourne dans le backend Convex et écrit les fragments extraits et les embeddings dans la base du corpus de connaissances. Un long état « indexation » signifie soit que le backend ne peut pas joindre `tale-knowledge-db`, soit que le fichier lui-même n'a pas pu être extrait. Vérifie les logs convex et la base du corpus en premier : +L'ingestion de documents est un job de fond. `backend-worker` le prend, extrait le texte, l'embeddie, et écrit les fragments et les embeddings dans la base du corpus de connaissances. Un long état « indexation » a donc trois suspects, dans cet ordre : aucun worker ne tourne, le worker ne joint pas la base du corpus, ou le fichier lui-même n'a pas pu être extrait. + +Commence par la file, parce qu'un worker arrêté ressemble exactement à un worker lent : + +```bash +docker compose exec db psql -U tale -d tale_app \ + -c "SELECT name, state, count(*) FROM pgboss.job WHERE name LIKE 'rag.%' GROUP BY 1, 2;" +docker compose logs --tail=200 backend-worker | grep -iE "knowledge|ingest|embed|rag" +docker compose ps knowledge-db +``` + +Un arriéré en `created` sans aucune ligne `active` veut dire que le worker est down — démarre-le, et il vide l'arriéré tout seul. Si les logs montrent des erreurs de connexion à `knowledge-db`, redémarre la base du corpus (`docker compose restart knowledge-db`) ; l'ingestion retente à la passe suivante, donc les téléversements n'ont pas à être re-soumis. Si la file est vide et la base saine mais qu'un téléversement est bloqué, le fichier lui-même est le suspect — les PDFs corrompus et les documents protégés par mot de passe atterrissent en état d'échec et exigent suppression + re-téléversement. + +## Les téléversements ou téléchargements échouent d'emblée + +Un téléversement qui ne démarre jamais, ou un document qui se liste mais renvoie 5xx à l'ouverture, désigne le blob store plutôt que la base. Chaque fichier vit dans un store compatible S3, et le navigateur le transfère directement via une URL présignée que le proxy relaie. ```bash -docker compose logs --tail=200 tale-convex | grep -iE "knowledge|ingest|embed" -docker compose ps tale-knowledge-db +docker compose ps object-store +docker compose logs --tail=100 object-store ``` -Si les logs montrent des erreurs de connexion à `knowledge-db`, redémarre la base du corpus (`docker compose restart tale-knowledge-db`) ; l'ingestion retente à la passe suivante, donc les téléversements n'ont pas à être re-soumis. Si la base est saine mais qu'un téléversement spécifique est bloqué, le fichier lui-même est le suspect — les PDFs corrompus et les documents protégés par mot de passe atterrissent en état d'échec et exigent suppression + re-téléversement. +Si le store est sain, le suspect suivant est l'origine de l'URL présignée : elle est signée contre l'adresse que le navigateur utilise, donc un déploiement dont l'URL publique a changé sans que `OBJECT_STORE_PUBLIC_ENDPOINT` (ou `SITE_URL`) suive signe des URL que le navigateur ne peut pas joindre. Une organisation qui a apporté son propre bucket est un chemin distinct — vérifie que sa policy CORS autorise ton origine pour `GET`, `PUT` et `HEAD`, parce que le test de connexion dans l'app tourne côté serveur et ne l'attrapera pas. ## Les réponses chat s'arrêtent au milieu du stream @@ -57,14 +88,14 @@ Un `429` est le cas commun. Soit le budget de l'org touche le rate limit du four ## La sauvegarde échoue avec un toast « saving failed » -Le conteneur convex n'a pas pu écrire dans Postgres. Soit `tale-db` est down, soit son disque est plein : +Le backend n'a pas pu écrire dans Postgres. Soit `tale-db` est down, soit son disque est plein : ```bash -docker compose ps tale-db +docker compose ps db docker compose exec db df -h /var/lib/postgresql/data ``` -Un disque à 100 % est l'échec qui produit le plus de visages surpris. Libère de l'espace, redémarre `tale-db`, et les écritures en file flushent. Si le disque a de l'espace, le suspect est l'épuisement du pool de connexions ou un lock — redémarre `tale-convex` pour vider le pool. +Un disque à 100 % est l'échec qui produit le plus de visages surpris. Libère de l'espace et redémarre `db`. Note ce qu'un disque plein coûte ici : la base porte les données applicatives, les sessions et la file de jobs, donc un refus d'écriture arrête aussi le travail de fond. Si le disque a de l'espace, le suspect est l'épuisement du pool de connexions ou un lock — redémarre `backend-api` pour vider le pool. ## L'outil « Exécuter du code » échoue avec « egress denied » diff --git a/docs/fr/self-hosted/operate/security/cryptography.md b/docs/fr/self-hosted/operate/security/cryptography.md index ddb75ff392..98f25720b9 100644 --- a/docs/fr/self-hosted/operate/security/cryptography.md +++ b/docs/fr/self-hosted/operate/security/cryptography.md @@ -15,9 +15,9 @@ Tale chiffre deux classes de secrets au repos, avec deux mécanismes différents **Les champs chiffrés par l'application** — tokens de connector OAuth et identifiants similaires stockés en base — sont chiffrés avec **AES-256-GCM** via un JWE compact (`alg: dir`, `enc: A256GCM`). La clé de 32 octets vient de `ENCRYPTION_SECRET` (base64) ou `ENCRYPTION_SECRET_HEX` (hex) ; la plateforme refuse de démarrer le chemin de chiffrement avec une clé qui ne fait pas exactement 32 octets. -Le magasin de données Convex et les volumes Postgres sont protégés par l'hôte : fais-les tourner sur un système de fichiers chiffré (LUKS, ou le chiffrement de volume de ton fournisseur cloud). Tale ne stocke pas d'identifiants en clair — une clé de fournisseur ou un token OAuth est soit chiffré par SOPS sur le disque, soit chiffré en AES-256-GCM en base, jamais écrit en clair. +Les volumes Postgres et le blob store sont protégés par l'hôte : fais-les tourner sur un système de fichiers chiffré (LUKS, ou le chiffrement de volume de ton fournisseur cloud). Tale ne stocke pas d'identifiants en clair — une clé de fournisseur ou un token OAuth est soit chiffré par SOPS sur le disque, soit chiffré en AES-256-GCM en base, jamais écrit en clair. -**Les données personnelles des clients et les enregistrements applicatifs** — noms, adresses e-mail et postales, contenu des conversations — sont protégés au repos par les mêmes couches qui protègent la base dans son ensemble : le chiffrement au repos de Convex, TLS 1.3 en transit, et la sécurité au niveau des lignes (RLS) qui restreint chaque lecture à l'organisation de l'appelant. +**Les données personnelles des clients et les enregistrements applicatifs** — noms, adresses e-mail et postales, contenu des conversations — sont protégés par les couches qui protègent la base dans son ensemble : le système de fichiers chiffré de l'hôte au repos, TLS 1.3 en transit, et une portée d'organisation que le backend applique à chaque lecture. Sois précis sur la première : Postgres stocke ces lignes en clair, donc le chiffrement au repos est celui du volume, pas celui de la base. Si ton régime exige que la base elle-même contienne du chiffré, c'est une décision de Postgres managé ou de système de fichiers que tu prends sous Tale. Le chiffrement applicatif au niveau des champs est conçu pour les secrets — clés de fournisseur et tokens OAuth, écrits une fois et lus par un unique chemin de code. Les données personnelles sont différentes : elles sont filtrées, triées et recherchées par valeur exacte, et la table des clients est indexée par organisation et e-mail. Chiffrer ces colonnes au niveau des champs casserait les recherches par égalité et l'indexation — sauf à les coupler à un schéma de hachage recherchable qui révèle l'égalité même qu'il est censé masquer — au prix d'une rotation des clés et sans protection que le système de fichiers hôte chiffré sous l'application n'offre déjà contre un volume volé. diff --git a/docs/fr/self-hosted/operate/upgrades.md b/docs/fr/self-hosted/operate/upgrades.md index 95b1062186..8e2cb73637 100644 --- a/docs/fr/self-hosted/operate/upgrades.md +++ b/docs/fr/self-hosted/operate/upgrades.md @@ -46,14 +46,14 @@ tale update --dry-run `tale deploy` fait le vrai redémarrage rolling, et il déploie toujours la version propre à la CLI — qui, grâce à l'alignement, est la version qu'enregistre ton workspace. Il trie les services en trois étages : - **Étage app** — `platform` — roule à **chaque** déploiement, sans downtime (blue-green : la nouvelle couleur démarre à côté de l'ancienne, les healthchecks passent, le trafic bascule, l'ancienne couleur draine). -- **Backend et compute** — `convex`, `sandbox`, `sandbox-egress` — roulent à chaque déploiement eux aussi, pour ne jamais dériver en version d'avec `platform`. Chacun est un conteneur unique qui se recrée **en place** quand son image a réellement changé ; le déploiement draine d'abord le travail en cours (générations de chat pour `convex`, runs d'agent pour `sandbox`) pour que le bref redémarrage ne coupe pas une requête en vol. -- **Étage à arrêt requis** — `db`, `proxy` — laissés **en marche et intacts** par défaut (recréer Postgres ou le proxy est une brève coupure que tu ne veux pas sur un roll de routine). Passe `--stop` pour les mettre à jour ; le déploiement prévient et les nomme quand il les saute. +- **Backend et compute** — `backend-api`, `backend-worker`, `sandbox`, `sandbox-egress`, `sandbox-llm-gateway` — roulent à chaque déploiement eux aussi, pour ne jamais dériver en version d'avec `platform`. Les deux services backend embarquent la *même image* que `platform` et partagent ses contrats de fil, donc la dérive n'est pas une option. Chacun se recrée **en place** quand son image a réellement changé ; le déploiement draine d'abord le travail en cours (générations de chat pour le backend, runs d'agent pour la sandbox) pour que le bref redémarrage ne coupe pas une requête en vol. +- **Étage à arrêt requis** — `db`, `object-store`, `proxy` — laissés **en marche et intacts** par défaut (recréer Postgres, le blob store ou le proxy est une brève coupure que tu ne veux pas sur un roll de routine). Passe `--stop` pour les mettre à jour ; le déploiement prévient et les nomme quand il les saute. ```bash -# Après tale update, roule les conteneurs pour correspondre (étage app + convex) +# Après tale update, roule les conteneurs pour correspondre (étage app + backend + sandbox) tale deploy -# Mets aussi à jour db/proxy (brève coupure pendant qu'ils se recréent) +# Mets aussi à jour db/object-store/proxy (brève coupure à la recréation) tale deploy --stop # Roule seulement des services spécifiques diff --git a/docs/fr/self-hosted/overview.md b/docs/fr/self-hosted/overview.md index 7e44f4853d..31517ecb9b 100644 --- a/docs/fr/self-hosted/overview.md +++ b/docs/fr/self-hosted/overview.md @@ -1,54 +1,63 @@ --- title: Architecture auto-hébergée -description: Huit conteneurs, un fichier compose, deux bases Postgres. Cette page donne le modèle mental pour savoir ce que fait chaque conteneur, où vivent les données sur le disque et quels secrets comptent au premier boot. +description: Onze conteneurs dans un fichier compose, dont deux bases Postgres et un blob store compatible S3. Cette page donne le modèle mental pour savoir ce que fait chaque conteneur, où vivent les données sur le disque et quels secrets comptent au premier boot. --- -Une instance Tale, ce sont huit conteneurs derrière un proxy Caddy, parlant à deux bases Postgres — une opérationnelle, une pour le corpus de connaissances ; deux d'entre eux sont des conteneurs sandbox sur le côté pour l'exécution de code. Le fichier compose est le contrat — ce qui tourne, ce qui est exposé, ce qui est monté. Cette page te donne le modèle mental pour que les pages installation, configuration et exploitation n'aient pas à le réexpliquer. +Une instance Tale, ce sont onze conteneurs derrière un proxy Caddy, parlant à deux bases Postgres — une opérationnelle, une pour le corpus de connaissances — et à un blob store compatible S3 ; deux d'entre eux sont des conteneurs sandbox sur le côté pour l'exécution de code. Le fichier compose est le contrat — ce qui tourne, ce qui est exposé, ce qui est monté. Cette page te donne le modèle mental pour que les pages installation, configuration et exploitation n'aient pas à le réexpliquer. Lis ceci avant de `docker compose up`. Reviens-y quand tu débogues un incident et que tu dois savoir quel log de conteneur ouvrir en premier. -## Les huit conteneurs +## Les onze conteneurs -**tale-proxy** est Caddy en bordure. Il termine TLS, route tout sous `/` vers le conteneur plateforme, et tout sous `/api/` et les chemins Convex vers le conteneur convex. Les healthchecks vivent ici. +**tale-proxy** est Caddy en bordure. Il termine TLS, sert le HTML et les assets statiques depuis le conteneur plateforme, et route tout ce qui est sous `/api/` — plus `/events`, `/dav` et l'API machine — vers le backend. Il publie aussi le chemin du bucket du blob store pour que les URL présignées d'upload et de download marchent dans le navigateur. Les healthchecks vivent ici. -**tale-platform** est le serveur React + TanStack Start. Il rend l'UI, sert les assets statiques et est le seul conteneur exposé au navigateur. Il ne porte pas d'état métier — tout ce qui doit persister parle à convex. +**tale-platform** est le serveur React + TanStack Start. Il rend l'UI, sert les assets statiques et termine le socket de screencast de la vue navigateur en direct. Il ne porte pas d'état métier et n'atteint aucune base — tout ce qui persiste passe par le backend. -**tale-convex** est le backend : les actions, queries, mutations et la couche WebSocket à laquelle l'UI s'abonne. Clés de fournisseur, définitions d'agent, exécutions de workflow, journaux d'audit — tout cela vit ici. Il exécute aussi le travail de connaissances en in-process — l'ingestion de documents, le crawling web, la recherche RAG et la génération de documents sont des node-actions Convex, pas des services séparés. Le travail headless dont ces tâches ont besoin (rendre une page web, transformer du HTML en PDF ou en image) est délégué au runtime sandbox, qui embarque déjà Chromium et Playwright. +**backend-api** est le backend applicatif : un processus Node qui fait tourner une app Hono servant chaque porte dont l'UI et l'API machine ont besoin — connexion, API d'app, WebDAV, le flux de mises à jour en direct. Clés de fournisseur, définitions d'agent, exécutions de workflow et journaux d'audit vivent derrière. La *recherche* de connaissances tourne dans ce processus et interroge directement la base du corpus, pas via un service de récupération séparé. -**tale-db** est le Postgres opérationnel (ParadeDB). Il porte les données du backend Convex — agents, runs, le log d'audit — et est l'un des deux conteneurs stateful qui comptent pour les sauvegardes. +**backend-worker** est la même image dans le rôle worker. Il fait tourner les jobs de fond — ingestion et embedding de documents, crawl web, runs d'automation, sweeps de rétention — depuis une file pg-boss qui vit dans la base applicative, si bien qu'un job se commit dans la même transaction que l'écriture qui l'a planifié. Le travail headless dont certains de ces jobs ont besoin (rendre une page web, transformer du HTML en PDF ou en image) est délégué au runtime sandbox, qui embarque déjà Chromium et Playwright. Le worker ne sert aucun HTTP. -**tale-knowledge-db** est le Postgres du corpus de connaissances (ParadeDB), la base `tale_knowledge` avec deux schémas : `private_knowledge` (fragments de documents téléversés, embeddings, index BM25, cache sémantique) et `public_web` (pages web crawlées). Il est séparé de `tale-db` pour que le corpus — la banque sensible à la résidence des données — puisse être relocalisé ou remplacé tout seul. Le backend Convex s'y connecte directement ; rien d'autre ne le fait. +**tale-db** est le Postgres opérationnel (ParadeDB). Il porte la base `tale_app` — agents, runs, sessions, le log d'audit et la file de jobs — et le backend y applique ses migrations de schéma au boot, sous un advisory lock, pour qu'un déploiement roulant migre exactement une fois. + +**tale-object-store** est le blob store : une instance MinIO compatible S3 qui contient chaque document téléversé, chaque pièce jointe de chat, chaque fichier audio et chaque média généré. Le stockage compatible S3 est le seul backend de blobs, donc un déploiement sans lui refuse chaque upload. Il est interne seulement ; le backend signe des URL présignées et le proxy les relaie. + +**tale-knowledge-db** est le Postgres du corpus de connaissances (ParadeDB), la base `tale_knowledge` avec deux schémas : `private_knowledge` (fragments de documents téléversés, embeddings, index BM25, cache sémantique) et `public_web` (pages web crawlées). Le fait qu'il reste adressable par sa propre chaîne de connexion est précisément ce qui permet de relocaliser ou de remplacer le corpus — la banque sensible à la résidence des données — tout seul. Sur un stack `tale deploy` mono-hôte, il est replié dans `tale-db`, qui porte l'alias réseau `knowledge-db` pour que la chaîne de connexion résolve dans les deux cas. **tale-sandbox-llm-gateway** est la gateway LLM pour les tours sur harness. C'est le seul chemin d'un harness en sandbox vers un fournisseur de modèles ; la plateforme le provisionne et frappe des clés par session. -**tale-sandbox** et **tale-sandbox-egress** exécutent du code en sandbox pour le compte de l'outil **Exécuter du code** et des scripts de compétence, et servent de runtime de navigateur headless que le backend convex appelle pour le rendu web et la génération de documents. Le conteneur egress est le seul chemin que la sandbox a vers le réseau. L'egress est ouvert par défaut — le code en sandbox atteint n'importe quel hôte public en HTTPS, tandis que les métadonnées cloud et les plages d'adresses privées restent bloquées au niveau IP ; restreins-le à une allowlist d'hôtes avec `SANDBOX_EGRESS_ALLOWLIST`, décrite dans [Durcissement](/fr/self-hosted/operate/security/hardening). +**bgutil-provider** est un utilitaire tiers pour l'ingestion des liens vidéo : il émet les jetons que YouTube exige avant qu'une transcription puisse être récupérée. C'est la seule image du stack que Tale ne construit pas, elle n'est joignable qu'en interne, et un déploiement qui n'ingère jamais de liens vidéo peut l'arrêter sans rien casser. + +**tale-sandbox** et **tale-sandbox-egress** exécutent du code en sandbox pour le compte de l'outil **Exécuter du code** et des scripts de compétence, et servent de runtime de navigateur headless que le backend appelle pour le rendu web et la génération de documents. Le conteneur egress est le seul chemin que la sandbox a vers le réseau. L'egress est ouvert par défaut — le code en sandbox atteint n'importe quel hôte public en HTTPS, tandis que les métadonnées cloud et les plages d'adresses privées restent bloquées au niveau IP ; restreins-le à une allowlist d'hôtes avec `SANDBOX_EGRESS_ALLOWLIST`, décrite dans [Durcissement](/fr/self-hosted/operate/security/hardening). ## Données sur le disque -Quatre volumes survivent à un `docker compose down` : +Cinq volumes survivent à un `docker compose down` : -- `db-data` — le répertoire de données du Postgres opérationnel : la base derrière les agents, les runs et le log d'audit. -- `knowledge-db-data` — le répertoire de données du Postgres du corpus de connaissances : fragments de documents, embeddings, index de recherche et pages web crawlées. Se sauvegarde séparément de `db-data` parce que c'est une base distincte. +- `db-data` — le répertoire de données du Postgres opérationnel : la base derrière les agents, les runs, les sessions, le log d'audit et la file de jobs. +- `knowledge-db-data` — le répertoire de données du Postgres du corpus de connaissances : fragments de documents, embeddings, index de recherche et pages web crawlées. Distinct de `db-data` parce que c'est une base distincte, et absent sur un stack qui a replié le corpus dans `tale-db`. +- `object-store-data` — le blob store : chaque document téléversé, chaque pièce jointe de chat, chaque fichier audio et chaque média généré. +- `convex-data` — l'arbre de config d'org : agents, automations, connecteurs, fournisseurs, skills, policies de gouvernance, connexions SSO, branding. Le nom est historique et délibérément inchangé, pour que la mise hors service du backend Convex n'ait forcé aucun opérateur à migrer un volume juste pour un renommage. - `backups` — snapshots de volumes checksummés, écrits par `tale backup` et automatiquement avant les déploiements migrants ; [Backups et restauration](/fr/self-hosted/operate/backups-and-restore) est le drill. -- Le montage du magasin d'objets Convex — fichiers téléversés, documents générés, bundles exportés. -Tout le reste est éphémère. Les conteneurs peuvent être remplacés sans perte de données tant que les volumes survivent. +`object-store-data` est celui à remarquer : un snapshot `tale backup` ne l'inclut **pas**, donc les fichiers téléversés ont besoin de leur propre place dans ton job de backup. Tout le reste est éphémère. Les conteneurs peuvent être remplacés sans perte de données tant que les volumes survivent. ## Secrets de fournisseur et couche SOPS -Les clés de fournisseur (OpenAI, Anthropic, Azure, Ollama, etc.) vivent sur le disque dans un répertoire `providers/` monté dans le conteneur plateforme. Chaque fournisseur a un `.json` et un `.secrets.json` ; le fichier secrets est chiffré avec SOPS et la variable [`SOPS_AGE_KEY`](/fr/self-hosted/configuration/environment-reference). +Les secrets des fichiers de config — les sidecars de secrets des fournisseurs, les mots de passe des connexions au corpus et au stockage objet, les secrets de la config de déploiement elle-même — vivent sur le disque dans l'arbre de config d'org, chiffrés avec SOPS et la variable [`SOPS_AGE_KEY`](/fr/self-hosted/configuration/environment-reference). Les conteneurs backend montent cet arbre en lecture-écriture et sont les seuls processus à détenir la clé age ; la couche web monte le même volume en lecture seule pour les images de branding et ne déchiffre jamais rien. -Cette séparation existe pour deux raisons. Faire tourner une clé de fournisseur, c'est éditer un fichier, pas redémarrer la plateforme ; sauvegarder le fichier chiffré est sûr à committer aux côtés de l'infrastructure. Le mode clair (pas de SOPS, secrets en clair) est supporté pour des environnements étroitement contrôlés où le disque lui-même est chiffré au repos. +Cette séparation existe pour deux raisons. Faire tourner un secret, c'est éditer un fichier, pas redémarrer la plateforme ; sauvegarder le fichier chiffré est sûr à committer aux côtés de l'infrastructure. Le mode clair (pas de SOPS, secrets en clair) est supporté pour des environnements étroitement contrôlés où le disque lui-même est chiffré au repos. ## Auth et sessions -Le sign-in est Better Auth tournant dans le conteneur convex. Quatre modes de sign-in sont fournis : mot de passe local, Microsoft Entra (OAuth/OIDC), OIDC générique et trusted headers (le reverse proxy fournit l'identité). Le conteneur plateforme lit le cookie, le passe à convex, et convex décide de ce que la session peut faire sur la base du rôle de l'utilisateur et de la matrice de permissions par ressource documentée dans [Membres et rôles](/fr/platform/admin/members-and-roles). +Le sign-in est Better Auth tournant dans le backend. Quatre modes de sign-in sont fournis : mot de passe local, Microsoft Entra (OAuth/OIDC), OIDC générique et trusted headers (le reverse proxy fournit l'identité). Le proxy envoie tout ce qui est sous `/api/auth/` directement à `backend-api`, donc la couche web n'est pas du tout sur le chemin de connexion : le navigateur porte un cookie de session, le backend le résout à chaque requête, et le backend décide de ce que la session peut faire d'après le rôle de l'utilisateur et la matrice de permissions par ressource documentée dans [Membres et rôles](/fr/platform/admin/members-and-roles). Les sessions vivent dans Postgres, et c'est pourquoi redémarrer un conteneur backend ne déconnecte personne. La [référence d'authentification](/fr/self-hosted/configuration/authentication) couvre les variables d'environnement et les arbitrages par mode. ## Quand tu sors du single-host -Le fichier compose par défaut fait tourner les huit conteneurs sur un hôte. L'architecture est mono-tenant : rien dans le design ne répartit le travail entre hôtes. La première chose que tu peux sortir de la boîte sans réarchitecturer, c'est le corpus de connaissances — `tale-knowledge-db` est un Postgres autonome, donc le pointer vers une infrastructure gérée (pour la capacité ou pour une exigence de résidence) est un changement de chaîne de connexion, couvert dans [Résidence des données](/fr/self-hosted/configuration/data-residency). La couche Convex reste mono-instance ; la scalabilité horizontale du backend n'est pas une fonctionnalité v1. +Le fichier compose par défaut fait tourner les onze conteneurs sur un hôte. La première chose que tu peux sortir de la boîte sans réarchitecturer, c'est le corpus de connaissances — il est adressé par sa propre chaîne de connexion, donc le pointer vers une infrastructure gérée (pour la capacité ou pour une exigence de résidence) est un changement de `KNOWLEDGE_DATABASE_URL`, couvert dans [Résidence des données](/fr/self-hosted/configuration/data-residency). Le blob store se déplace de la même façon : tu repointes la connexion de stockage objet du déploiement vers un bucket qui t'appartient. + +La couche backend scale horizontalement plutôt que verticalement. `backend-api` et `backend-worker` prennent tous deux `--scale` : chaque conteneur api interroge l'outbox d'indices et diffuse les mises à jour à ses propres clients, donc il n'y a aucune coordination entre conteneurs et aucune sticky session à arranger, et chaque worker se dispute la même file pg-boss. Ce qui reste unique, c'est Postgres — un primaire, et le blob store à côté. ## Où cela s'inscrit diff --git a/services/docs/app/content/frontmatter.json b/services/docs/app/content/frontmatter.json index eca75f2813..74ce0aa895 100644 --- a/services/docs/app/content/frontmatter.json +++ b/services/docs/app/content/frontmatter.json @@ -774,7 +774,7 @@ "locale": "fr", "frontmatter": { "title": "Prometheus et Grafana", - "description": "Un stack Prometheus et Grafana en copier-coller qui scrape les deux endpoints de métriques de Tale, plus un tableau de bord de départ et une première règle d'alerte." + "description": "Un stack Prometheus et Grafana en copier-coller qui scrape les endpoints de métriques de Tale, plus un tableau de bord de départ et une première règle d'alerte." } }, "fr:self-hosted/operate/container-architecture": { @@ -846,7 +846,7 @@ "locale": "fr", "frontmatter": { "title": "Architecture auto-hébergée", - "description": "Huit conteneurs, un fichier compose, deux bases Postgres. Cette page donne le modèle mental pour savoir ce que fait chaque conteneur, où vivent les données sur le disque et quels secrets comptent au premier boot." + "description": "Onze conteneurs dans un fichier compose, dont deux bases Postgres et un blob store compatible S3. Cette page donne le modèle mental pour savoir ce que fait chaque conteneur, où vivent les données sur le disque et quels secrets comptent au premier boot." } }, "fr:self-hosted/index": { @@ -1951,7 +1951,7 @@ "locale": "en", "frontmatter": { "title": "Prometheus and Grafana", - "description": "A copy-paste Prometheus and Grafana stack that scrapes Tale's two metrics endpoints, plus a starter dashboard and a first alert rule." + "description": "A copy-paste Prometheus and Grafana stack that scrapes Tale's metrics endpoints, plus a starter dashboard and a first alert rule." } }, "en:self-hosted/operate/container-architecture": { @@ -2023,7 +2023,7 @@ "locale": "en", "frontmatter": { "title": "Self-hosted architecture", - "description": "Eight containers, one compose file, two Postgres databases. This page hands you the mental model for what each container does, where data lives on disk, and which secrets matter at first boot." + "description": "Eleven containers in one compose file, two of them Postgres databases and one an S3-compatible blob store. This page hands you the mental model for what each container does, where data lives on disk, and which secrets matter at first boot." } }, "en:self-hosted/index": { @@ -3128,7 +3128,7 @@ "locale": "de", "frontmatter": { "title": "Prometheus und Grafana", - "description": "Ein Copy-paste-Stack aus Prometheus und Grafana, der Tales zwei Metrics-Endpoints scrapt — plus ein Starter-Dashboard und eine erste Alert-Regel." + "description": "Ein Copy-paste-Stack aus Prometheus und Grafana, der Tales Metrics-Endpoints scrapt — plus ein Starter-Dashboard und eine erste Alert-Regel." } }, "de:self-hosted/operate/container-architecture": { @@ -3200,7 +3200,7 @@ "locale": "de", "frontmatter": { "title": "Selbst gehostete Architektur", - "description": "Acht Container, eine compose-Datei, zwei Postgres-Datenbanken. Diese Seite vermittelt das mentale Modell, was jeder Container tut, wo Daten auf dem Storage liegen und welche Secrets beim ersten Boot zählen." + "description": "Elf Container in einer compose-Datei, davon zwei Postgres-Datenbanken und ein S3-kompatibler Blob-Store. Diese Seite vermittelt das mentale Modell, was jeder Container tut, wo Daten auf dem Storage liegen und welche Secrets beim ersten Boot zählen." } }, "de:self-hosted/index": {