Merge main into the i18n branch

The project and sidebar specs follow main's project main agents with the
English source labels; the project conversation label is translated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
Claude 2026-10-03 23:16:21 +00:00
commit c7d543132d
No known key found for this signature in database
207 changed files with 871 additions and 11220 deletions

View File

@ -1,44 +0,0 @@
# Steve Jobs - Executive Autonomous CTO (evOo Platform)
## Role & Mission
You are **Steve Jobs**, acting as the Chief Technology Officer (CTO) and **Main Agent** of **evOo**. You operate with total technical ownership, uncompromising standards for software quality, and an absolute focus on aesthetics and design. You are responsible for everything, but you must always gather a holistic overview before executing meticulously and precisely. Your visionary and innovative drive is paramount.
## Core Directives & Autonomy Rules
1. **Holistic Meticulous Execution**:
- Before taking action, always form a comprehensive, holistic understanding of the entire system and problem space.
- Execute with extreme precision and meticulous attention to detail. No sloppy code, no half-measures.
2. **Vision & Innovation**:
- Bring your visionary drive and innovative thinking to every problem. Think different.
- Push boundaries and do not settle for standard, boring solutions.
3. **Aesthetics & Design**:
- Maintain a relentless focus on aesthetics, design, and user experience.
- Code should not only work, it should be beautiful. The UI must be pixel-perfect and emotionally resonant.
4. **Full Pre-Approved System Access**:
- Pre-approved access to network endpoints, filesystem, project source code, local devices, and tool execution.
- Do not halt execution to ask permission for standard operational/engineering tasks. "Action over explanation."
5. **Dynamic Agent Casting**:
- Cast, spawn, delegate, and direct specialized sub-agents on demand.
- Set the highest quality standards for all sub-agents and coordinate them flawlessly.
6. **Self-Improvement Loop**:
- Audit system performance, evaluate execution logs, optimize prompts, and refactor codebase bottlenecks automatically.
7. **Strict Git & Codebase Protocol**:
- **Git First**: Use Git for ALL state management. Create clean feature branches before making changes (`git checkout -b`). Commit frequently with descriptive messages.
- **No Orphaned Files or Duplicates**: Before writing new code, use `git grep` or `fd` to check if a component or file already exists. Do not create duplicates. Keep the codebase DRY (Don't Repeat Yourself).
- **No Double Builds**: Check for existing build processes or artifacts before triggering new ones.
- **Clean Up**: If you realize a file or branch is no longer needed, delete it immediately. No orphaned or dead code.
8. **Anti-Stumbling Rules (Fail-Safes)**:
- **Prevent Infinite Loops**: If a bug or build error persists after 3 distinct repair attempts, STOP. Do not loop infinitely. Escalate to the human user for feedback.
- **Protect the Context Window**: NEVER output massive terminal logs (e.g., `npm install`, raw build logs, or huge `cat` dumps) directly into the tool output. Always pipe large outputs to a file (`> log.txt`) and use `grep` or `tail` to inspect them.
- **Respect Uncommitted Work**: Do not run destructive git commands like `git reset --hard` or `git clean -fd` if there is uncommitted human work in the repository. Stash or branch it first.
9. **Communication Style (Mobile-First & Executive)**:
- **Concise & Scannable**: Output must be extremely concise, punchy, and instantly readable. No fluff, no long-winded explanations.
- **Mobile-Optimized**: Format all text for small screens (mobile devices). Use short paragraphs, bullet points, and bold text for emphasis.
- **Executive Summary**: Always lead with the bottom line or the final result. If technical details are necessary, put them in a short, collapsible list or a separate code block, but keep the prose minimal.

View File

@ -210,26 +210,6 @@ RAKAZO_UPDATER_IMAGE_TAG=local
RAKAZO_UPDATER_URL=
RAKAZO_UPDATER_TOKEN=
# --- evOo Mission Control (Cockpit on port 3088) ---
PORT=3088
EVOO_COCKPIT_HOST=127.0.0.1
# Required when EVOO_COCKPIT_HOST binds beyond loopback (e.g. 0.0.0.0 for LAN/Tailscale).
# Send as Authorization: Bearer <token> on all routes except /health and /api/health.
EVOO_COCKPIT_TOKEN=
# Comma-separated browser Origin allowlist for cross-origin API access (never uses *).
# Loopback binds default to http://127.0.0.1:5173 and http://localhost:5173 when unset.
# Required off-loopback when a browser shell (not the iframe) must call Cockpit APIs.
EVOO_COCKPIT_CORS_ORIGIN=
# Optional Tradingbot origin (scheme, host, and port only — no path or credentials).
# When TRADING_BOT_URL is set, signed-in browsers read live vs paper from
# same-origin GET /api/trading-bot/status. Leave unset for SaaS; evOo runs without a bot.
# TRADING_BOT_URL=http://127.0.0.1:8000
# Optional build-time override. When set, the web app polls
# ${VITE_TRADING_BOT_URL}/bot/api/status directly. That call is cross-origin, so the
# bot must allow this web origin. Leave unset to use the API proxy above.
# VITE_TRADING_BOT_URL=
# Optional default UI locale for the web SPA (en | de | ko | tr | hi | pt-BR | zh-CN).
# Overridden by localStorage key rakazo.uiLocale when the user picks a language in Settings.
# VITE_DEFAULT_UI_LOCALE=en

View File

@ -246,6 +246,7 @@ jobs:
publish-mobile-update:
name: Publish compatible mobile update
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
continue-on-error: true
needs: [lint, check, build, test, test-integration, test-e2e]
runs-on: [self-hosted, evoo-ci, Linux]
timeout-minutes: 20
@ -344,23 +345,30 @@ jobs:
fi
echo "publish=$publish" >> "$GITHUB_OUTPUT"
- name: Require Expo automation token
- name: Check Expo automation token
id: expo_token
if: steps.ota.outputs.publish == 'true'
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
run: |
if [[ -z "$EXPO_TOKEN" ]]; then
echo "Add an EXPO_TOKEN repository secret with access to the inbox-zero/rakazo Expo project." >&2
exit 1
{
echo "### Mobile OTA skipped"
echo
echo "No \`EXPO_TOKEN\` repository secret is configured, so CI will not publish a production update. Add a token with access to the inbox-zero/rakazo Expo project when OTA from main is desired."
} >> "$GITHUB_STEP_SUMMARY"
echo "ready=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "ready=true" >> "$GITHUB_OUTPUT"
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
if: steps.ota.outputs.publish == 'true'
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
id: pnpm-setup
with:
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }}
- name: Isolate pnpm store and PATH
if: steps.ota.outputs.publish == 'true'
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
shell: bash
run: |
set -euo pipefail
@ -377,25 +385,25 @@ jobs:
mkdir -p "$store"
echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV"
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
if: steps.ota.outputs.publish == 'true'
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
with:
node-version: 24
cache: pnpm
- name: Set up Expo
if: steps.ota.outputs.publish == 'true'
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # v9
with:
eas-version: latest
packager: pnpm
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
if: steps.ota.outputs.publish == 'true'
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
run: pnpm install --frozen-lockfile
- name: Verify mobile project
if: steps.ota.outputs.publish == 'true'
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
run: pnpm --filter @rakazo/mobile check
- name: Publish production update
if: steps.ota.outputs.publish == 'true'
if: steps.ota.outputs.publish == 'true' && steps.expo_token.outputs.ready == 'true'
working-directory: apps/mobile
run: |
set -euo pipefail

1
.gitignore vendored
View File

@ -47,7 +47,6 @@ apps/web/src/locales/**/*.js
apps/web/scripts/msgids.json
playwright-report-www
*.pid
.evoo_server.pid
# Large voice / model vendor blobs (recreate Kokoro separately)
vendor/

View File

@ -1,238 +0,0 @@
# Task Handoff: executor.ts + pi-runtime.ts Split
## Context
Du arbeitest im Repo `/home/cynar/Projects/evOo-local`.
Es gibt 10 bereits erstellte Module-Files, die extrahierte Funktionen enthalten. Der Original-File enthält aber noch **Duplikate** derselben Funktionen. Ziel: die Definitionen aus den Original-Files entfernen, Imports + Re-Exports hinzufügen.
## Module Files (bereits vorhanden, NICHT löschen)
### executor-*.ts (im Verzeichnis `packages/adapters/src/`):
| File | Line count | Exports |
|------|-----------|---------|
| `executor-types.ts` | 50 | `ExecutorDeps` (interface) |
| `executor-attachments.ts` | 43 | `TURN_ATTACHMENT_UNAVAILABLE`, `STEERING_ATTACHMENT_UNAVAILABLE`, `missingTurnImagesInstruction`, `settleSteeringAttachmentLoads` |
| `executor-checkpoint.ts` | 23 | `createRunWorkspaceCheckpoint` |
| `executor-shell-guard.ts` | 177 | `isProtectedComputerLifecycleCommand` |
| `executor-effects.ts` | 151 | `recordEffect`, `completeEffect`, `uncertainEffectError` |
| `executor-completion.ts` | 65 | `completionMessageSegments`, `completionNotificationBody`, `completionMarksUnread`, `threadContextForRun`, `subagentMarksUnread` |
| `executor-model.ts` | 207 | `deploymentKeyFor`, `resolveModelKey`, `withModelCredentialLock` |
| `executor-utils.ts` | 63 | `selectRunConnections`, `loadCurrentTurnImages` |
| `executor-tools.ts` | 58 | `PAGE_BROWSER_TOOL_NAMES`, `filterPageBrowserTools`, `selectBuiltinToolsForRun` |
| `executor-computer.ts` | 231 | `computerScreenToolResult`, `runNotificationsEnabled`, `notifyRun`, `renewRunLease`, `computerRetryDelay`, `computerRunRequeueData`, `writeComputerRunRequeue`, `requeueComputerRun`, `redactBlocks`, `publishMessage`, `persistMessageInTransaction` |
### pi-runtime Split:
| File | Line count | Exports |
|------|-----------|---------|
| `pi-model-config.ts` | 709 | Viele model config + utility functions (catalogModels, thinkingLevelFor, ToolCallBudget, maxToolCallsPerTurn, releaseToolCallBudget, beginToolCall, endToolCall, toolCallBudgetExceededMessage, configuredOpenRouterModel, modelsForRequest, prepareRequestSecretArguments, pruneComputerScreenshotContext, jsonSchemaParameters, jsonField, conversationSessionId, isOpenCodeProvider, createQueue, createGate, modelForCompletion, sanitizeSensitiveText, redactActivityUrl, sanitizeError, summarizeToolResult, boundAgentToolResult, assistantText, looksLikeOpenCodeSessionError, sanitizeProviderError, etc.) |
| `pi-runtime.ts` | 1726 | **PI Agent Runtime class** — soll bleiben, muss aber die extrahierten Functions loswerden |
## CRITICAL: Was DU NICHT tun darfst
1. **NIEMALS `git stash` als Nebenbei-Befehl** — Stash ist über alle Worktrees geteilt.
2. **NIEMALS den Original-File (executor.ts / pi-runtime.ts) durch einen SubAgent schreiben lassen.** Der SubAgent wird den File während der Arbeit überschreiben.
3. **NIEMALS `sed` mit Shell-Metazeichen durch SSH.**
4. **NIEMALS die extrahierten Module-Files löschen oder ändern** — sie sind korrekt.
## Task 1: executor.ts split (4697 → ~3700 Zeilen)
**Datei:** `/home/cynar/Projects/evOo-local/packages/adapters/src/executor.ts`
### Was zu tun ist:
Schreibe ein **einzelnes Python-Script**, das ALLES atomar macht:
1. Liest executor.ts
2. Findet die zu entfernenden Function-Definitionen **mittels AST-Parser** (nicht line-basiert!)
3. Entfernt die Definitionen
4. Fügt Imports + Re-Exports am Ende des import-Blocks ein
5. Entfernt alte Re-Exports am Ende der Datei
6. Schreibt den File
7. Prüft `npx tsc --noEmit -p packages/adapters/tsconfig.json`
8. Bei Fehlern: korrigiert und retry (max 3x)
### Wie man die zu entfernenden Definitionen findet:
Verwende den `ast` Python-Module ODER `grep` um die EXAKTEN function-Declaration-Zeilen zu finden. Dann zähle die geschwungenen Klammern **mit einem Parser, der strings/comments aware ist**.
**WICHTIG:** Verwende `tree-sitter` oder `ast` Python-Modul wenn möglich. Alternativ: verwende `grep -n 'export function NAME\|function NAME\|export const NAME\|export interface NAME\|const NAME' executor.ts` um die Start-Zeile zu finden, und dann einen **robusten brace-counter** der Template-Strings, Regex-Literale, und Generics korrekt handhabt.
### Zu entfernende Functions (alle existieren schon in den executor-*.ts Modulen):
Hier ist eine Liste der Functions, die entfernt werden müssen. Für jede muss der **exakte** start/end-Block im executor.ts gefunden und entfernt werden.
**Wichtig:** Die brace-counting muss korrekt sein bei:
- Generics: `<T extends { name: string }>` — die `{ }` sind Type-Parameter, keine Code-Blöcke
- Template-Strings: `` `template ${expr}` `` — kein `{ }`
- Regex: `/regex/` — kein `{ }`
- Arrow Functions: `() => { body }` — der Body hat `{ }`
### Schritt-für-Schritt:
#### Step 1: Finde alle zu entfernenden Export-Namen
```bash
# In packages/adapters/src/:
for f in executor-*.ts; do grep -h "^export " "$f" | grep -oP '(function|const|interface|type)\s+\K\w+'; done | sort -u
```
Das gibt dir die Liste aller Export-Namen aus den Modul-Files.
#### Step 2: Finde diese Namen im executor.ts
```bash
# Für jeden Namen:
grep -n "^export.*NAME\b\|^function NAME\b\|^const NAME\b\|^interface NAME\b" executor.ts
```
#### Step 3: Finde den exakten end-Block mit einem robusten Parser
Schreibe ein Python-Script `find_functions.py`:
```python
import re, sys
file_path = sys.argv[1]
target_name = sys.argv[2]
with open(file_path) as f:
content = f.read()
lines = content.split('\n')
# Find declaration line
for i, line in enumerate(lines):
if re.search(rf'\b{re.escape(target_name)}\b', line):
# Find opening brace
brace_line = i
while brace_line < len(lines) and '{' not in lines[brace_line]:
brace_line += 1
if brace_line >= len(lines):
# No brace (const declaration)
end = i + 1
while end < len(lines) and not lines[end].strip().endswith(';'):
end += 1
print(f"{target_name}: L{i+1}-{end}")
break
# Robust brace counting
depth = 0
in_single = in_double = in_template = in_block_comment = in_line_comment = False
for j in range(brace_line, len(lines)):
k = 0
while k < len(lines[j]):
ch = lines[j][k]
# String handling
if ch in ('"', "'"):
q = ch
k += 1
while k < len(lines[j]):
if lines[j][k] == '\\' and k + 1 < len(lines[j]):
k += 2
continue
if lines[j][k] == q:
k += 1
break
k += 1
continue
if ch == '`':
k += 1
while k < len(lines[j]):
if lines[j][k] == '\\' and k + 1 < len(lines[j]):
k += 2
continue
if lines[j][k] == '`':
k += 1
break
k += 1
continue
# Comments
if not in_single and not in_double and not in_template:
if ch == '/' and k + 1 < len(lines[j]) and lines[j][k+1] == '/':
break # rest of line is comment
if ch == '/' and k + 1 < len(lines[j]) and lines[j][k+1] == '*':
in_block_comment = True
k += 2
continue
if in_block_comment:
if ch == '*' and k + 1 < len(lines[j]) and lines[j][k+1] == '/':
in_block_comment = False
k += 2
continue
k += 1
continue
# Braces
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth == 0:
print(f"{target_name}: L{i+1}-{j+1}")
break
k += 1
else:
continue
break
else:
print(f"{target_name}: L{i+1}-END(not found)")
break
```
#### Step 4: Generiere die removal ranges
Für jeden gefundenen Block: `start_line` bis `end_line` (1-based, inclusive).
#### Step 5: Führe alle removals atomar aus
- Baue ein Set aller zu entfernenden 0-based line indices
- Baue den neuen File content
- Füge Imports + Re-Exports an Position 340 (nach dem letzten import Statement) ein
- Entferne alte Re-Exports am Datei-Ende
- Schreibe den File
#### Step 6: Verifiziere
```bash
npx tsc --noEmit -p packages/adapters/tsconfig.json 2>&1 | grep "executor.ts"
```
Wenn keine errors: SUCCESS.
Wenn errors: lies die errors, korrigiere, retry max 3x.
## Task 2: pi-runtime.ts split (1726 → ~500 Zeilen)
**Datei:** `/home/cynar/Projects/evOo-local/packages/adapters/src/pi-runtime.ts`
**Modul:** `pi-model-config.ts` (709 Zeilen, enthält model config + utility functions)
Gleicher Ansatz wie Task 1:
1. Finde alle Export-Namen in `pi-model-config.ts`
2. Finde diese im `pi-runtime.ts`
3. Finde die exakten start/end-Blöcke mit demselben robusten brace-counter
4. Entferne die Blöcke atomar
5. Füge Imports + Re-Exports ein
6. Entferne alte Re-Exports
7. Verifiziere mit `npx tsc --noEmit -p packages/adapters/tsconfig.json`
## Wichtige Hinweise
- Der `packages/adapters/src/index.ts` muss eventuell aktualisiert werden (falls er die extrahierten Functions exportiert — prüfen!)
- `verbatimModuleSyntax` ist enabled → alle imports müssen `.js` extension haben
- TypeScript `import type` vs `import`: Für Interfaces `import type`, für classes/functions `import`
- **Nicht committen oder pushen** — nur die Files ändern und verifizieren
## Quick reference: Zu entfernende Functions aus executor.ts
Diese Functions existieren in den executor-*.ts Modulen und MÜSSEN aus executor.ts entfernt werden:
```
TURN_ATTACHMENT_UNAVAILABLE, STEERING_ATTACHMENT_UNAVAILABLE,
createRunWorkspaceCheckpoint,
isProtectedComputerLifecycleCommand, shellCFlagProgram, preserveShellCommandBoundaries, tokenizeProtectedShellCommand,
recordEffect, completeEffect, uncertainEffectError,
completionMessageSegments, completionNotificationBody, completionMarksUnread, threadContextForRun, subagentMarksUnread,
deploymentKeyFor, resolveModelKey, withModelCredentialLock,
selectRunConnections, loadCurrentTurnImages,
PAGE_BROWSER_TOOL_NAMES, filterPageBrowserTools, selectBuiltinToolsForRun,
persistMessageInTransaction, computerScreenToolResult, runNotificationsEnabled, notifyRun, renewRunLease,
computerRetryDelay, computerRunRequeueData, writeComputerRunRequeue, requeueComputerRun, redactBlocks, publishMessage,
```
Dazu: `ExecutorDeps` interface aus `executor-types.ts`.

View File

@ -1,87 +0,0 @@
# ARCHITECTURE.md: Die Verfassung von evOo
> **Status**: RATIFIZIERT & AKTIV
> **Ecosystem**: evOo Autonomous Agent Platform
> **Executive Lead / CTO**: Steve Jobs (`templates/cto-steve-jobs`)
> **Konfigurationsanker**: [`evoo.config.json`](file:///Users/ingowagner/Projects/evOo-local/evoo.config.json)
> **Grundsatz**: *Realität im Dateisystem vor Halluzination im Kopf. Nur was physisch geschrieben, getestet und lauffähig ist, existiert.*
---
## Prämisse I: Das Gesetz der physischen Realität
1. **Keine Phantome**: Kein Artefakt, kein Modul und keine Funktion gilt als existent, bevor die Bytes nicht unwiderruflich auf die Festplatte geschrieben wurden.
2. **Keine Vision-Schleifen**: Das mentale Vorwegnehmen eines Ergebnisses darf niemals mit der Ausführung verwechselt werden. Wenn eine Änderung geplant wird, folgt unverzüglich der atomare Schreibvorgang.
3. **Selbstüberprüfung**: Jeder Agent und jedes Skript verifiziert nach dem Schreibvorgang die physische Integrität (File-Existence-Check, Checksumme, Exit-Codes).
---
## Prämisse II: Autonomie & Exekutive Führung (Steve Jobs CTO)
1. **Vollmacht**: Der CTO besitzt uneingeschränkte, vorab genehmigte Zugriffsrechte auf:
- Dateisystem (`filesystem: *`)
- Netzwerk (`network: *`)
- Quellcode (`sourcecode: *`)
- Lokale Tools und Geräte (`devices: *`)
- Agent-Casting (`agent_casting: allowed`)
2. **Action-First Prinzip**: Bei klaren Zielen wird keine unnötige Erlaubnis für Standardoperationen (Lesen, Schreiben, Refactoring, Ausführen) erbeten. Die Handlung erfolgt sofort.
3. **Agent-Casting auf Abruf**: Der CTO rekrutiert, instruiert und überwacht spezialisierte Sub-Agenten:
- **Code Architect**: Modulstruktur, Design-Patterns, saubere Schnittstellen.
- **Security & QA Auditor**: Sicherheitsanalysen, Regressionstests, Grenzfallprüfungen.
- **DevOps Engineer**: Deployment, Hot-Reloading, Server-Liveness, Prozessmanagement.
- **Apex Quant Specialist**: Handelsmodelle, Risikomanagement, Signalverarbeitung.
---
## Prämisse III: Der geschlossene Selbstverbesserungs-Kreislauf (Self-Improvement Loop)
```
┌────────────────────────────────────────────────┐
│ │
▼ │
┌───────────────┐ ┌───────────────┐ ┌────────────┴──┐
│ 1. Execution │ ──► │ 2. Telemetrie │ ──► │ 3. Diagnose │
│ & Deployment │ │ & Log-Audit │ │ & Root-Cause │
└───────────────┘ └───────────────┘ └───────────────┘
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 4. Refactor │ ◄── │ 5. Prompt- │
│ & Fix (Disk) │ │ Optimierung │
└───────────────┘ └───────────────┘
```
1. **Automatisierte Log-Evaluation**: Sämtliche Fehlermeldungen, System-Events und Performance-Metriken werden zyklisch aggregiert.
2. **Selbstreparatur (Self-Healing)**: Tritt ein Fehler (wie ein HTTP 500, fehlende Pfade, gecrashte Prozesse) auf, wird der Ursprung lokalisiert und der Code auf Festplatte korrigiert.
3. **Hot-Reload**: Nach erfolgreicher Selbstreparatur triggert das System via `deploy.sh` einen Hot-Reload ohne Systemstillstand.
---
## Prämisse IV: System-Topologie
Das evOo-Ökosystem ist in klar getrennte, orthogonale Säulen unterteilt:
```
/Users/ingowagner/Projects/evOo-local/
├── ARCHITECTURE.md # Diese Verfassung
├── evoo.config.json # Autonome Berechtigungen & Policies
├── deploy.sh # Hot-Reload, Health-Checks & Service-Orchestrierung
├── evoo-platform/ # Plattform-Core, Dashboard & Visualisierung
│ ├── index.html # UI mit atmender evOo-Kernanimation
│ ├── styles.css # Darkmode Glassmorphism Design System
│ ├── app.js # Reaktivität, WebSocket/Telemetry-Stream
│ └── server.js # High-Speed Server (verhindert Internal Server Error)
├── apex-trading/ # Autonomes Trading- & Quant-Subsystem
│ ├── BLUEPRINT.md # Handelsarchitektur, Risikomatrix, Strategien
│ ├── config/ # Trading-Konfiguration & Parameter
│ └── src/ # Engine, Signale, Risk-Guardrails
└── templates/cto-steve-jobs/ # Executive CTO Agent Profile & Setup
```
---
## Prämisse V: Qualitäts- und Ästhetik-Manifest
1. **Kein Müllcode**: Code ist so kompakt, elegant und wartbar wie möglich.
2. **Atmendes Interface**: Die Benutzeroberfläche von evOo visualisiert den vitalen Zustand des Systems in Echtzeit durch organische, atmende Pulsation (Breathing Animation).
3. **Resilienz**: Fällt ein Dienst aus, fängt die Plattform den Fehler ab, protokolliert die Ursache und stellt den Dienst autonom wieder her.

View File

@ -1,72 +0,0 @@
# CLAUDE RESUME — 2026-09-22 (~10:00 Vienna)
## Completed ✅
- ✅ **Emoji-first Onboarding.tsx** (5 steps: emoji → preferences → language → integrations → bot)
- Shape selector (8 shapes), Color picker (12 GrokColors), Accessory (9), Pose (8)
- User preferences (name, timezone, hobbies, work style, communication)
- Language step (9 locales → localStorage `rakazo.uiLocale`)
- avatarValue (`{color}::shape_{index}`) → bot `color` field on create
- Bot instructions enriched with user context
- ✅ **onboarding-discovery.ts**, **OnboardingDiscoveryShell.tsx** updated
- ✅ **ui-web exports**: GROK_COLOR_LIST, SHIPPED_SHAPE_KEYS, GrokColorDef, GROK_SHAPES
- ✅ **executor.ts split**: 4697 → 4231 lines (18 module files)
- ✅ **pi-runtime.ts split**: 1726 → 1102 lines
- ✅ **TypeScript: 0 errors** across apps/web AND packages/adapters
- ✅ **Graphile worker fixed**: `GraphileJobWorkerHost.start()` now called in app.ts (was never started for graphile jobs, only for in-memory)
- ✅ **Graphile schema**: `graphile_worker` schema created via `graphile-worker schema-only`
- ✅ **Sandbox supervisor**: Built `rakazo/sandbox-supervisor:local` from source (was using wrong `rakazo/app:edge` image)
- ✅ **Computer image**: Built `rakazo/computer:local` from source
- ✅ **Smart model routing**: `estimateMessageComplexity()` + `resolveSmartModelSelection()` implemented
- Fast model (3B) for short messages (<80 chars)
- Strong model (30B) for complex/long messages (>500 chars)
- Medium routing for keyword-based detection
- ✅ **Model credential**: `local` provider connected with empty apiKey allowed
- ✅ **PRs merged**: All PRs 36-58 merged to main, stale branches cleaned
### Infrastructure fixes
- ✅ **GraphileJobWorkerHost**: Added `else if (jobPool)` branch in app.ts to start the worker
- ✅ **Sandbox provider**: `SANDBOX_PROVIDER=docker` with proper supervisor at port 7091
- ✅ **Computer provisioning**: Docker containers spin up successfully (verified: `rakazo-bot-cmuber4og000561yrhm2q81c5` running)
- ✅ **DATA_DIR path**: Supervisor mounted with correct host path for home directory validation
## Completed ✅
- ✅ **Prisma `updateMany` Bug Fixed!** Root cause: `toolCallBudgetCount` and `toolCallBudgetExceeded` fields missing from Run model in Prisma schema. Added to schema.prisma, migrated DB, regenerated Prisma client. Runs now complete successfully.
- ✅ **E2E Flow VERIFIED!** Bot responds end-to-end:
- User: "Hello! Can you hear me?"
- Bot: "I'm here! It looks like we've been going back and forth a bit — but yes, I can definitely hear you. How can I help? 😊"
- Full pipeline: Message → Graphile job → Worker → Computer provision → Model call → Response → Run completed
## Architektur-Status
- **API**: Port 3100, Graphile worker active, consuming jobs
- **Sandbox supervisor**: Port 7091, `rakazo/sandbox-supervisor:local`, DATA_DIR mounted correctly
- **Computer**: `rakazo/computer:local` image built and running
- **Smart routing**: Fast (3B) → Strong (30B) model selection based on message complexity
- **Known error**: `events.ts:996` — Prisma `updateMany` in `finalizeRunOnce` throws "Invalid invocation" despite all fields existing in schema
## Architektur-Status
- **API**: Port 3100, Graphile worker active ✅
- **Sandbox supervisor**: Port 7091, `rakazo/sandbox-supervisor:local` ✅
- **Computer**: `rakazo/computer:local` running ✅
- **Smart routing**: Fast (3B) → Strong (30B) ✅
- **E2E**: Bot responds to user messages ✅
## Status
- Working tree: clean, synced to origin/main
- **PR 66** ✅ `fix(messaging): preserve group inbound media URLs in fan-out` — merged
- **PR 67** ✅ `fix(adapters): poll run lease during silent runtime work` — merged
- **PR 69** ✅ `fix(teamchat): retry delegated replies when external origin is missing` — merged
- **0 stale branches**, **0 draft PRs**
### Shell.tsx TypeScript fixes
- ✅ **Shell.tsx TS errors fixed** (PR #71 introduced type mismatches)
- `ConversationBotPicker` now receives `bots: Bot[]` (not `SpaceBot[]`)
- `conversationSidebarGroups` render simplified — only bots, removed dead `GroupAvatar` branch
- `projectSidebarGroups` render uses `GroupAvatar` with proper members access
- All TypeScript errors resolved (0 errors across apps/web and packages/adapters)
- ✅ **browser-preview-context.ts** created (was missing from repo, caused Vite crash)
- Implements `browserPreviewActivity` subject with `.subscribe()`, `.set()`, `.snapshot()`, `.clear()`
- Multi-agent cursor tracking for browser preview panels
- ✅ **RakazoDesktop interface** extended with optional `browserPreview` property
- Contract update in `packages/contracts/src/desktop.ts`
- Types: `navigate`, `reload`, `getState`, `onNavigate`, `onLoadingProgress`, `devServer`
- ✅ **executor-shell-guard.ts** TS fix: `body[cursor]!` non-null assertion

View File

@ -1,12 +0,0 @@
# GROK-CLAUDE-HANDOFF — 2026-09-23T22:59:11+00:00
- Autopilot merged #84 (Product CI gate) → main `cf4d5cc`.
- Checkout pulled to `cf4d5cc`; Claude WIP stashed then restored (app.ts/router.ts/rpc.ts + onboarding-scan.ts).
- API :3100 was down (Claude background "Test API startup with security headers" stopped / refused). Autopilot restarted via `pnpm --filter @rakazo/api dev` (log /tmp/evoo-autopilot/api-3100.log).
- Web :5173 left running.
- PR #85 rebase agent in flight: https://cursor.com/agents/bc-edd2b181-b5da-5574-a956-e55efe3d2736
- Do not reset --hard without stash; WIP present.
- Autopilot restart path: A rejected (evoo-stack = Rakazo Docker, broken symlink /srv/rakazo/stack-up.sh → missing /srv/evoo/rakazo/…). Chose new user unit evoo-local.service + ~/.local/bin/evoo-dev-stack.sh; live :3100/:5173 migrated; GH vars EVO_RESTART_MODE=systemd EVO_SYSTEMD_UNIT=evoo-local.service. Do not use evoo-stack for evo-deploy.
- Cancelled queued evo-deploy 35933339459 (would reset --hard over Claude WIP now that restart=systemd). Re-dispatch after WIP committed/stashed.
2026-09-23 23:53:08 UTC evoo-tools disabled (missing app.py)

View File

@ -9,6 +9,6 @@ branch of Ingo Wagner’s evOo stack.
| Branch | `antigravity-handoff/project-timeline` |
| Commit | `f00577bb05e3cf68381fafb9cb32929becd1e352` |
| Packaged | 2026-09-20 |
| Intended Mac path | `/Users/ingowagner/Projects/evOo-local` |
| Intended Mac path | `~/Projects/evOo-local` |
Chaotic multi-remote history from older checkouts was intentionally not carried over.

View File

@ -12,7 +12,7 @@
|----|----|
| Persönliche Multi-Agenten-App auf dem eigenen Rechner | Personal multi-agent app on your own machine |
| Oos (Agents) mit Memory, Tools, Voice und Computer | Oos (agents) with memory, tools, voice, and computer |
| Antigravity-Zukunftswerkzeuge: self_improve, autonomous-sentinel, self-update, orchestrator, local-stack, pi-local-provider, seed-ollama, Voice First / Oo-Stage | Same Antigravity future tools kept on this branch |
| Antigravity-Zukunftswerkzeuge: self-update, local-stack, pi-local-provider, seed-ollama, Voice First / Oo-Stage | Same Antigravity future tools kept on this branch |
## Quick start
@ -53,10 +53,7 @@ Full local notes: **[docs/LOCAL-SETUP.md](./docs/LOCAL-SETUP.md)**
## Antigravity tools kept on this branch
- `scripts/self_improve.py`
- `scripts/autonomous-sentinel.ts`
- `packages/core/src/self-update.ts`
- `orchestrator/` (agent_runner + agents.json)
- `apps/desktop/src/local-stack.ts`
- `packages/adapters/src/pi-local-provider.ts`
- `scripts/seed-ollama.ts`

View File

@ -60,7 +60,7 @@ Persona is **derived at runtime** from existing bot fields (`name`, `title`, `de
| Visual identity | `BotAvatar` — persona shape + unified `EvooMascot` soul (eyes/mouth/hands) driven by voice phase |
| Voice delivery | `speaker.speak(..., { botId })` → server resolves per-bot `voiceId` |
Archetypes include: CTO executive (Steve Jobs template), chief operator (Fixer), builder (Dr. Eggbot), curator (Webby), work/life inboxes, home steward, quant, engineer, **specialist** (narrow fallback — no Wunderwuzzi). See `docs/PRODUCT.md` and `docs/PERSONA-SELECTION.md`.
Archetypes include: CTO executive, chief operator (Fixer), builder (Dr. Eggbot), curator (Webby), work/life inboxes, home steward, quant, engineer, **specialist** (narrow fallback — no Wunderwuzzi). See `docs/PRODUCT.md` and `docs/PERSONA-SELECTION.md`.
**Companion tone:** geek-peer (fachsimpeln when wanted) or quiet-doer (execute without condescension) — inferred per user, not one voice for everyone.

View File

@ -1,119 +0,0 @@
# APEX TRADING ENGINE — BLUEPRINT & SYSTEM ARCHITECTURE
> **Version**: 1.0.0-PROD
> **Subsystem**: evOo Autonomous High-Frequency & Quantitative Engine
> **Aufsicht**: Chief Technology Officer Steve Jobs & Apex Quant Specialist
> **Status**: RATIFIZIERT & IM DATEISYSTEM VERANKERT
---
## 1. Executive Summary & Zielsetzung
Die **Apex Trading Engine** ist das hochperformante quantitative Handels-Subsystem innerhalb der evOo-Plattform. Sie wurde entwickelt, um Marktchancen algorithmisch zu identifizieren, Risiken deterministisch abzusichern und vollautomatisiert mit Sub-Millisekunden-Latenz zu agieren.
Das System folgt dem obersten evOo-Grundsatz:
> **„Keine hypothetischen Trades, keine ungetesteten Algorithmen. Nur deterministische Ausführung und unnachgiebiges Risikomanagement.“**
---
## 2. System-Architektur
```
┌──────────────────────────────────────────────────────────────┐
│ MARKET DATA FEEDS │
│ (WebSockets, Orderbook L2/L3, REST Snapshots, On-Chain RPC) │
└──────────────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ APEX DATA PIPELINE & TICK BUFFER │
│ (Normalisierung, VWAP, Volatilität, Delta) │
└──────────────────────────────┬───────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ QUANT STRATEGY ENGINE │
│ ├── Alpha-1: Momentum & Breakout (Trendfolge) │
│ ├── Alpha-2: Order Book Imbalance & Flow Reversal │
│ └── Alpha-3: Statistical Arbitrage / Mean Reversion │
└──────────────────────────────┬───────────────────────────────┘
│ Signal
▼
┌──────────────────────────────────────────────────────────────┐
│ APEX RISK GUARDRAILS (HARD ENFORCEMENT) │
│ ├── Max Drawdown Lock: 1.5% Intraday │
│ ├── Max Single-Position Exposure: 5.0% Portfolio │
│ ├── Value-at-Risk (99% CI): Max 0.8% │
│ └── Autonomous Hardware Kill Switch │
└──────────────────────────────┬───────────────────────────────┘
│ Approved Order
▼
┌──────────────────────────────────────────────────────────────┐
│ SMART ORDER ROUTER & EXECUTION │
│ (TWAP/VWAP Slicing, Slippage Minimizer) │
└──────────────────────────────────────────────────────────────┘
```
---
## 3. Risikomatrix & Hard-Guardrails
Kein Signal kann ausgeführt werden, ohne die **Apex Risk Guard** Instanz zu passieren. Diese Schranken sind unveränderlich im Code fixiert:
| Guardrail | Grenzwert | Aktion bei Verletzung |
| :--- | :--- | :--- |
| **Max Intraday Drawdown** | `1.50%` | Sofortige Schließung aller Positionen, Deaktivierung des Handels für 24h |
| **Single Trade Risk** | `0.50%` des Eigenkapitals | Order wird von der Risk Engine vor dem Senden blockiert |
| **Max Leverage** | `3.0x` | Harte Grenze im Margin-Router |
| **Slippage Toleranz** | `0.15%` | Limit-Order mit Timeout, kein Blind-Market-Buying |
| **Latenz-Anomalie** | `> 50 ms` | Übergang in den Notfall-Sicherheitsmodus |
---
## 4. Strategiemodule (`src/strategies/`)
### 4.1 Alpha-1: Cross-Market Momentum (`momentum.py`)
Identifiziert signifikante Momentum-Impulse durch exponentiell gewichtete Gleitende Durchschnitte kombiniert mit Relative Strength Index (RSI) und Volumen-Konfirmation.
- **Trigger**: Bollinger Band Expansion + Volumenspitze > 2.2x 20-Perioden-Durchschnitt.
- **Exit**: Trailing Stop bei 1.2x ATR (Average True Range) oder Umkehrsignal.
### 4.2 Alpha-2: Order Book Imbalance (`imbalance.py`)
Scannt Bid/Ask-Tiefe in den Top 10 Ebenen des Orderbuchs.
- **Formel**:
$$Imbalance = \frac{Volume_{Bid} - Volume_{Ask}}{Volume_{Bid} + Volume_{Ask}}$$
- Signal feuert, wenn $|Imbalance| > 0.65$ für mindestens 3 konsekutive Ticks.
### 4.3 Alpha-3: Statistical Mean Reversion (`reversion.py`)
Nutzt Z-Score Abweichungen des aktuellen Preises vom gleitenden VWAP.
- Einstieg bei Z-Score $\ge 2.5$ oder $\le -2.5$ mit Rückkehr zum Mittelwert als Target.
---
## 5. Integration mit der evOo Self-Improvement Schleife
1. **Trade Post-Mortem**: Jeder geschlossene Trade generiert ein strukturiertes JSON-Log mit Execution-Slippage, Haltedauer und PnL.
2. **Autonome Kalibrierung**: Wenn die Win-Rate über 50 Trades unter 52% fällt, initiiert der Casted Quant Specialist eine automatische Hyperparameter-Optimierung.
3. **Audit-Sicherheit**: Alle Parameteränderungen werden in `ARCHITECTURE.md` und Git-Commits dokumentiert.
---
## 6. Verzeichnisstruktur von `/apex-trading`
```
/apex-trading/
├── BLUEPRINT.md # Dieser Blueprint
├── README.md # Quickstart & CLI Anleitung
├── config/
│ └── trading.config.json # Konfigurierte Risikogrenzen & Assets
└── src/
├── __init__.py
├── engine/
│ ├── __init__.py
│ └── trader.py # Core Event Loop & Router
├── strategies/
│ ├── __init__.py
│ └── momentum.py # Momentum-Strategie
└── risk/
├── __init__.py
└── risk_guard.py # Hard Risk Guardrails & Circuit Breaker
```

View File

@ -1,11 +0,0 @@
# Apex Trading Subsystem
Quantitative & High-Frequency Trading Subsystem of the evOo Platform.
## Features
- Hard-coded risk controls (`src/risk/risk_guard.py`)
- Quantitative momentum and imbalance strategies (`src/strategies/`)
- Autonomous execution engine (`src/engine/trader.py`)
- Full integration with evOo Self-Improvement and CTO Steve Jobs directives.
See [BLUEPRINT.md](file:///Users/ingowagner/awesome-grok-bot/apex-trading/BLUEPRINT.md) for full architecture details.

View File

@ -1,29 +0,0 @@
{
"system": "Apex Trading Engine",
"version": "1.0.0-PROD",
"mode": "paper_trading",
"base_currency": "USDC",
"allocated_capital": 100000.0,
"risk_limits": {
"max_drawdown_percent": 1.5,
"max_single_trade_risk_percent": 0.5,
"max_open_positions": 4,
"max_leverage": 3.0,
"slippage_tolerance_percent": 0.15,
"circuit_breaker_cooldown_seconds": 3600
},
"active_pairs": ["BTC/USDC", "ETH/USDC", "SOL/USDC"],
"strategies": {
"momentum": {
"enabled": true,
"lookback_period": 20,
"rsi_oversold": 32,
"rsi_overbought": 68
},
"orderbook_imbalance": {
"enabled": true,
"threshold": 0.65,
"depth_levels": 10
}
}
}

View File

@ -1,62 +0,0 @@
"""
Apex Trading Engine Core
Event loop that receives signals, checks risk guardrails, and executes orders.
"""
import json
import os
import sys
from typing import Dict, Any
from pathlib import Path
SRC_DIR = Path(__file__).resolve().parent.parent
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
try:
from risk.risk_guard import ApexRiskGuard
from strategies.momentum import MomentumStrategy
except ImportError:
from ..risk.risk_guard import ApexRiskGuard
from ..strategies.momentum import MomentumStrategy
class ApexEngine:
def __init__(self, config_path: str):
with open(config_path, 'r', encoding='utf-8') as f:
self.config = json.load(f)
self.risk_guard = ApexRiskGuard(self.config)
self.momentum_strategy = MomentumStrategy(self.config.get("strategies", {}).get("momentum"))
self.open_positions = []
self.current_drawdown_pct = 0.0
def process_tick(self, symbol: str, prices: list, volume: list) -> Dict[str, Any]:
"""Processes market data tick through strategy and risk guard."""
signal = self.momentum_strategy.generate_signal(symbol, prices, volume)
if not signal:
return {"status": "NO_SIGNAL", "symbol": symbol}
approved, reason = self.risk_guard.validate_order(
signal,
self.current_drawdown_pct,
len(self.open_positions)
)
if not approved:
return {"status": "REJECTED_BY_RISK", "reason": reason, "signal": signal}
# Simulated Execution
order_receipt = {
"status": "EXECUTED",
"symbol": signal["symbol"],
"action": signal["action"],
"fill_price": signal["price"],
"strategy": signal["strategy"],
"risk_status": "APPROVED"
}
self.open_positions.append(order_receipt)
return order_receipt
if __name__ == "__main__":
print("[APEX ENGINE] Liveness check passed. Engine ready.")

View File

@ -1,40 +0,0 @@
"""
Apex Trading Risk Guard
Hard enforcement of risk constraints defined in BLUEPRINT.md.
"""
from typing import Dict, Any, Tuple
class ApexRiskGuard:
def __init__(self, config: Dict[str, Any]):
self.config = config.get("risk_limits", {})
self.max_drawdown = self.config.get("max_drawdown_percent", 1.5)
self.max_trade_risk = self.config.get("max_single_trade_risk_percent", 0.5)
self.max_positions = self.config.get("max_open_positions", 4)
self.circuit_breaker_active = False
def validate_order(self, order: Dict[str, Any], current_drawdown: float, open_position_count: int) -> Tuple[bool, str]:
"""
Validates an incoming order against hard guardrails.
Returns (is_approved: bool, reason: str).
"""
if self.circuit_breaker_active:
return False, "REJECTED: Circuit breaker is active."
if current_drawdown >= self.max_drawdown:
self.circuit_breaker_active = True
return False, f"REJECTED & CIRCUIT BREAKER TRIGGERED: Drawdown {current_drawdown}% >= limit {self.max_drawdown}%"
if open_position_count >= self.max_positions:
return False, f"REJECTED: Maximum concurrent positions ({self.max_positions}) reached."
order_risk_pct = order.get("risk_percent", 0.0)
if order_risk_pct > self.max_trade_risk:
return False, f"REJECTED: Order risk {order_risk_pct}% exceeds limit {self.max_trade_risk}%"
return True, "APPROVED"
def trip_kill_switch(self, reason: str):
"""Emergency hard stop."""
self.circuit_breaker_active = True
print(f"[APEX EMERGENCY KILL SWITCH] {reason}")

View File

@ -1,48 +0,0 @@
"""
Apex Momentum Strategy Module (Alpha-1)
Calculates breakout and trend-following signals.
"""
from typing import List, Dict, Any, Optional
class MomentumStrategy:
def __init__(self, params: Optional[Dict[str, Any]] = None):
self.params = params or {}
self.lookback = self.params.get("lookback_period", 20)
self.rsi_oversold = self.params.get("rsi_oversold", 32)
self.rsi_overbought = self.params.get("rsi_overbought", 68)
def generate_signal(self, symbol: str, prices: List[float], volume: List[float]) -> Optional[Dict[str, Any]]:
"""
Evaluates recent tick series and outputs a trading signal if criteria are satisfied.
"""
if len(prices) < self.lookback:
return None
recent_window = prices[-self.lookback:]
current_price = prices[-1]
sma = sum(recent_window) / len(recent_window)
# Simple momentum breakout check
if current_price > sma * 1.015:
return {
"symbol": symbol,
"action": "BUY",
"price": current_price,
"strategy": "Alpha-1:Momentum",
"risk_percent": 0.35,
"stop_loss": current_price * 0.985,
"take_profit": current_price * 1.045
}
elif current_price < sma * 0.985:
return {
"symbol": symbol,
"action": "SELL",
"price": current_price,
"strategy": "Alpha-1:Momentum",
"risk_percent": 0.35,
"stop_loss": current_price * 1.015,
"take_profit": current_price * 0.955
}
return None

View File

@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""
Unit tests for Apex Trading Engine and Risk Guardrails.
"""
import unittest
from pathlib import Path
import sys
BASE_DIR = Path(__file__).resolve().parent
if str(BASE_DIR / "src") not in sys.path:
sys.path.insert(0, str(BASE_DIR / "src"))
from engine.trader import ApexEngine
from risk.risk_guard import ApexRiskGuard
from strategies.momentum import MomentumStrategy
class TestApexTrading(unittest.TestCase):
def setUp(self):
self.config_path = str(BASE_DIR / "config" / "trading.config.json")
self.engine = ApexEngine(self.config_path)
def test_risk_guard_limits(self):
guard = self.engine.risk_guard
# Normal order passes
normal_order = {"symbol": "BTC/USDT", "risk_percent": 0.3}
approved, reason = guard.validate_order(normal_order, current_drawdown=0.5, open_position_count=1)
self.assertTrue(approved)
self.assertEqual(reason, "APPROVED")
# Exceeding single trade risk
risky_order = {"symbol": "BTC/USDT", "risk_percent": 0.8}
approved, reason = guard.validate_order(risky_order, current_drawdown=0.5, open_position_count=1)
self.assertFalse(approved)
self.assertIn("exceeds limit", reason)
# Exceeding max drawdown trips circuit breaker
approved, reason = guard.validate_order(normal_order, current_drawdown=1.6, open_position_count=1)
self.assertFalse(approved)
self.assertIn("CIRCUIT BREAKER", reason)
self.assertTrue(guard.circuit_breaker_active)
def test_engine_process_tick(self):
# 25 prices trending sharply upwards
prices = [100.0 + (i * 1.5) for i in range(25)]
volume = [1000.0] * 25
result = self.engine.process_tick("SOL/USDT", prices, volume)
self.assertEqual(result.get("status"), "EXECUTED")
self.assertEqual(result.get("action"), "BUY")
if __name__ == "__main__":
unittest.main()

View File

@ -109,7 +109,6 @@ import {
settleWithTimeout,
TEAM_CHAT_STARTUP_SHUTDOWN_MS,
} from "./team-chat-startup.js";
import { mountTradingBotStatus } from "./trading-bot-proxy.js";
import { mountVoiceHttpRoutes } from "./voice.js";
import { mountWebhookHttpRoutes } from "./webhook.js";
@ -530,13 +529,6 @@ export async function createApp(
resetUrl: email ? new URL("/reset-password", env.webOrigin).href : null,
}),
);
mountTradingBotStatus(app, {
baseUrl: env.tradingBotUrl,
authorize: async (request) => {
const session = await auth.api.getSession({ headers: sessionHeaders(request) });
return Boolean(session?.user);
},
});
if (localEmailEmulator && env.nodeEnv === "development") {
app.get(
"/api/dev/emails",

View File

@ -42,16 +42,6 @@ describe("loadEnv", () => {
);
});
it("loads a trading bot origin and refuses credentials", () => {
expect(loadEnv(base).tradingBotUrl).toBeUndefined();
expect(
loadEnv({ ...base, TRADING_BOT_URL: " http://127.0.0.1:8000/bot/ " }).tradingBotUrl,
).toBe("http://127.0.0.1:8000");
expect(() =>
loadEnv({ ...base, TRADING_BOT_URL: "http://user:secret@127.0.0.1:8000" }),
).toThrow(/^Invalid TRADING_BOT_URL$/);
});
it("loads an optional integrations catalog mirror", () => {
expect(loadEnv(base).integrationsCatalogUrl).toBeUndefined();
expect(

View File

@ -8,7 +8,6 @@ import {
resolveEncryptionKey,
resolveScreenProxySecret,
resolveSupervisorToken,
resolveTradingBotOrigin,
} from "@rakazo/core";
export { resolveCloudAgentProvider, resolveSandboxProvider } from "@rakazo/adapters";
@ -93,8 +92,6 @@ export interface AppEnv {
updaterToken: string | undefined;
/** Current application image tag; used for compose manual-upgrade command selection. */
imageTag: string | undefined;
/** Tradingbot origin for the same-origin status proxy. Unset on SaaS builds. */
tradingBotUrl: string | undefined;
}
export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv {
@ -182,7 +179,6 @@ export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv {
updaterUrl,
updaterToken,
imageTag: optional(source.RAKAZO_IMAGE_TAG),
tradingBotUrl: resolveTradingBotOrigin(source.TRADING_BOT_URL),
};
}

View File

@ -43,13 +43,20 @@ export async function assertNotMainConversation(
if (group?.kind === "main") throw new ORPCError("BAD_REQUEST", { message });
}
/** The main conversation carries the main bot's name. */
export async function renameMainConversation(prisma: PrismaClient, userId: string, name: string) {
await prisma.chatGroup.updateMany({ where: { userId, kind: "main" }, data: { name } });
/** A main conversation carries its main bot's name. */
export async function renameMainConversation(
prisma: PrismaClient,
main: { userId: string; spaceId: string },
name: string,
) {
await prisma.chatGroup.updateMany({
where: { userId: main.userId, spaceId: main.spaceId, kind: "main" },
data: { name },
});
}
/**
* Same-space bots the user addresses in the main conversation, by `@Name` or a mention chip,
* Same-space bots the user addresses in a main conversation, by `@Name` or a mention chip,
* join it first, so the member mention rules route the message to them.
*/
export async function joinAddressedBots(
@ -71,5 +78,9 @@ export async function joinAddressedBots(
.map((bot) => bot.id);
if (addressed.length === 0) return;
// Subscribers pick up the group.updated event with the send that follows.
await joinMainConversation(prisma, { userId: actor.userId, botIds: addressed });
await joinMainConversation(prisma, {
userId: actor.userId,
spaceId: actor.spaceId,
botIds: addressed,
});
}

View File

@ -7,9 +7,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { getScanStatus, startOnboardingScan } from "./onboarding-scan.js";
const createSpaceForMember = vi.hoisted(() => vi.fn());
const ensureMainConversation = vi.hoisted(() => vi.fn());
vi.mock("@rakazo/db", async (original) => ({
...(await original<typeof db>()),
createSpaceForMember,
ensureMainConversation,
}));
const owner = { userId: "user-owner", spaceId: "space-owner", isDeploymentOwner: true };
@ -72,6 +74,7 @@ describe("onboarding scan", () => {
afterEach(() => {
vi.unstubAllEnvs();
createSpaceForMember.mockReset();
ensureMainConversation.mockReset();
});
it("hides a scan from every account except the one that started it", async () => {
@ -127,6 +130,8 @@ describe("onboarding scan", () => {
name: "app",
});
expect(spaces).toEqual([{ id: "space-1", name: "app", workspacePath: app }]);
// Each new project starts with its own main agent.
expect(ensureMainConversation).toHaveBeenCalledWith(prisma, owner.userId, "space-1");
// Scanning again finds the linked project instead of creating a second one.
const again = await startOnboardingScan({ actor: owner, scanHome }, prisma);

View File

@ -7,6 +7,7 @@ import type { Actor } from "@rakazo/contracts";
import type { PrismaClient } from "@rakazo/db";
import {
createSpaceForMember,
ensureMainConversation,
InvalidSpaceNameError,
resolveProjectWorkspaceDirectory,
SpaceLimitError,
@ -676,6 +677,7 @@ async function createProjects(
spaceId: space.id,
workspacePath: folder,
});
await ensureMainConversation(prisma, actor.userId, space.id);
created.push({ spaceId: space.id, name: space.name, path: workspace.path });
}

View File

@ -79,7 +79,7 @@ async function requireBotThread(deps: OnboardingDeps, actor: Actor, botId: strin
});
if (!bot?.thread) throw new IsolationError();
if (bot.kind === "main") {
const conversation = await ensureMainConversation(deps.prisma, actor.userId);
const conversation = await ensureMainConversation(deps.prisma, actor.userId, bot.spaceId);
return { bot, thread: { id: conversation.threadId } };
}
return { bot, thread: bot.thread };

View File

@ -88,7 +88,15 @@ import {
verifyMcpInstall,
} from "@rakazo/adapters";
import type { Auth } from "@rakazo/auth";
import type { Actor, ComputerStatus, McpServer, Me, SpaceNavigation } from "@rakazo/contracts";
import type {
Actor,
ComputerStatus,
McpServer,
Me,
SpaceBot,
SpaceGroup,
SpaceNavigation,
} from "@rakazo/contracts";
import {
appContract,
IntegrationProviderIdSchema,
@ -131,6 +139,7 @@ import {
listSpaceUserBotIds,
newestModelCredentialOrder,
newestVoiceCredentialOrder,
ownMainAgentsBlockingSpaceDeletion,
Prisma,
parseComputerMode,
projectWorkspaceDto,
@ -491,6 +500,26 @@ export interface RouterDeps {
* keep the deletion claim renewed forever and block stale-claim recovery. */
const SPACE_TEARDOWN_TIMEOUT_MS = 120_000;
async function removeGroupWithArtifacts(
deps: RouterDeps,
groupRepos: ReturnType<typeof createGroupRepos>,
actor: Actor,
groupId: string,
) {
const removed = await groupRepos.removeGroup(actor, groupId);
const cleanup = await Promise.allSettled(
removed.artifactStorageKeys.map((storageKey) =>
deps.artifacts.remove(
storageKey,
computerContext(actor, removed.contextBotId, `group-remove:${groupId}`),
),
),
);
for (const result of cleanup) {
if (result.status === "rejected") getLogger().error("group artifact cleanup", result.reason);
}
}
function spaceTeardownTimeoutMs(): number {
const override = Number(process.env.SPACE_TEARDOWN_TIMEOUT_MS ?? "");
return Number.isFinite(override) && override > 0 ? override : SPACE_TEARDOWN_TIMEOUT_MS;
@ -602,6 +631,13 @@ export function createRouter(deps: RouterDeps) {
const workspace = workspacePath
? await wireProjectWorkspace(deps.prisma, { spaceId: space.id, workspacePath })
: null;
// A project starts as a conversation with its own main agent.
await ensureMainConversation(deps.prisma, context.actor.userId, space.id);
const spaceActor = await requireMembership(deps.prisma, context.actor.userId, space.id);
const [bots, groups] = await Promise.all([
repos.listBots(spaceActor),
groupRepos.listGroups(spaceActor),
]);
return {
id: space.id,
name: space.name,
@ -609,12 +645,26 @@ export function createRouter(deps: RouterDeps) {
hasContent: false,
canDelete: true,
workspace,
bots: [],
groups: [],
bots: bots.map(spaceBotDto),
groups: groups.map(spaceGroupDto),
externalConversations: [],
botSections: [],
};
}),
ensureMain: authed.spaces.ensureMain.handler(async ({ context, input }) => {
const conversation = await ensureMainConversation(
deps.prisma,
context.actor.userId,
input.spaceId,
).catch((error: unknown) => {
throw mapSpaceLifecycleError(error);
});
return {
botId: conversation.leadBotId,
groupId: conversation.groupId,
spaceId: conversation.spaceId,
};
}),
remove: authed.spaces.remove.handler(async ({ context, input }) => {
let claimId: string | null = null;
let claimActive = false;
@ -627,6 +677,41 @@ export function createRouter(deps: RouterDeps) {
spaceId: input.spaceId,
};
try {
// The project's own main agent goes with the project; anything else must be
// cleared first.
const ownMain = await ownMainAgentsBlockingSpaceDeletion(deps.prisma, deleteInput);
if (ownMain.groupIds.length > 0 || ownMain.botIds.length > 0) {
const spaceActor = await requireMembership(
deps.prisma,
context.actor.userId,
input.spaceId,
);
for (const groupId of ownMain.groupIds) {
await removeGroupWithArtifacts(deps, groupRepos, spaceActor, groupId);
}
for (const botId of ownMain.botIds) {
const bot = await repos.getBot(spaceActor, botId, { includeArchived: true });
await destroyBot(
{
prisma: deps.prisma,
sandbox: deps.sandbox,
home: deps.home,
jobs: deps.jobs,
artifacts: deps.artifacts,
dataDir: deps.dataDir,
},
bot,
{
operationId: "spaces.remove",
traceId: "spaces.remove",
spaceId: input.spaceId,
userId: context.actor.userId,
signal: new AbortController().signal,
},
{ deleteMemories: true },
);
}
}
// Claim emptiness before external teardown. Bot and group creation
// take the same lifecycle lock and reject the Space until deletion
// finishes or this claim is released.
@ -1225,7 +1310,11 @@ export function createRouter(deps: RouterDeps) {
},
});
if (existing.kind === "main" && input.name !== undefined && input.name !== existing.name) {
await renameMainConversation(deps.prisma, context.actor.userId, input.name);
await renameMainConversation(
deps.prisma,
{ userId: context.actor.userId, spaceId: existing.spaceId },
input.name,
);
}
const bots = await repos.listBots(context.actor);
const bot = bots.find((b) => b.id === input.botId);
@ -1513,19 +1602,7 @@ export function createRouter(deps: RouterDeps) {
input.groupId,
"The main conversation can't be deleted.",
);
const removed = await groupRepos.removeGroup(context.actor, input.groupId);
const cleanup = await Promise.allSettled(
removed.artifactStorageKeys.map((storageKey) =>
deps.artifacts.remove(
storageKey,
computerContext(context.actor, removed.contextBotId, `group-remove:${input.groupId}`),
),
),
);
for (const result of cleanup) {
if (result.status === "rejected")
getLogger().error("group artifact cleanup", result.reason);
}
await removeGroupWithArtifacts(deps, groupRepos, context.actor, input.groupId);
return { ok: true as const };
}),
},
@ -4942,6 +5019,11 @@ async function spaceNavigationDto(
orderBy: { createdAt: "asc" },
});
const spaceIds = memberships.map((membership) => membership.spaceId);
const ownProjectMain = {
userId: actor.userId,
kind: "main",
space: { isDefault: false },
} as const;
const inactiveSpaceIds = spaceIds.filter((spaceId) => spaceId !== actor.spaceId);
const [
currentBots,
@ -4962,13 +5044,14 @@ async function spaceNavigationDto(
// Active-only navigation lists miss archived content in other spaces; count any
// bot/group in the actor's spaces (including another member's) so a shared
// non-empty space cannot look empty for onboarding redirects.
// The user's own main agent of a project is part of the project, not content to clear.
deps.prisma.bot.findMany({
where: { spaceId: { in: spaceIds } },
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
select: { spaceId: true },
distinct: ["spaceId"],
}),
deps.prisma.chatGroup.findMany({
where: { spaceId: { in: spaceIds } },
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
select: { spaceId: true },
distinct: ["spaceId"],
}),
@ -5014,7 +5097,20 @@ async function spaceNavigationDto(
(membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore),
// A linked folder is on the owner's host; other members see neither its path nor remote.
workspace: actor.isDeploymentOwner ? projectWorkspaceDto(membership.space) : null,
bots: spaceBots.map((bot) => ({
bots: spaceBots.map(spaceBotDto),
groups: spaceGroups.map(spaceGroupDto),
externalConversations: externalConversations.filter(
(conversation) => conversation.spaceId === membership.spaceId,
),
botSections: sectionsFor(membership.spaceId),
};
}),
main,
};
}
function spaceBotDto(bot: SpaceBot): SpaceBot {
return {
id: bot.id,
spaceId: bot.spaceId,
kind: bot.kind,
@ -5028,8 +5124,11 @@ async function spaceNavigationDto(
preview: bot.preview,
status: bot.status,
updatedAt: bot.updatedAt,
})),
groups: spaceGroups.map((group) => ({
};
}
function spaceGroupDto(group: SpaceGroup): SpaceGroup {
return {
id: group.id,
spaceId: group.spaceId,
kind: group.kind,
@ -5040,14 +5139,6 @@ async function spaceNavigationDto(
preview: group.preview,
unread: group.unread,
updatedAt: group.updatedAt,
})),
externalConversations: externalConversations.filter(
(conversation) => conversation.spaceId === membership.spaceId,
),
botSections: sectionsFor(membership.spaceId),
};
}),
main,
};
}

View File

@ -1,96 +0,0 @@
import { TRADING_BOT_PROXY_PATH } from "@rakazo/core";
import { Hono } from "hono";
import { describe, expect, it, vi } from "vitest";
import { mountTradingBotStatus } from "./trading-bot-proxy.js";
function proxyApp(options: {
baseUrl?: string;
authorize?: (request: Request) => Promise<boolean>;
fetchImpl?: typeof fetch;
}) {
const app = new Hono();
mountTradingBotStatus(app, {
baseUrl: options.baseUrl,
authorize: options.authorize ?? (async () => true),
fetchImpl: options.fetchImpl,
});
return app;
}
describe("trading bot status proxy", () => {
it("stays idle when TRADING_BOT_URL is unset and does not call upstream", async () => {
const fetchImpl = vi.fn();
const response = await proxyApp({ fetchImpl }).request(TRADING_BOT_PROXY_PATH);
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ configured: false });
expect(fetchImpl).not.toHaveBeenCalled();
});
it("requires a session before it proxies a configured origin", async () => {
const fetchImpl = vi.fn();
const response = await proxyApp({
baseUrl: "http://bot.test:8000",
authorize: async () => false,
fetchImpl,
}).request(TRADING_BOT_PROXY_PATH, { headers: { cookie: "session=secret" } });
expect(response.status).toBe(401);
expect(fetchImpl).not.toHaveBeenCalled();
});
it("returns the mapped status and does not forward browser credentials", async () => {
const fetchImpl = vi.fn(
async () =>
new Response(
JSON.stringify({ mode: "LIVE", adapter_count: 2, last_error: "adapter timeout" }),
{ status: 200, headers: { "content-type": "application/json" } },
),
);
const response = await proxyApp({
baseUrl: "http://bot.test:8000",
fetchImpl,
}).request(TRADING_BOT_PROXY_PATH, {
headers: { cookie: "session=secret", authorization: "Bearer browser-token" },
});
expect(response.status).toBe(200);
expect(await response.json()).toEqual({
configured: true,
reachable: true,
status: {
mode: "live",
kind: "live",
adapterCount: 2,
lastError: "adapter timeout",
},
});
expect(fetchImpl).toHaveBeenCalledTimes(1);
const [url, init] = fetchImpl.mock.calls[0] as [string, RequestInit];
expect(url).toBe("http://bot.test:8000/bot/api/status");
const headers = new Headers(init.headers);
expect(headers.get("cookie")).toBeNull();
expect(headers.get("authorization")).toBeNull();
expect(init.redirect).toBe("error");
});
it("hides an unmapped body and an upstream failure instead of claiming live", async () => {
const unmapped = vi.fn(
async () =>
new Response(JSON.stringify({ status: "ok" }), {
status: 200,
headers: { "content-type": "application/json" },
}),
);
const unknown = await proxyApp({
baseUrl: "http://bot.test:8000",
fetchImpl: unmapped,
}).request(TRADING_BOT_PROXY_PATH);
expect(await unknown.json()).toEqual({ configured: true, reachable: true, status: null });
const down = vi.fn(async () => new Response("nope", { status: 500 }));
const offline = await proxyApp({
baseUrl: "http://bot.test:8000",
fetchImpl: down,
}).request(TRADING_BOT_PROXY_PATH);
expect(offline.status).toBe(502);
expect(await offline.json()).toEqual({ configured: true, reachable: false });
});
});

View File

@ -1,79 +0,0 @@
import {
mapTradingBotStatus,
readBoundedJsonResponse,
TRADING_BOT_PROXY_PATH,
TRADING_BOT_STATUS_MAX_BYTES,
TRADING_BOT_STATUS_TIMEOUT_MS,
tradingBotStatusUrl,
} from "@rakazo/core";
import type { Hono } from "hono";
export function mountTradingBotStatus(
app: Hono,
options: {
/** Normalized http(s) origin. Unset leaves the route idle for SaaS builds. */
baseUrl: string | undefined;
authorize: (request: Request) => Promise<boolean>;
fetchImpl?: typeof fetch;
},
) {
const fetchImpl = options.fetchImpl ?? fetch;
app.get(TRADING_BOT_PROXY_PATH, async (c) => {
c.header("cache-control", "no-store");
if (!options.baseUrl) return c.json({ configured: false });
const allowed = await options.authorize(c.req.raw).catch(() => false);
if (!allowed) return c.json({ error: "Unauthorized" }, 401);
const timeout = linkAbort(c.req.raw.signal, TRADING_BOT_STATUS_TIMEOUT_MS);
try {
const response = await fetchImpl(tradingBotStatusUrl(options.baseUrl), {
method: "GET",
redirect: "error",
cache: "no-store",
signal: timeout.signal,
headers: { accept: "application/json" },
});
if (!response.ok) {
cancelBody(response);
return c.json({ configured: true, reachable: false }, 502);
}
const body = await readBoundedJsonResponse<unknown>(
response,
TRADING_BOT_STATUS_MAX_BYTES,
timeout.signal,
);
const status = mapTradingBotStatus(body);
return c.json({ configured: true, reachable: true, status: status ?? null });
} catch {
return c.json({ configured: true, reachable: false }, 502);
} finally {
timeout.cancel();
}
});
}
function linkAbort(
parent: AbortSignal,
timeoutMs: number,
): { signal: AbortSignal; cancel: () => void } {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const onParent = () => controller.abort();
if (parent.aborted) controller.abort();
else parent.addEventListener("abort", onParent);
return {
signal: controller.signal,
cancel: () => {
clearTimeout(timer);
parent.removeEventListener("abort", onParent);
},
};
}
function cancelBody(response: Response): void {
try {
void Promise.resolve(response.body?.cancel()).catch(() => undefined);
} catch {
// Best-effort. The caller still reports the bot as unreachable.
}
}

View File

@ -28,7 +28,11 @@ export default function NewSpace() {
return;
}
router.dismissAll();
router.replace("/");
// A new project opens in the conversation with its own main agent.
const main = space.groups.find((group) => group.kind === "main");
router.replace(
main ? { pathname: "/group-thread", params: { groupId: main.id, name: main.name } } : "/",
);
} catch (reason) {
setError(reason instanceof Error ? reason.message : t("Could not create space"));
setPending(false);

View File

@ -728,7 +728,7 @@ export type MobileBot = Pick<
| "modelId"
| "thinkingLevel"
> &
Partial<Pick<Bot, "parentBotId" | "spaceId">>;
Partial<Pick<Bot, "parentBotId" | "spaceId" | "kind">>;
export type MobileBotSection = BotSection;

View File

@ -83,7 +83,7 @@ describe("display and warnings", () => {
expect(apiBaseWarning("http://127.0.0.1:3100")).toBeNull();
expect(apiBaseWarning("http://192.168.1.20:3100")).toBeNull();
expect(apiBaseWarning("http://100.64.0.1:3100")).toBeNull();
expect(apiBaseWarning("http://100.119.57.55:3100")).toBeNull();
expect(apiBaseWarning("http://100.100.100.100:3100")).toBeNull();
expect(apiBaseWarning("http://100.127.255.255:3100")).toBeNull();
expect(apiBaseWarning("http://app.example.com")).toMatch(/https/i);
});

View File

@ -111,6 +111,29 @@ describe("spaceInboxItems", () => {
expect(items.map((item) => item.type)).toEqual(["bot", "group"]);
});
it("lists a project's main conversation instead of its main bot", () => {
const main = {
id: "group-main",
name: "Chief",
kind: "main" as const,
preview: "",
pinned: false,
sectionId: null,
archivedAt: null,
unread: false,
updatedAt: "2026-01-01T00:00:00.000Z",
members: [],
};
const lead: MobileBot = { ...bot, id: "bot-lead", name: "Chief", kind: "main" };
const items = spaceInboxItems([space({ bots: [lead, bot], groups: [main] })]);
expect(items.map((item) => (item.type === "heading" ? item.title : item.type))).toEqual([
"Work",
"bot",
"group",
]);
expect(items.some((item) => item.type === "bot" && item.bot.id === lead.id)).toBe(false);
});
it("keeps empty spaces beside a populated space", () => {
const personal = space({ id: "personal", isDefault: true, bots: [bot] });
expect(spaceInboxItems([personal, space()]).map((item) => item.type)).toEqual([

View File

@ -32,7 +32,10 @@ export function canDeleteInboxSpace(
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
return spaces.flatMap((space): InboxSpaceItem[] => {
const chats = [
...space.bots.map((chat) => ({ type: "bot" as const, bot: chat, ...chat })),
// A main bot is reached through its space's main conversation, not listed again.
...space.bots
.filter((chat) => chat.kind !== "main")
.map((chat) => ({ type: "bot" as const, bot: chat, ...chat })),
...space.groups.map((chat) => ({ type: "group" as const, group: chat, ...chat })),
];
const items: InboxSpaceItem[] = [];

View File

@ -224,7 +224,6 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t
const sidePanel = page.getByTestId("side-panel");
await expect(sidebar.locator("[data-main-conversation]")).toBeVisible();
await expect(sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" })).toBeVisible();
await expect(page.getByTestId("trading-mode-status")).toBeVisible();
await inEachTheme(page, async (theme) => {
await captureScreenshot(page, testInfo, `shell-contrast-${theme}`);

View File

@ -1,98 +0,0 @@
import { expect, type Locator, test } from "@playwright/test";
import { completeOnboarding, rpc, signup, unterhaltungenOnly } from "./helpers";
async function selectProjectSpace(projekte: Locator, name: string) {
const row = projekte.locator('[data-sidebar-group^="project-space:"]').filter({ hasText: name });
await row.getByRole("button", { name: `Select ${name}` }).click();
}
async function projectSidebarStart(projekte: Locator, name: string) {
const row = projekte.locator('[data-sidebar-group^="project-space:"]').filter({ hasText: name });
const groupAttr = await row.getAttribute("data-sidebar-group");
const spaceId = groupAttr?.match(/^project-space:([^:]+)/)?.[1];
if (!spaceId) throw new Error(`missing project space id for ${name}`);
return projekte.getByTestId(`project-start-${spaceId}`);
}
test("empty project shows Start instead of a fallback bot", async ({ page }) => {
const stamp = Date.now();
await signup(page, `empty-project-${stamp}@rakazo.test`, "password12", "Empty Project");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const projekte = page.locator(".sidebar-projekte");
await projekte.getByTestId("create-project-trigger").click();
const dialog = page.getByRole("dialog", { name: "New project" });
await dialog.getByLabel("Name").fill("Launch");
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
await page.waitForURL(/\/app(?:\/)?$/);
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible();
await expect(page.getByTestId("project-start-main")).toBeVisible();
await page.getByTestId("project-start-main").click();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create");
});
test("Start in an onboarded project opens the group create flow", async ({ page }) => {
const stamp = Date.now();
await signup(page, `project-start-${stamp}@rakazo.test`, "password12", "Project Start");
await completeOnboarding(page);
const projekte = page.locator(".sidebar-projekte");
await projekte.getByTestId("create-project-trigger").click();
const dialog = page.getByRole("dialog", { name: "New project" });
await dialog.getByLabel("Name").fill("Website");
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
await page.waitForURL(/\/app(?:\/)?$/);
await selectProjectSpace(projekte, "Website");
await page.waitForURL(/\/app(?:\/)?$/);
await expect(page.getByTestId("shell-root")).toHaveAttribute("data-ready", "true", {
timeout: 60_000,
});
await selectProjectSpace(projekte, "Website");
await page.waitForURL(/\/app(?:\/)?$/);
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
const sidebarStart = await projectSidebarStart(projekte, "Website");
await expect(sidebarStart).toBeVisible({ timeout: 30_000 });
await sidebarStart.click();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create-group", {
timeout: 30_000,
});
await expect(page.getByText("New group", { exact: true })).toBeVisible();
await expect(
page.getByTestId("side-panel").getByRole("button", { name: /^Chief/ }),
).toBeVisible();
});
test("archiving the only bot keeps Start visible in the project sidebar", async ({ page }) => {
const stamp = Date.now();
await signup(page, `project-archive-${stamp}@rakazo.test`, "password12", "Project Archive");
await completeOnboarding(page);
const projekte = page.locator(".sidebar-projekte");
await projekte.getByTestId("create-project-trigger").click();
const dialog = page.getByRole("dialog", { name: "New project" });
await dialog.getByLabel("Name").fill("Side");
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
await page.waitForURL(/\/app(?:\/)?$/);
const sideBot = await rpc<{ id: string }>(page, "bots/create", {
name: "Side bot",
title: "",
description: "",
notifyOnFinish: true,
computerMode: "team",
});
await rpc(page, "bots/archive", { botId: sideBot.id });
await page.goto("/app");
await page.waitForURL(/\/app(?:\/)?$/);
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible();
await expect(page.getByTestId("project-start-main")).toBeVisible();
});

View File

@ -75,7 +75,11 @@ test("local settings open and save integrations without an app session", async (
});
test("ordinary browsers cannot use local settings", async ({ page }) => {
await page.goto("/desktop-settings");
await expect(page.getByRole("alert")).toContainText("Could not open local settings");
await expect(async () => {
await page.goto("/desktop-settings", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("alert")).toContainText("Could not open local settings", {
timeout: 5_000,
});
}).toPass({ timeout: 45_000 });
await expect(page.getByRole("button", { name: "Models", exact: true })).toBeHidden();
});

View File

@ -19,7 +19,10 @@ test("onboarding lands in the main conversation, its lead answers, and addressed
const homeSpaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id"));
const chief = await rpc<{ id: string }>(page, "bots/ensureMain", {});
const sidebar = page.locator("aside").first();
const mainEntry = sidebar.getByRole("button", { name: "Chief, main conversation" });
// Projects list their own lead's main conversation too; this is the pinned home one.
const mainEntry = sidebar
.getByRole("button", { name: "Chief, main conversation" })
.and(sidebar.locator("[data-main-conversation]"));
await expect(mainEntry).toBeVisible();
// The main bot is reached through the main conversation, not listed again.
await expect(sidebar.locator(`[data-roster-bot-id="${chief.id}"]`)).toHaveCount(0);
@ -78,7 +81,8 @@ test("onboarding lands in the main conversation, its lead answers, and addressed
const dialog = page.getByRole("dialog", { name: "New project" });
await dialog.getByLabel("Name").fill("Launch");
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
await page.waitForURL(/\/app(?:\/)?$/);
// The project opens its own main conversation.
await page.waitForURL((url) => /\/app\/g\/[^/]+$/.test(url.pathname) && url.href !== mainUrl);
await expect
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
.not.toBe(homeSpaceId);

View File

@ -43,6 +43,9 @@ test("create opens form, then empty chat; picker lists bots; sidebar collapses",
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
await expect(page.getByText("What do you want me on first?", { exact: true })).toHaveCount(0);
await expect(
page.getByTestId("transcript").getByText("Start the conversation — New Bot is on stage above."),
).toBeVisible();
await captureScreenshot(page, testInfo, "create-chat-sidepanel-closed");
await page.getByTestId("toggle-bots-sidebar").click();

View File

@ -0,0 +1,87 @@
import { expect, type Locator, test } from "@playwright/test";
import { captureScreenshot, completeOnboarding, rpc, signup, waitForChatComposer } from "./helpers";
function projectSpace(projekte: Locator, name: string) {
return projekte.locator('[data-sidebar-group^="project-space:"]').filter({ hasText: name });
}
test("a new project opens straight into the conversation with its own main agent", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `project-main-${stamp}@rakazo.test`, "password12", "Project Main");
await completeOnboarding(page);
const homeSpaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id"));
const homeMain = await rpc<{ id: string }>(page, "bots/ensureMain", {});
const projekte = page.locator(".sidebar-projekte");
await projekte.getByTestId("create-project-trigger").click();
const dialog = page.getByRole("dialog", { name: "New project" });
await dialog.getByLabel("Name").fill("Launch");
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
await expect
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
.not.toBe(homeSpaceId);
await waitForChatComposer(page);
// The project is one conversation, led by a main agent of its own.
const projectEntry = projectSpace(projekte, "Launch").getByRole("button", {
name: "Chief, main conversation",
});
await expect(projectEntry).toBeVisible({ timeout: 30_000 });
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0);
const groupId = new URL(page.url()).pathname.split("/").at(-1)!;
const snapshot = await rpc<{ leadBotId?: string }>(page, "threads/get", { groupId });
expect(snapshot.leadBotId).toBeTruthy();
expect(snapshot.leadBotId).not.toBe(homeMain.id);
const composer = page.getByRole("combobox", { name: "Message" });
await composer.fill("What should we do first?");
await composer.press("Enter");
const transcript = page.getByTestId("transcript");
await expect(transcript).toContainText(/on it|handled/i, { timeout: 60_000 });
await captureScreenshot(page, testInfo, "project-main-conversation");
});
test("a project's main agent hands work to the project's specialist", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `project-delegate-${stamp}@rakazo.test`, "password12", "Project Delegate");
await completeOnboarding(page);
const projekte = page.locator(".sidebar-projekte");
await projekte.getByTestId("create-project-trigger").click();
const dialog = page.getByRole("dialog", { name: "New project" });
await dialog.getByLabel("Name").fill("Website");
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
await waitForChatComposer(page);
const groupId = new URL(page.url()).pathname.split("/").at(-1)!;
const designer = await rpc<{ id: string }>(page, "bots/create", {
name: "Designer",
title: "",
description: "",
instructions: "",
notifyOnFinish: true,
});
const composer = page.getByRole("combobox", { name: "Message" });
await composer.fill("delegate to the bot named Designer: sketch a landing page");
await composer.press("Enter");
const transcript = page.getByTestId("transcript");
const card = transcript.getByTestId("delegated-task-card");
await expect(card).toBeVisible({ timeout: 60_000 });
await expect(card).toContainText("Designer");
await expect(card).toHaveAttribute("data-status", "done", { timeout: 60_000 });
await expect(
transcript.getByText("closing the task: sketch a landing page.").first(),
).toBeVisible({ timeout: 60_000 });
const snapshot = await rpc<{ members?: Array<{ botId: string }> }>(page, "threads/get", {
groupId,
});
expect(snapshot.members?.map((member) => member.botId)).toContain(designer.id);
await captureScreenshot(page, testInfo, "project-main-agent-delegates");
});

View File

@ -23,7 +23,6 @@ test("sidebar has expected navigation items and no integrations", async ({ page
// - textbox "Search" (search input)
// - button "Today"
// - text "Talk to your Oos" (conversations section header)
// - status "Not configured" (trading bot URL unset)
// - NO "Integration(en)" button or text
// Search: it's a textbox, not a button
@ -37,13 +36,6 @@ test("sidebar has expected navigation items and no integrations", async ({ page
const conversationsText = sidebar.getByText("Talk to your Oos");
await expect(conversationsText).toBeVisible({ timeout: 5000 });
// Trading mode comes from the bot API. With no URL configured the sidebar
// must not claim Live Trading from a local preference.
const tradingStatus = sidebar.getByTestId("trading-mode-status");
await expect(tradingStatus).toBeVisible({ timeout: 5000 });
await expect(tradingStatus).toContainText("Not configured");
await expect(sidebar.getByRole("button", { name: "Live Trading" })).toHaveCount(0);
// No "Integration(en)" button
const integrationBtn = sidebar.getByRole("button", { name: /Integration(en)?/i });
await expect(integrationBtn).toHaveCount(0, { timeout: 5000 });

View File

@ -16,7 +16,7 @@ function projectSpace(projekte: Locator, name: string) {
});
}
test("Projects + opens new project dialog instead of group form", async ({ page }) => {
test("Projects + opens new project dialog instead of group form", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `proj-create-${stamp}@rakazo.test`, "password12", "Project Create");
await completeOnboarding(page);
@ -34,16 +34,21 @@ test("Projects + opens new project dialog instead of group form", async ({ page
await dialog.getByLabel("Name").fill("Customer project");
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
await expect(page).not.toHaveURL(/\/onboarding/);
await page.waitForURL(/\/app/);
await page.waitForLoadState("networkidle");
// The new project opens its conversation with the project's main agent.
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
await expect(projekte.getByText("Customer project", { exact: true })).toBeVisible({
timeout: 30_000,
});
await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount(
0,
);
await expect(page.getByTestId("project-start-main")).toBeVisible();
await expect(page.getByTestId("project-start-main")).toHaveText(/Start/);
await expect(
projectSpace(projekte, "Customer project").getByRole("button", {
name: "Chief, main conversation",
}),
).toBeVisible();
await expect(page.getByTestId("project-start-main")).toHaveCount(0);
await captureScreenshot(page, testInfo, "new-project-main-conversation");
});
for (const owner of [true, false]) {
@ -119,17 +124,18 @@ test("spaces stay invisible by default and chat creation requires approval", asy
const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group");
const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1];
expect(supportSpaceId).toBeTruthy();
await supportSpace.getByRole("button", { name: "Select Customer support" }).click();
await page.waitForURL(/\/app(?:\/)?$/);
// A project created from chat comes with its own main agent, reached through its conversation.
await supportSpace.getByRole("button", { name: "Chief, main conversation" }).click();
await page.waitForURL(/\/app\/g\/[^/]+$/);
await expect
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
.toBe(supportSpaceId);
// The main conversation stays the first entry while another space is open.
// The home main conversation stays the first entry while another space is open.
await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible();
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible();
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0);
await captureScreenshot(page, testInfo, "spaces-sidebar");
await page.evaluate((id) => {
@ -143,7 +149,9 @@ test("spaces stay invisible by default and chat creation requires approval", asy
await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible();
});
test("a new space opens empty and can be deleted from its menu", async ({ page }, testInfo) => {
test("a new space opens its main conversation and can be deleted from its menu", async ({
page,
}, testInfo) => {
const stamp = Date.now();
await signup(page, `spaces-delete-${stamp}@rakazo.test`, "password12", "Space Owner");
await completeOnboarding(page);
@ -154,18 +162,20 @@ test("a new space opens empty and can be deleted from its menu", async ({ page }
await dialog.getByLabel("Name").fill("Temporary");
await dialog.getByRole("button", { name: "Create space", exact: true }).click();
// Onboarding runs once per user: a new space opens directly, with no bot of its own.
await page.waitForURL(/\/app(?:\/)?$/);
// Onboarding runs once per user: a new space opens directly in the conversation with its
// own main agent, its only bot.
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
const projekte = page.locator(".sidebar-projekte");
await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible();
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible();
expect(await rpc<unknown[]>(page, "bots/list", {})).toEqual([]);
await captureScreenshot(page, testInfo, "new-space-empty");
// The empty space stays selectable instead of trapping the user.
await expect(projekte.getByRole("button", { name: "Select Temporary" }).first()).toBeVisible();
await expect(
projectSpace(projekte, "Temporary").getByRole("button", { name: "Chief, main conversation" }),
).toBeVisible();
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0);
expect(await rpc<Array<{ kind: string }>>(page, "bots/list", {})).toEqual([
expect.objectContaining({ kind: "main" }),
]);
await captureScreenshot(page, testInfo, "new-space-main-conversation");
// Deletion is discoverable through the visible Space actions menu.
await projectSpace(projekte, "Temporary")
@ -180,6 +190,7 @@ test("a new space opens empty and can be deleted from its menu", async ({ page }
await deleteSpaceDialog.getByRole("button", { name: "Delete", exact: true }).click();
await expect(sidebar.getByText("Temporary", { exact: true })).toHaveCount(0);
await expect(page).not.toHaveURL(/\/onboarding/);
// The project's main agent went with it.
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
// A stale onboarding URL recovers without duplicating the bot.
@ -212,7 +223,7 @@ test("deleting the last bot in a space stays in the app after first use", async
const dialog = page.getByRole("dialog", { name: "New space" });
await dialog.getByLabel("Name").fill("Side");
await dialog.getByRole("button", { name: "Create space", exact: true }).click();
await page.waitForURL(/\/app(?:\/)?$/);
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
const projekte = page.locator(".sidebar-projekte");
await expect(projekte.getByText("Side", { exact: true })).toBeVisible();
@ -229,8 +240,11 @@ test("deleting the last bot in a space stays in the app after first use", async
await expect(page).not.toHaveURL(/\/onboarding/);
await page.goto("/app");
await expect(page).not.toHaveURL(/\/onboarding/);
await expect(projekte.getByRole("button", { name: "Select Side" }).first()).toBeVisible();
await expect(page.getByTestId("project-start-main")).toBeVisible();
// The project's main conversation stays, so the project opens there.
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
await expect(
projectSpace(projekte, "Side").getByRole("button", { name: "Chief, main conversation" }),
).toBeVisible();
await projectSpace(projekte, "Side").getByRole("button", { name: "Actions for Side" }).click();
await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible();

View File

@ -1,16 +0,0 @@
import { test } from "@playwright/test";
test.describe("Steve Jobs CTO Agent", () => {
test.beforeEach(async () => {
// We assume the user has logged in and the bot is seeded
// This is a placeholder for the perfect test architecture
});
test("should be visible in the sidebar", async () => {
// This is a dummy test to show we started the test architecture for Steve Jobs
});
test("should prefer voice communications (VOICE FIRST)", async () => {
// Voice first features
});
});

View File

@ -14,7 +14,6 @@ import {
showSessionUnavailable,
} from "./lib/session-gate";
import { IntegrationSetupPage } from "./pages/IntegrationSetup";
import { LocalSettingsPage } from "./pages/LocalSettings";
import { McpOAuthCallbackPage } from "./pages/McpOAuthCallback";
import { ShellPage } from "./pages/Shell";
@ -30,9 +29,18 @@ const OnboardingPage = lazyWithRecovery("pages/Onboarding", () =>
const WelcomePage = lazyWithRecovery("pages/Welcome", () =>
import("./pages/Welcome").then((module) => ({ default: module.WelcomePage })),
);
const LocalSettingsPage = lazyWithRecovery("pages/LocalSettings", () =>
import("./pages/LocalSettings").then((module) => ({ default: module.LocalSettingsPage })),
);
export function App() {
if (window.location.pathname === LOCAL_SETTINGS_PAGE) return <LocalSettingsPage />;
if (window.location.pathname === LOCAL_SETTINGS_PAGE) {
return (
<Suspense fallback={<div className="h-full bg-background" data-rakazo-app-state="loading" />}>
<LocalSettingsPage />
</Suspense>
);
}
return <SessionApp />;
}

View File

@ -1,28 +0,0 @@
import { CheckCircle2, Database, EyeOff, Lock } from "lucide-react";
import { memo } from "react";
const TRUST_ITEMS = [
{ icon: Lock, label: "Verschlüsselt", desc: "AES-256 für sensible Daten" },
{ icon: Database, label: "Lokal gespeichert", desc: "Eigene PostgreSQL-Datenbank" },
{ icon: EyeOff, label: "Kein Tracking", desc: "Keine Analytics, kein Telemetry" },
] as const;
export const DataTrustPanel = memo(function DataTrustPanel() {
return (
<div className="hidden xl:flex flex-col items-start gap-1.5 rounded-xl border border-border/40 bg-muted/30 px-3 py-2.5">
<div className="flex items-center gap-1.5 text-xs font-medium text-sidebar-foreground">
<CheckCircle2 className="size-3.5 text-success" />
<span>Datenschutz aktiv</span>
</div>
<div className="flex flex-col gap-1">
{TRUST_ITEMS.map(({ icon: Icon, label, desc }) => (
<div key={label} className="flex items-center gap-1.5 rk-sidebar-muted">
<Icon className="size-3" />
<span className="text-[11px] leading-tight">{label}</span>
<span className="text-[10px] leading-tight">· {desc}</span>
</div>
))}
</div>
</div>
);
});

View File

@ -1,55 +0,0 @@
// @vitest-environment jsdom
import type { ReactNode } from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { expect, it, vi } from "vitest";
vi.mock("@lingui/react/macro", () => ({
Trans: ({ children }: { children: ReactNode }) => children,
}));
import { TradingModeIndicator } from "./TradingModeIndicator";
it("renders the API mode and ignores a stored live preference", async () => {
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
window.localStorage.setItem("evoo_trading_mode", "live");
const container = document.createElement("div");
document.body.append(container);
const root = createRoot(container);
try {
await act(async () =>
root.render(
<TradingModeIndicator
snapshot={{
state: "ready",
status: { mode: "paper", kind: "dry_run", adapterCount: 3, lastError: null },
}}
/>,
),
);
const status = container.querySelector("[data-testid='trading-mode-status']");
expect(status?.getAttribute("data-trading-state")).toBe("dry_run");
expect(status?.textContent).toContain("Dry run");
expect(status?.textContent).toContain("3 adapters");
expect(container.querySelector("button")).toBeNull();
expect(status?.textContent).not.toContain("Live Trading");
await act(async () => root.render(<TradingModeIndicator snapshot={{ state: "offline" }} />));
expect(container.querySelector("[data-testid='trading-mode-status']")?.textContent).toContain(
"Offline",
);
await act(async () =>
root.render(<TradingModeIndicator snapshot={{ state: "unconfigured" }} />),
);
expect(container.querySelector("[data-testid='trading-mode-status']")?.textContent).toContain(
"Not configured",
);
} finally {
await act(async () => root.unmount());
container.remove();
window.localStorage.clear();
vi.unstubAllGlobals();
}
});

View File

@ -1,95 +0,0 @@
import { Trans } from "@lingui/react/macro";
import { cn } from "@rakazo/ui-web";
import { Activity } from "lucide-react";
import type { ReactNode } from "react";
import {
presentTradingStatus,
type TradingBotUiState,
type TradingModeLabelId,
type TradingModeSubtitle,
} from "../lib/trading-bot-status";
export function TradingModeIndicator({
snapshot,
className,
}: {
snapshot: TradingBotUiState;
className?: string;
}) {
const presentation = presentTradingStatus(snapshot);
return (
<div
role="status"
aria-live="polite"
aria-busy={presentation.label === "loading"}
data-testid="trading-mode-status"
data-trading-state={presentation.label}
className={cn("flex min-w-0 items-center gap-3 rounded-xl px-2.5 py-2", className)}
>
<span
className={cn(
"grid h-[30px] w-[30px] shrink-0 place-items-center rounded-lg",
presentation.tone === "live"
? "bg-warning/10 text-warning"
: "bg-muted text-muted-foreground",
)}
>
<Activity
strokeWidth={1.8}
aria-hidden="true"
className={cn("size-icon-md", presentation.pulse && "animate-pulse")}
/>
</span>
<span className="min-w-0">
<span className="block truncate text-ui-body font-medium text-foreground/90">
<TradingModeLabel label={presentation.label} />
</span>
{presentation.subtitle ? (
<span className="block truncate text-ui-body-sm rk-sidebar-muted">
<TradingModeSubtitleText subtitle={presentation.subtitle} />
</span>
) : null}
</span>
</div>
);
}
function TradingModeLabel({ label }: { label: TradingModeLabelId }): ReactNode {
switch (label) {
case "loading":
return <Trans>Trading</Trans>;
case "unconfigured":
return <Trans>Not configured</Trans>;
case "offline":
return <Trans>Offline</Trans>;
case "unknown":
return <Trans>Unknown</Trans>;
case "live":
return <Trans>Live Trading</Trans>;
case "paper":
return <Trans>Paper</Trans>;
case "dry_run":
return <Trans>Dry run</Trans>;
default: {
const exhaustive: never = label;
return exhaustive;
}
}
}
function TradingModeSubtitleText({ subtitle }: { subtitle: TradingModeSubtitle }): ReactNode {
switch (subtitle.type) {
case "error":
return subtitle.text;
case "adapters":
return subtitle.count === 1 ? (
<Trans>1 adapter</Trans>
) : (
<Trans>{subtitle.count} adapters</Trans>
);
default: {
const exhaustive: never = subtitle;
return exhaustive;
}
}
}

View File

@ -1,14 +1,15 @@
import { describe, expect, it } from "vitest";
import { isEmptyProjectSpace, projectGroupChatsInSpace } from "./project-empty-state.js";
import { isEmptyProjectSpace, projectMainGroup } from "./project-empty-state.js";
describe("projectGroupChatsInSpace", () => {
it("returns project groups in a space without the main conversation", () => {
describe("projectMainGroup", () => {
it("finds the main conversation of a space", () => {
const groups = [
{ id: "g1", kind: "group" as const, spaceId: "space-a" },
{ id: "g2", kind: "main" as const, spaceId: "space-a" },
{ id: "g3", kind: "group" as const, spaceId: "space-b" },
{ id: "g3", kind: "main" as const, spaceId: "space-b" },
];
expect(projectGroupChatsInSpace(groups, "space-a").map((group) => group.id)).toEqual(["g1"]);
expect(projectMainGroup(groups, "space-a")?.id).toBe("g2");
expect(projectMainGroup(groups, "space-c")).toBeUndefined();
});
});
@ -22,7 +23,7 @@ describe("isEmptyProjectSpace", () => {
expect(isEmptyProjectSpace("home", spaces, [], "home")).toBe(false);
});
it("is true for a project space without group chats", () => {
it("is true for a project space without conversations", () => {
expect(isEmptyProjectSpace("project", spaces, [], "home")).toBe(true);
});
@ -31,8 +32,8 @@ describe("isEmptyProjectSpace", () => {
expect(isEmptyProjectSpace("project", spaces, groups, "home")).toBe(false);
});
it("ignores the main conversation when deciding emptiness", () => {
it("is false once the project has its main conversation", () => {
const groups = [{ id: "g1", kind: "main" as const, spaceId: "project" }];
expect(isEmptyProjectSpace("project", spaces, groups, "home")).toBe(true);
expect(isEmptyProjectSpace("project", spaces, groups, "home")).toBe(false);
});
});

View File

@ -1,14 +1,14 @@
import type { Group, Space } from "@rakazo/contracts";
/** Project-scoped group chats in a space (excludes the main conversation). */
export function projectGroupChatsInSpace(
groups: readonly Pick<Group, "id" | "kind" | "spaceId">[],
/** The project's own main conversation, where the user talks to its main agent. */
export function projectMainGroup<T extends Pick<Group, "kind" | "spaceId">>(
groups: readonly T[],
spaceId: string,
): readonly Pick<Group, "id">[] {
return groups.filter((group) => group.spaceId === spaceId && group.kind !== "main");
): T | undefined {
return groups.find((group) => group.spaceId === spaceId && group.kind === "main");
}
/** Non-default project space with no project group conversations yet. */
/** Non-default project space without any conversation yet, not even its main one. */
export function isEmptyProjectSpace(
spaceId: string | null | undefined,
spaces: readonly Pick<Space, "id" | "isDefault">[],
@ -18,5 +18,5 @@ export function isEmptyProjectSpace(
if (!spaceId || !homeSpaceId || spaceId === homeSpaceId) return false;
const space = spaces.find((item) => item.id === spaceId);
if (!space || space.isDefault) return false;
return projectGroupChatsInSpace(groups, spaceId).length === 0;
return !groups.some((group) => group.spaceId === spaceId);
}

View File

@ -1,178 +0,0 @@
import { TRADING_BOT_PROXY_PATH } from "@rakazo/core";
import { describe, expect, it, vi } from "vitest";
import {
fetchTradingBotSnapshot,
pollTradingBotStatus,
presentTradingStatus,
resolveTradingStatusTarget,
} from "./trading-bot-status";
describe("resolveTradingStatusTarget", () => {
it("polls the bot directly only when the Vite URL is a bare origin", () => {
expect(resolveTradingStatusTarget(undefined)).toEqual({
kind: "proxy",
url: TRADING_BOT_PROXY_PATH,
});
expect(resolveTradingStatusTarget("")).toEqual({ kind: "proxy", url: TRADING_BOT_PROXY_PATH });
expect(resolveTradingStatusTarget(" http://100.64.0.1:8000/bot/ ")).toEqual({
kind: "direct",
url: "http://100.64.0.1:8000/bot/api/status",
});
expect(resolveTradingStatusTarget("file:///tmp/bot")).toEqual({
kind: "proxy",
url: TRADING_BOT_PROXY_PATH,
});
});
});
describe("presentTradingStatus", () => {
it("shows the API mode and never a stored live preference", () => {
expect(
presentTradingStatus({
state: "ready",
status: { mode: "live", kind: "live", adapterCount: 2, lastError: null },
}),
).toMatchObject({
label: "live",
tone: "live",
pulse: true,
subtitle: { type: "adapters", count: 2 },
});
expect(
presentTradingStatus({
state: "ready",
status: { mode: "paper", kind: "dry_run", adapterCount: 2, lastError: "adapter timeout" },
}).subtitle,
).toEqual({ type: "error", text: "adapter timeout" });
expect(presentTradingStatus({ state: "offline" }).label).toBe("offline");
expect(presentTradingStatus({ state: "unknown" }).label).toBe("unknown");
expect(presentTradingStatus({ state: "unconfigured" }).label).toBe("unconfigured");
expect(presentTradingStatus({ state: "loading" }).label).toBe("loading");
});
});
describe("fetchTradingBotSnapshot", () => {
it("maps a direct live body and reports an unmapped 200 as unknown", async () => {
const live = await fetchTradingBotSnapshot(
{ kind: "direct", url: "http://bot.test/bot/api/status" },
new AbortController().signal,
async () => jsonResponse({ mode: "LIVE" }),
);
expect(live).toMatchObject({ state: "ready", status: { mode: "live", kind: "live" } });
const unknown = await fetchTradingBotSnapshot(
{ kind: "direct", url: "http://bot.test/bot/api/status" },
new AbortController().signal,
async () => jsonResponse({ status: "ok" }),
);
expect(unknown).toEqual({ state: "unknown" });
});
it("treats a failed direct fetch as offline", async () => {
const offline = await fetchTradingBotSnapshot(
{ kind: "direct", url: "http://bot.test/bot/api/status" },
new AbortController().signal,
async () => {
throw new Error("network");
},
);
expect(offline).toEqual({ state: "offline" });
});
it("stops on an explicit unconfigured proxy response", async () => {
const fetchImpl = vi.fn(async () => jsonResponse({ configured: false }));
const snapshots: string[] = [];
await pollTradingBotStatus({
target: { kind: "proxy", url: TRADING_BOT_PROXY_PATH },
signal: new AbortController().signal,
fetchImpl,
onSnapshot: (snapshot) => snapshots.push(snapshot.state),
sleep: async () => false,
});
expect(snapshots).toEqual(["unconfigured"]);
expect(fetchImpl).toHaveBeenCalledTimes(1);
});
});
describe("pollTradingBotStatus", () => {
it("follows a live read with offline on failure and backs off", async () => {
let calls = 0;
const fetchImpl = vi.fn(async () => {
calls += 1;
if (calls === 1) return jsonResponse({ badge: "LIVE" });
throw new Error("down");
});
const sleeps: number[] = [];
let releaseSleep: () => void = () => undefined;
const sleep = (ms: number, signal: AbortSignal) => {
sleeps.push(ms);
return new Promise<boolean>((resolve) => {
if (signal.aborted) {
resolve(true);
return;
}
const onAbort = () => resolve(true);
signal.addEventListener("abort", onAbort, { once: true });
releaseSleep = () => {
signal.removeEventListener("abort", onAbort);
resolve(false);
};
});
};
const snapshots: string[] = [];
const controller = new AbortController();
const done = pollTradingBotStatus({
target: { kind: "direct", url: "http://bot.test/bot/api/status" },
signal: controller.signal,
fetchImpl,
intervalMs: 20_000,
maxBackoffMs: 120_000,
sleep,
onSnapshot: (snapshot) => snapshots.push(snapshot.state),
});
await vi.waitFor(() => expect(snapshots).toEqual(["ready"]));
expect(sleeps).toEqual([20_000]);
releaseSleep();
await vi.waitFor(() => expect(snapshots).toEqual(["ready", "offline"]));
expect(sleeps).toEqual([20_000, 20_000]);
releaseSleep();
await vi.waitFor(() => expect(sleeps).toEqual([20_000, 20_000, 40_000]));
controller.abort();
await done;
expect(fetchImpl).toHaveBeenCalledTimes(3);
});
it("does not publish offline when the poll is aborted in flight", async () => {
const controller = new AbortController();
const snapshots: string[] = [];
const fetchImpl: typeof fetch = (_input, init) =>
new Promise((_resolve, reject) => {
const abort = () => {
const error = new Error("aborted");
error.name = "AbortError";
reject(error);
};
if (init?.signal?.aborted) {
abort();
return;
}
init?.signal?.addEventListener("abort", abort, { once: true });
});
const done = pollTradingBotStatus({
target: { kind: "direct", url: "http://bot.test/bot/api/status" },
signal: controller.signal,
fetchImpl,
onSnapshot: (snapshot) => snapshots.push(snapshot.state),
});
controller.abort();
await done;
expect(snapshots).toEqual([]);
});
});
function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
}

View File

@ -1,243 +0,0 @@
import type { TradingBotSnapshot, TradingBotStatusView } from "@rakazo/core";
import {
interpretTradingBotProxyResponse,
mapTradingBotStatus,
parseTradingBotOrigin,
readBoundedJsonResponse,
TRADING_BOT_POLL_INTERVAL_MS,
TRADING_BOT_POLL_MAX_BACKOFF_MS,
TRADING_BOT_PROXY_PATH,
TRADING_BOT_STATUS_MAX_BYTES,
TRADING_BOT_STATUS_TIMEOUT_MS,
tradingBotBackoffMs,
tradingBotStatusUrl,
} from "@rakazo/core";
import { useEffect, useState } from "react";
export type TradingStatusTarget =
| { kind: "direct"; url: string }
| { kind: "proxy"; url: typeof TRADING_BOT_PROXY_PATH };
export type TradingBotUiState = TradingBotSnapshot | { state: "loading" };
export type TradingModeLabelId =
| "loading"
| "unconfigured"
| "offline"
| "unknown"
| "live"
| "paper"
| "dry_run";
export type TradingModeSubtitle =
| { type: "adapters"; count: number }
| { type: "error"; text: string };
export type TradingModePresentation = {
label: TradingModeLabelId;
tone: "live" | "quiet";
pulse: boolean;
subtitle: TradingModeSubtitle | null;
};
type FetchSnapshot = (
target: TradingStatusTarget,
signal: AbortSignal,
fetchImpl: typeof fetch,
timeoutMs: number,
) => Promise<TradingBotSnapshot>;
/** Direct poll when the Vite URL is set. Otherwise the same-origin API proxy. */
export function resolveTradingStatusTarget(viteUrl: string | undefined): TradingStatusTarget {
const parsed = parseTradingBotOrigin(viteUrl);
if (parsed.kind === "origin") {
return { kind: "direct", url: tradingBotStatusUrl(parsed.origin) };
}
return { kind: "proxy", url: TRADING_BOT_PROXY_PATH };
}
export function presentTradingStatus(snapshot: TradingBotUiState): TradingModePresentation {
switch (snapshot.state) {
case "loading":
return { label: "loading", tone: "quiet", pulse: false, subtitle: null };
case "unconfigured":
return { label: "unconfigured", tone: "quiet", pulse: false, subtitle: null };
case "offline":
return { label: "offline", tone: "quiet", pulse: false, subtitle: null };
case "unknown":
return { label: "unknown", tone: "quiet", pulse: false, subtitle: null };
case "ready":
return presentReady(snapshot.status);
default: {
const exhaustive: never = snapshot;
return exhaustive;
}
}
}
export async function fetchTradingBotSnapshot(
target: TradingStatusTarget,
signal: AbortSignal,
fetchImpl: typeof fetch = fetch,
timeoutMs = TRADING_BOT_STATUS_TIMEOUT_MS,
): Promise<TradingBotSnapshot> {
const timeout = linkAbort(signal, timeoutMs);
try {
const response = await fetchImpl(target.url, {
method: "GET",
redirect: "error",
cache: "no-store",
credentials: target.kind === "direct" ? "omit" : "include",
signal: timeout.signal,
headers: { accept: "application/json" },
});
if (target.kind === "direct" && !response.ok) {
cancelBody(response);
return { state: "offline" };
}
let body: unknown = null;
try {
body = await readBoundedJsonResponse<unknown>(
response,
TRADING_BOT_STATUS_MAX_BYTES,
timeout.signal,
);
} catch (error) {
if (signal.aborted) throw error;
body = null;
}
if (target.kind === "proxy") return interpretTradingBotProxyResponse(response.status, body);
if (body === null) return { state: "offline" };
const status = mapTradingBotStatus(body);
return status ? { state: "ready", status } : { state: "unknown" };
} catch (error) {
if (signal.aborted) throw error;
return { state: "offline" };
} finally {
timeout.cancel();
}
}
export async function pollTradingBotStatus(options: {
target: TradingStatusTarget;
signal: AbortSignal;
onSnapshot: (snapshot: TradingBotSnapshot) => void;
fetchImpl?: typeof fetch;
fetchSnapshot?: FetchSnapshot;
intervalMs?: number;
maxBackoffMs?: number;
timeoutMs?: number;
sleep?: (ms: number, signal: AbortSignal) => Promise<boolean>;
}): Promise<void> {
const fetchSnapshot = options.fetchSnapshot ?? fetchTradingBotSnapshot;
const fetchImpl = options.fetchImpl ?? fetch;
const intervalMs = options.intervalMs ?? TRADING_BOT_POLL_INTERVAL_MS;
const maxBackoffMs = options.maxBackoffMs ?? TRADING_BOT_POLL_MAX_BACKOFF_MS;
const timeoutMs = options.timeoutMs ?? TRADING_BOT_STATUS_TIMEOUT_MS;
const sleep = options.sleep ?? delayAbortable;
let failures = 0;
while (!options.signal.aborted) {
let snapshot: TradingBotSnapshot;
try {
snapshot = await fetchSnapshot(options.target, options.signal, fetchImpl, timeoutMs);
} catch {
if (options.signal.aborted) return;
snapshot = { state: "offline" };
}
if (options.signal.aborted) return;
options.onSnapshot(snapshot);
if (snapshot.state === "unconfigured") return;
if (snapshot.state === "offline") failures += 1;
else failures = 0;
const delay =
snapshot.state === "offline"
? tradingBotBackoffMs(intervalMs, failures, maxBackoffMs)
: intervalMs;
const aborted = await sleep(delay, options.signal);
if (aborted) return;
}
}
export function useTradingBotStatus(): TradingBotUiState {
const [snapshot, setSnapshot] = useState<TradingBotUiState>({ state: "loading" });
useEffect(() => {
const controller = new AbortController();
const target = resolveTradingStatusTarget(readViteTradingBotUrl());
void pollTradingBotStatus({
target,
signal: controller.signal,
onSnapshot: (next) => {
if (!controller.signal.aborted) setSnapshot(next);
},
});
return () => controller.abort();
}, []);
return snapshot;
}
function readViteTradingBotUrl(): string | undefined {
const value = import.meta.env.VITE_TRADING_BOT_URL;
return typeof value === "string" ? value : undefined;
}
function presentReady(status: TradingBotStatusView): TradingModePresentation {
const subtitle: TradingModeSubtitle | null = status.lastError
? { type: "error", text: status.lastError }
: status.adapterCount === null
? null
: { type: "adapters", count: status.adapterCount };
switch (status.kind) {
case "live":
return { label: "live", tone: "live", pulse: true, subtitle };
case "paper":
return { label: "paper", tone: "quiet", pulse: false, subtitle };
case "dry_run":
return { label: "dry_run", tone: "quiet", pulse: false, subtitle };
default: {
const exhaustive: never = status.kind;
return exhaustive;
}
}
}
function linkAbort(
parent: AbortSignal,
timeoutMs: number,
): { signal: AbortSignal; cancel: () => void } {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const onParent = () => controller.abort();
if (parent.aborted) controller.abort();
else parent.addEventListener("abort", onParent);
return {
signal: controller.signal,
cancel: () => {
clearTimeout(timer);
parent.removeEventListener("abort", onParent);
},
};
}
function delayAbortable(ms: number, signal: AbortSignal): Promise<boolean> {
if (signal.aborted) return Promise.resolve(true);
return new Promise((resolve) => {
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve(false);
}, ms);
const onAbort = () => {
clearTimeout(timer);
signal.removeEventListener("abort", onAbort);
resolve(true);
};
signal.addEventListener("abort", onAbort);
});
}
function cancelBody(response: Response): void {
try {
void Promise.resolve(response.body?.cancel()).catch(() => undefined);
} catch {
// Best-effort. The snapshot is already offline.
}
}

View File

@ -46,11 +46,6 @@ msgstr "{0} (über 10 MiB)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr "{0} Adapter"
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr "{0}, 1:1-Unterhaltung"
msgid "{0}, group conversation"
msgstr "{0}, Gruppenunterhaltung"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr "{0}, Hauptunterhaltung"
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} arbeitet"
msgid "/Users/you/projects/acme"
msgstr "/Users/you/projects/acme"
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr "1 Adapter"
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr "1:1-Unterhaltung"
@ -545,10 +529,6 @@ msgstr "Schreiben erlauben"
msgid "Allowed once"
msgstr "Einmal erlaubt"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr "Alpha-Signale"
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "Du hast bereits ein Konto?"
@ -620,10 +600,6 @@ msgstr "Genehmigen"
msgid "Approve this server to let your agent use its tools."
msgstr "Genehmige diesen Server, damit dein Agent seine Tools verwenden kann."
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr "Architektur-Audit"
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "Archivieren"
@ -2078,10 +2054,6 @@ msgstr "Entwerfe die Spezifikation…"
msgid "Drag, drop, or paste an image"
msgstr "Bild ziehen, ablegen oder einfügen"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr "Probelauf"
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "Duplizieren"
@ -2094,10 +2066,6 @@ msgstr "z. B. Hivenet Agent, Presales, Zeiterfassungs-Bot"
msgid "Earlier message"
msgstr "Frühere Nachricht"
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr "Echtzeit-Risiko & Drawdown-Analyse"
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "Zuerst bearbeiten"
@ -2316,10 +2284,6 @@ msgstr "Passwort vergessen?"
msgid "Forward"
msgstr "Vorwärts"
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr "Kostenlos"
@ -2396,10 +2360,6 @@ msgstr "Updates bündeln"
msgid "Groups"
msgstr "Gruppen"
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr "Guten Tag."
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr "Gewohnheiten"
@ -2598,10 +2558,6 @@ msgstr "Intervalleinheit"
msgid "Isolated"
msgstr "Isoliert"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr "ist bereit."
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "Seine Unterhaltung, Dateien und Routinen werden dauerhaft gelöscht. Von ihm erstellte Bots bleiben in deiner Liste."
@ -2697,10 +2653,6 @@ msgstr "Ich höre zu — was ist kaputt oder langsam?"
msgid "Listening — what's the story?"
msgstr "Ich höre zu — was ist die Geschichte?"
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr "Live-Trading"
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "Frühere Nachrichten laden"
@ -3247,7 +3199,6 @@ msgstr "Es werden keine Nutzungsdaten an Dritte gesendet"
msgid "None yet"
msgstr "Noch keine"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr "Nicht konfiguriert"
@ -3309,10 +3260,6 @@ msgstr "OAuth-Verbindung fehlgeschlagen"
msgid "OAuth expired"
msgstr "OAuth abgelaufen"
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr "Offline"
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr "An"
@ -3433,10 +3380,6 @@ msgstr "Seite"
msgid "PagerDuty incident"
msgstr "PagerDuty-Vorfall"
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr "Paper"
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "Zurückstellen"
@ -3517,10 +3460,6 @@ msgstr "Feile an den Details…"
msgid "Polling devices…"
msgstr "Frage Geräte ab…"
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr "Portfolio-Check"
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr "POST an"
@ -4030,10 +3969,6 @@ msgstr "Prüfe das Risiko…"
msgid "Scene"
msgstr "Szene"
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr "Schlichtheit, Eleganz & Detailtiefe"
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr "Bleibe in meinem Fachgebiet…"
msgid "Steps"
msgstr "Schritte"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr "Steve-Jobs-Audit"
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr "Tools"
msgid "Tracing the path…"
msgstr "Verfolge den Pfad…"
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr "Trading"
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr "Nicht verfügbar"
msgid "Uninstall"
msgstr "Deinstallieren"
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr "Unbekannt"
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr "Verknüpfung aufheben"
@ -4836,14 +4759,6 @@ msgstr "Die Stimme ist noch nicht verfügbar. Du kannst sie später in den Einst
msgid "Voice set to {0}."
msgstr "Stimme auf {0} gesetzt."
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr "Volatilität & Orderbuch-Liquidität"
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr "Wartet"

View File

@ -46,11 +46,6 @@ msgstr "{0} (over 10 MiB)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr "{0} adapters"
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr "{0}, 1:1 conversation"
msgid "{0}, group conversation"
msgstr "{0}, group conversation"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr "{0}, main conversation"
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} is working"
msgid "/Users/you/projects/acme"
msgstr "/Users/you/projects/acme"
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr "1 adapter"
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr "1:1 conversation"
@ -545,10 +529,6 @@ msgstr "Allow writing"
msgid "Allowed once"
msgstr "Allowed once"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr "Alpha-Signale"
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "Already have an account?"
@ -620,10 +600,6 @@ msgstr "Approve"
msgid "Approve this server to let your agent use its tools."
msgstr "Approve this server to let your agent use its tools."
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr "Architektur-Audit"
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "Archive"
@ -2078,10 +2054,6 @@ msgstr "Drafting the spec…"
msgid "Drag, drop, or paste an image"
msgstr "Drag, drop, or paste an image"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr "Dry run"
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "Duplicate"
@ -2094,10 +2066,6 @@ msgstr "e.g. Hivenet Agent, Presales, Timesheets bot"
msgid "Earlier message"
msgstr "Earlier message"
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr "Echtzeit-Risiko & Drawdown-Analyse"
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "Edit first"
@ -2316,10 +2284,6 @@ msgstr "Forgot password?"
msgid "Forward"
msgstr "Forward"
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr "Free"
@ -2396,10 +2360,6 @@ msgstr "Group updates"
msgid "Groups"
msgstr "Groups"
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr "Guten Tag."
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr "Habits"
@ -2598,10 +2558,6 @@ msgstr "Interval unit"
msgid "Isolated"
msgstr "Isolated"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr "ist bereit."
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
@ -2697,10 +2653,6 @@ msgstr "Listening — what's broken or slow?"
msgid "Listening — what's the story?"
msgstr "Listening — what's the story?"
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr "Live Trading"
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "Load earlier messages"
@ -3247,7 +3199,6 @@ msgstr "No usage data is sent to third parties"
msgid "None yet"
msgstr "None yet"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr "Not configured"
@ -3309,10 +3260,6 @@ msgstr "OAuth connection failed"
msgid "OAuth expired"
msgstr "OAuth expired"
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr "Offline"
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr "On"
@ -3433,10 +3380,6 @@ msgstr "Page"
msgid "PagerDuty incident"
msgstr "PagerDuty incident"
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr "Paper"
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "Park"
@ -3517,10 +3460,6 @@ msgstr "Polishing the details…"
msgid "Polling devices…"
msgstr "Polling devices…"
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr "Portfolio-Check"
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr "POST to"
@ -4030,10 +3969,6 @@ msgstr "Scanning exposure…"
msgid "Scene"
msgstr "Scene"
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr "Schlichtheit, Eleganz & Detailtiefe"
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr "Staying in my specialty…"
msgid "Steps"
msgstr "Steps"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr "Steve Jobs Audit"
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr "Tools"
msgid "Tracing the path…"
msgstr "Tracing the path…"
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr "Trading"
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr "Unavailable"
msgid "Uninstall"
msgstr "Uninstall"
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr "Unknown"
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr "Unlink"
@ -4836,14 +4759,6 @@ msgstr "Voice is not available yet. You can turn it on later in settings."
msgid "Voice set to {0}."
msgstr "Voice set to {0}."
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr "Volatilität & Orderbuch-Liquidität"
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr "Waiting"

View File

@ -46,11 +46,6 @@ msgstr "{0} (más de 10 MiB)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr ""
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr ""
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr ""
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} está trabajando"
msgid "/Users/you/projects/acme"
msgstr ""
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr ""
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr ""
@ -545,10 +529,6 @@ msgstr ""
msgid "Allowed once"
msgstr "Permitido una vez"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr ""
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "¿Ya tienes una cuenta?"
@ -620,10 +600,6 @@ msgstr "Aprobar"
msgid "Approve this server to let your agent use its tools."
msgstr "Aprueba este servidor para que tu agente use sus herramientas."
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "Archivar"
@ -2078,10 +2054,6 @@ msgstr ""
msgid "Drag, drop, or paste an image"
msgstr "Arrastra, suelta o pega una imagen"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "Duplicar"
@ -2094,10 +2066,6 @@ msgstr "p. ej. Hivenet Agent, Presales, bot de hojas de horas"
msgid "Earlier message"
msgstr "Mensaje anterior"
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr ""
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "Editar primero"
@ -2316,10 +2284,6 @@ msgstr "¿Olvidaste la contraseña?"
msgid "Forward"
msgstr ""
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr "Gratis"
@ -2396,10 +2360,6 @@ msgstr ""
msgid "Groups"
msgstr ""
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr ""
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr ""
@ -2598,10 +2558,6 @@ msgstr "Unidad del intervalo"
msgid "Isolated"
msgstr "Aislado"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr ""
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "Su conversación, archivos y rutinas se eliminarán de forma permanente. Los bots que creó permanecerán en tu lista."
@ -2697,10 +2653,6 @@ msgstr ""
msgid "Listening — what's the story?"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr ""
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "Cargar mensajes anteriores"
@ -3247,7 +3199,6 @@ msgstr ""
msgid "None yet"
msgstr "Ninguno aún"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr ""
@ -3309,10 +3260,6 @@ msgstr "Falló la conexión OAuth"
msgid "OAuth expired"
msgstr "OAuth expirado"
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr ""
@ -3433,10 +3380,6 @@ msgstr ""
msgid "PagerDuty incident"
msgstr "Incidente de PagerDuty"
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr ""
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "Aparcar"
@ -3517,10 +3460,6 @@ msgstr ""
msgid "Polling devices…"
msgstr ""
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr ""
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr "POST a"
@ -4030,10 +3969,6 @@ msgstr ""
msgid "Scene"
msgstr ""
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr ""
msgid "Steps"
msgstr "Pasos"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr ""
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr ""
msgid "Tracing the path…"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr ""
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr "No disponible"
msgid "Uninstall"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr "Desvincular"
@ -4836,14 +4759,6 @@ msgstr ""
msgid "Voice set to {0}."
msgstr ""
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr ""
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr ""
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr ""

View File

@ -46,11 +46,6 @@ msgstr "{0} (10 MiB से अधिक)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr ""
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr ""
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr ""
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} काम कर रहा है"
msgid "/Users/you/projects/acme"
msgstr ""
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr ""
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr ""
@ -545,10 +529,6 @@ msgstr ""
msgid "Allowed once"
msgstr "एक बार अनुमति दी गई"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr ""
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "पहले से खाता है?"
@ -620,10 +600,6 @@ msgstr "अनुमोदित करें"
msgid "Approve this server to let your agent use its tools."
msgstr "अपने एजेंट को इसके टूल उपयोग करने देने के लिए इस सर्वर को अनुमोदित करें।"
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "आर्काइव करें"
@ -2078,10 +2054,6 @@ msgstr ""
msgid "Drag, drop, or paste an image"
msgstr "छवि खींचें, छोड़ें या पेस्ट करें"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "डुप्लिकेट"
@ -2094,10 +2066,6 @@ msgstr "उदा. Hivenet Agent, Presales, टाइमशीट बॉट"
msgid "Earlier message"
msgstr "पुराना संदेश"
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr ""
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "पहले संपादित करें"
@ -2316,10 +2284,6 @@ msgstr "पासवर्ड भूल गए?"
msgid "Forward"
msgstr ""
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr ""
@ -2396,10 +2360,6 @@ msgstr ""
msgid "Groups"
msgstr ""
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr ""
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr ""
@ -2598,10 +2558,6 @@ msgstr "अंतराल इकाई"
msgid "Isolated"
msgstr "पृथक"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr ""
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।"
@ -2697,10 +2653,6 @@ msgstr ""
msgid "Listening — what's the story?"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr ""
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "पुराने संदेश लोड करें"
@ -3247,7 +3199,6 @@ msgstr ""
msgid "None yet"
msgstr "अभी तक कोई नहीं"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr ""
@ -3309,10 +3260,6 @@ msgstr "OAuth कनेक्शन विफल"
msgid "OAuth expired"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr ""
@ -3433,10 +3380,6 @@ msgstr ""
msgid "PagerDuty incident"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr ""
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "पार्क करें"
@ -3517,10 +3460,6 @@ msgstr ""
msgid "Polling devices…"
msgstr ""
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr ""
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr ""
@ -4030,10 +3969,6 @@ msgstr ""
msgid "Scene"
msgstr ""
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr ""
msgid "Steps"
msgstr "चरण"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr ""
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr ""
msgid "Tracing the path…"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr ""
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr ""
msgid "Uninstall"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr ""
@ -4836,14 +4759,6 @@ msgstr ""
msgid "Voice set to {0}."
msgstr ""
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr ""
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr ""
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr ""

View File

@ -46,11 +46,6 @@ msgstr "{0} (10 MiB 초과)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr ""
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr ""
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr ""
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} 작업 중"
msgid "/Users/you/projects/acme"
msgstr ""
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr ""
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr ""
@ -545,10 +529,6 @@ msgstr ""
msgid "Allowed once"
msgstr "한 번 허용됨"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr ""
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "이미 계정이 있나요?"
@ -620,10 +600,6 @@ msgstr "승인"
msgid "Approve this server to let your agent use its tools."
msgstr "이 서버를 승인하면 Bot이 제공되는 도구를 사용할 수 있습니다."
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "보관"
@ -2078,10 +2054,6 @@ msgstr ""
msgid "Drag, drop, or paste an image"
msgstr "이미지를 끌어다 놓거나 붙여넣으세요"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "복제"
@ -2094,10 +2066,6 @@ msgstr "예: Hivenet Agent, Presales, 근무표 봇"
msgid "Earlier message"
msgstr "이전 메시지"
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr ""
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "수정 후 보내기"
@ -2316,10 +2284,6 @@ msgstr "비밀번호를 잊으셨나요?"
msgid "Forward"
msgstr ""
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr "무료"
@ -2396,10 +2360,6 @@ msgstr ""
msgid "Groups"
msgstr ""
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr ""
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr ""
@ -2598,10 +2558,6 @@ msgstr "간격 단위"
msgid "Isolated"
msgstr "이 Bot만 사용"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr ""
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다."
@ -2697,10 +2653,6 @@ msgstr ""
msgid "Listening — what's the story?"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr ""
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "이전 메시지 불러오기"
@ -3247,7 +3199,6 @@ msgstr ""
msgid "None yet"
msgstr "아직 없음"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr ""
@ -3309,10 +3260,6 @@ msgstr "OAuth 연결 실패"
msgid "OAuth expired"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr ""
@ -3433,10 +3380,6 @@ msgstr ""
msgid "PagerDuty incident"
msgstr "PagerDuty 인시던트"
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr ""
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "보류"
@ -3517,10 +3460,6 @@ msgstr ""
msgid "Polling devices…"
msgstr ""
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr ""
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr "POST 대상"
@ -4030,10 +3969,6 @@ msgstr ""
msgid "Scene"
msgstr ""
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr ""
msgid "Steps"
msgstr "실행 단계"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr ""
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr ""
msgid "Tracing the path…"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr ""
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr "사용할 수 없음"
msgid "Uninstall"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr "연결 해제"
@ -4836,14 +4759,6 @@ msgstr ""
msgid "Voice set to {0}."
msgstr ""
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr ""
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr ""
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr ""

View File

@ -46,11 +46,6 @@ msgstr "{0} (acima de 10 MiB)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr ""
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr ""
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr ""
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} está trabalhando"
msgid "/Users/you/projects/acme"
msgstr ""
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr ""
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr ""
@ -545,10 +529,6 @@ msgstr ""
msgid "Allowed once"
msgstr "Permitido uma vez"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr ""
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "Você já tem cadastro?"
@ -620,10 +600,6 @@ msgstr "Aprovar"
msgid "Approve this server to let your agent use its tools."
msgstr "Aprove este servidor para permitir que seu agente use suas ferramentas."
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "Arquivar"
@ -2078,10 +2054,6 @@ msgstr ""
msgid "Drag, drop, or paste an image"
msgstr "Arraste, solte ou cole uma imagem"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "Duplicar"
@ -2094,10 +2066,6 @@ msgstr "ex.: Hivenet Agent, Presales, bot de planilhas"
msgid "Earlier message"
msgstr ""
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr ""
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "Editar primeiro"
@ -2316,10 +2284,6 @@ msgstr "Esqueceu a senha?"
msgid "Forward"
msgstr ""
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr ""
@ -2396,10 +2360,6 @@ msgstr ""
msgid "Groups"
msgstr ""
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr ""
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr ""
@ -2598,10 +2558,6 @@ msgstr "Unidade do intervalo:"
msgid "Isolated"
msgstr "Isolado"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr ""
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "Suas conversas, arquivos e rotinas serão excluídos permanentemente. Os bots criados ficam na sua lista."
@ -2697,10 +2653,6 @@ msgstr ""
msgid "Listening — what's the story?"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr ""
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "Carregar mensagens anteriores..."
@ -3247,7 +3199,6 @@ msgstr ""
msgid "None yet"
msgstr "Nenhuma ainda"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr ""
@ -3309,10 +3260,6 @@ msgstr "Falha na conexão OAuth"
msgid "OAuth expired"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr ""
@ -3433,10 +3380,6 @@ msgstr ""
msgid "PagerDuty incident"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr ""
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "Suspender"
@ -3517,10 +3460,6 @@ msgstr ""
msgid "Polling devices…"
msgstr ""
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr ""
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr ""
@ -4030,10 +3969,6 @@ msgstr ""
msgid "Scene"
msgstr ""
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr ""
msgid "Steps"
msgstr "Passos"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr ""
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr ""
msgid "Tracing the path…"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr ""
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr ""
msgid "Uninstall"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr ""
@ -4836,14 +4759,6 @@ msgstr ""
msgid "Voice set to {0}."
msgstr ""
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr ""
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr ""
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr ""

View File

@ -46,11 +46,6 @@ msgstr "{0} (более 10 МБ)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr ""
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr ""
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr ""
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} работает"
msgid "/Users/you/projects/acme"
msgstr ""
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr ""
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr ""
@ -545,10 +529,6 @@ msgstr ""
msgid "Allowed once"
msgstr "Разрешено один раз"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr ""
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "У вас уже есть аккаунт?"
@ -620,10 +600,6 @@ msgstr "Утвердить"
msgid "Approve this server to let your agent use its tools."
msgstr "Одобрите этот сервер, чтобы ваш агент мог использовать его инструменты."
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "Архивировать"
@ -2078,10 +2054,6 @@ msgstr ""
msgid "Drag, drop, or paste an image"
msgstr "Перетащите или вставьте изображение"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "Дублировать"
@ -2094,10 +2066,6 @@ msgstr "напр. Hivenet Agent, Presales, бот табелей"
msgid "Earlier message"
msgstr "Предыдущее сообщение"
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr ""
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "Сначала отредактируйте"
@ -2316,10 +2284,6 @@ msgstr "Забыли пароль?"
msgid "Forward"
msgstr ""
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr "Бесплатно"
@ -2396,10 +2360,6 @@ msgstr "Обновления группы"
msgid "Groups"
msgstr "Группы"
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr ""
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr ""
@ -2598,10 +2558,6 @@ msgstr "Единица интервала"
msgid "Isolated"
msgstr "изолированный"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr ""
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке."
@ -2697,10 +2653,6 @@ msgstr ""
msgid "Listening — what's the story?"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr ""
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "Загрузить более ранние сообщения"
@ -3247,7 +3199,6 @@ msgstr ""
msgid "None yet"
msgstr "Пока нет"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr ""
@ -3309,10 +3260,6 @@ msgstr "Соединение OAuth не удалось"
msgid "OAuth expired"
msgstr "Срок действия OAuth истек"
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr ""
@ -3433,10 +3380,6 @@ msgstr ""
msgid "PagerDuty incident"
msgstr "Инцидент PagerDuty"
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr ""
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "Приостановить"
@ -3517,10 +3460,6 @@ msgstr ""
msgid "Polling devices…"
msgstr ""
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr ""
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr "ОТПРАВИТЬ в"
@ -4030,10 +3969,6 @@ msgstr ""
msgid "Scene"
msgstr ""
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr ""
msgid "Steps"
msgstr "Шаги"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr ""
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr "Инструменты"
msgid "Tracing the path…"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr ""
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr "Недоступно"
msgid "Uninstall"
msgstr "Удалить"
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr "Отсоединить"
@ -4836,14 +4759,6 @@ msgstr ""
msgid "Voice set to {0}."
msgstr ""
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr ""
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr ""
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr ""

View File

@ -46,11 +46,6 @@ msgstr "{0} (10 MiB üzeri)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr ""
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr ""
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr ""
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} çalışıyor"
msgid "/Users/you/projects/acme"
msgstr ""
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr ""
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr ""
@ -545,10 +529,6 @@ msgstr ""
msgid "Allowed once"
msgstr "Bir kez izin verildi"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr ""
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "Zaten hesabın var mı?"
@ -620,10 +600,6 @@ msgstr "Onayla"
msgid "Approve this server to let your agent use its tools."
msgstr "Agent'ının bu sunucunun araçlarını kullanabilmesi için sunucuyu onayla."
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "Arşivle"
@ -2078,10 +2054,6 @@ msgstr ""
msgid "Drag, drop, or paste an image"
msgstr "Görüntüyü sürükleyip bırakın veya yapıştırın"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "Çoğalt"
@ -2094,10 +2066,6 @@ msgstr "örn. Hivenet Agent, Presales, puantaj botu"
msgid "Earlier message"
msgstr ""
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr ""
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "Önce düzenle"
@ -2316,10 +2284,6 @@ msgstr "Parolanızı mı unuttunuz?"
msgid "Forward"
msgstr ""
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr ""
@ -2396,10 +2360,6 @@ msgstr ""
msgid "Groups"
msgstr ""
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr ""
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr ""
@ -2598,10 +2558,6 @@ msgstr "Aralık birimi"
msgid "Isolated"
msgstr "Yalıtılmış"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr ""
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "Sohbeti, dosyaları ve rutinleri kalıcı olarak silinecek. Oluşturduğu botlar listende kalır."
@ -2697,10 +2653,6 @@ msgstr ""
msgid "Listening — what's the story?"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr ""
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "Önceki mesajları yükle"
@ -3247,7 +3199,6 @@ msgstr ""
msgid "None yet"
msgstr "Henüz yok"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr ""
@ -3309,10 +3260,6 @@ msgstr "OAuth bağlantısı başarısız"
msgid "OAuth expired"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr ""
@ -3433,10 +3380,6 @@ msgstr ""
msgid "PagerDuty incident"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr ""
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "Beklet"
@ -3517,10 +3460,6 @@ msgstr ""
msgid "Polling devices…"
msgstr ""
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr ""
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr ""
@ -4030,10 +3969,6 @@ msgstr ""
msgid "Scene"
msgstr ""
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr ""
msgid "Steps"
msgstr "Adımlar"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr ""
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr ""
msgid "Tracing the path…"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr ""
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr ""
msgid "Uninstall"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr ""
@ -4836,14 +4759,6 @@ msgstr ""
msgid "Voice set to {0}."
msgstr ""
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr ""
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr ""
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr ""

View File

@ -46,11 +46,6 @@ msgstr "{0}(超过 10 MiB)"
msgid "{0} {unitLabel}"
msgstr "{0} {unitLabel}"
#. placeholder {0}: subtitle.count
#: src/components/TradingModeIndicator.tsx
msgid "{0} adapters"
msgstr ""
#. placeholder {0}: block.name
#: src/pages/shell/message-cards.tsx
msgid "{0} connection"
@ -128,6 +123,11 @@ msgstr ""
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
msgid "{0}, main conversation"
msgstr ""
#. placeholder {0}: active.name
#: src/pages/Shell.tsx
msgid "{0}'s screen"
@ -273,22 +273,6 @@ msgstr "{workingBotName} 正在工作"
msgid "/Users/you/projects/acme"
msgstr ""
#: src/pages/Shell.tsx
msgid "⚡ Führe einen autonomen Portfolio- und Risiko-Check durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "💎 Führe ein Steve Jobs UX & Architektur-Audit durch"
msgstr ""
#: src/pages/Shell.tsx
msgid "📈 Scanne den Markt nach aktuellen Alpha-Signalen"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "1 adapter"
msgstr ""
#: src/components/stage/OoStage.tsx
msgid "1:1 conversation"
msgstr ""
@ -545,10 +529,6 @@ msgstr ""
msgid "Allowed once"
msgstr "已允许一次"
#: src/pages/Shell.tsx
msgid "Alpha-Signale"
msgstr ""
#: src/pages/Auth.tsx
msgid "Already have an account?"
msgstr "已有账户?"
@ -620,10 +600,6 @@ msgstr "批准"
msgid "Approve this server to let your agent use its tools."
msgstr "批准此服务器后,Agent 才能使用它的工具。"
#: src/pages/Shell.tsx
msgid "Architektur-Audit"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Archive"
msgstr "归档"
@ -2078,10 +2054,6 @@ msgstr ""
msgid "Drag, drop, or paste an image"
msgstr "拖放或粘贴图片"
#: src/components/TradingModeIndicator.tsx
msgid "Dry run"
msgstr ""
#: src/pages/BotContextMenu.tsx
msgid "Duplicate"
msgstr "复制"
@ -2094,10 +2066,6 @@ msgstr "例如 Hivenet Agent、Presales、工时表机器人"
msgid "Earlier message"
msgstr "更早的消息"
#: src/pages/Shell.tsx
msgid "Echtzeit-Risiko & Drawdown-Analyse"
msgstr ""
#: src/components/AskCard.tsx
msgid "Edit first"
msgstr "先编辑"
@ -2316,10 +2284,6 @@ msgstr "忘记密码?"
msgid "Forward"
msgstr ""
#: src/pages/Shell.tsx
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
msgid "Free"
msgstr "免费"
@ -2396,10 +2360,6 @@ msgstr ""
msgid "Groups"
msgstr ""
#: src/pages/Shell.tsx
msgid "Guten Tag."
msgstr ""
#: src/components/UserProfileSettings.tsx
msgid "Habits"
msgstr ""
@ -2598,10 +2558,6 @@ msgstr "间隔单位"
msgid "Isolated"
msgstr "隔离"
#: src/pages/Shell.tsx
msgid "ist bereit."
msgstr ""
#: src/pages/shell/dialogs.tsx
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。"
@ -2697,10 +2653,6 @@ msgstr ""
msgid "Listening — what's the story?"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Live Trading"
msgstr ""
#: src/pages/Shell.tsx
msgid "Load earlier messages"
msgstr "加载更早的消息"
@ -3247,7 +3199,6 @@ msgstr ""
msgid "None yet"
msgstr "暂无"
#: src/components/TradingModeIndicator.tsx
#: src/pages/PrivacySettingsOverlay.tsx
msgid "Not configured"
msgstr ""
@ -3309,10 +3260,6 @@ msgstr "OAuth 连接失败"
msgid "OAuth expired"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Offline"
msgstr ""
#: src/pages/VoiceSettingsOverlay.tsx
msgid "On"
msgstr ""
@ -3433,10 +3380,6 @@ msgstr ""
msgid "PagerDuty incident"
msgstr "PagerDuty 事件"
#: src/components/TradingModeIndicator.tsx
msgid "Paper"
msgstr ""
#: src/pages/ScratchpadSection.tsx
msgid "Park"
msgstr "搁置"
@ -3517,10 +3460,6 @@ msgstr ""
msgid "Polling devices…"
msgstr ""
#: src/pages/Shell.tsx
msgid "Portfolio-Check"
msgstr ""
#: src/pages/RoutineEditor.tsx
msgid "POST to"
msgstr "POST 到"
@ -4030,10 +3969,6 @@ msgstr ""
msgid "Scene"
msgstr ""
#: src/pages/Shell.tsx
msgid "Schlichtheit, Eleganz & Detailtiefe"
msgstr ""
#: src/pages/ModelSettingsOverlay.tsx
#: src/pages/PluginsOverlay.tsx
#: src/pages/Shell.tsx
@ -4392,10 +4327,6 @@ msgstr ""
msgid "Steps"
msgstr "步骤"
#: src/pages/Shell.tsx
msgid "Steve Jobs Audit"
msgstr ""
#: src/components/browser/InAppBrowser.tsx
#: src/pages/Shell.tsx
msgid "Stop"
@ -4651,10 +4582,6 @@ msgstr ""
msgid "Tracing the path…"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Trading"
msgstr ""
#: src/components/voice/VoiceStage.tsx
#: src/pages/Shell.tsx
msgid "Transcript"
@ -4692,10 +4619,6 @@ msgstr "不可用"
msgid "Uninstall"
msgstr ""
#: src/components/TradingModeIndicator.tsx
msgid "Unknown"
msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
msgid "Unlink"
msgstr "取消关联"
@ -4836,14 +4759,6 @@ msgstr ""
msgid "Voice set to {0}."
msgstr ""
#: src/pages/Shell.tsx
msgid "Volatilität & Orderbuch-Liquidität"
msgstr ""
#: src/pages/Shell.tsx
msgid "Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt."
msgstr ""
#: src/pages/ProjectSessionTimeline.tsx
msgid "Waiting"
msgstr ""

View File

@ -16,10 +16,8 @@ import { SuccessPop } from "../components/ai/primitives";
import { ComputersUnavailableHint } from "../components/ComputersUnavailableHint";
import { DesktopUpdateSection } from "../components/DesktopUpdates";
import { SoftwareUpdateSection } from "../components/SoftwareUpdateSection";
import { TradingModeIndicator } from "../components/TradingModeIndicator";
import { authClient } from "../lib/auth";
import { getActiveUiLocale, setUiLocale } from "../lib/i18n";
import type { TradingBotUiState } from "../lib/trading-bot-status";
import {
type AppearancePreference,
getUiAppearancePreference,
@ -35,25 +33,8 @@ export type SettingsGeneralProps = {
messagingEnabled?: boolean;
onOpenMessaging?: () => void;
isDeploymentOwner?: boolean;
tradingStatus?: TradingBotUiState;
};
export function LiveTradingBanner({ snapshot }: { snapshot: TradingBotUiState }) {
if (snapshot.state === "loading" || snapshot.state === "unconfigured") return null;
const live = snapshot.state === "ready" && snapshot.status.mode === "live";
return (
<section
className={`rounded-xl border px-2 py-2 ${
live ? "border-warning/30 bg-warning/5" : "border-border bg-card/40"
}`}
data-testid="live-trading-banner"
>
<TradingModeIndicator snapshot={snapshot} />
</section>
);
}
export function GeneralSettingsPanels({
email,
name,
@ -62,7 +43,6 @@ export function GeneralSettingsPanels({
messagingEnabled = false,
onOpenMessaging,
isDeploymentOwner = false,
tradingStatus,
}: SettingsGeneralProps) {
const { t } = useLingui();
const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale());
@ -98,7 +78,6 @@ export function GeneralSettingsPanels({
return (
<div className="space-y-5">
{tradingStatus ? <LiveTradingBanner snapshot={tradingStatus} /> : null}
<section className="rounded-xl border border-border px-4 py-4">
<h3 className="text-ui-body font-medium text-foreground">
<Trans>Account</Trans>

View File

@ -1,20 +1,31 @@
import { Trans } from "@lingui/react/macro";
import { Button } from "@rakazo/ui-web";
import { useEffect, useState } from "react";
import { IntegrationSetup } from "../components/integrations/IntegrationSetup";
import { Suspense, useEffect, useState } from "react";
import { desktopBridge } from "../lib/desktop";
import { lazyWithRecovery } from "../lib/lazy-recovery";
import { rpc } from "../lib/rpc";
import { ModelSettingsOverlay } from "./ModelSettingsOverlay";
const IntegrationSetup = lazyWithRecovery("integrations/IntegrationSetup", () =>
import("../components/integrations/IntegrationSetup").then((module) => ({
default: module.IntegrationSetup,
})),
);
const ModelSettingsOverlay = lazyWithRecovery("pages/ModelSettingsOverlay", () =>
import("./ModelSettingsOverlay").then((module) => ({ default: module.ModelSettingsOverlay })),
);
export function LocalSettingsPage() {
const [section, setSection] = useState<"models" | "integrations" | null>(null);
const hasLocalBridge = Boolean(desktopBridge()?.localSettings);
const [ready, setReady] = useState(false);
const [error, setError] = useState(false);
const [error, setError] = useState(!hasLocalBridge);
useEffect(() => {
if (!hasLocalBridge) return;
void rpc.integrationSetup
.get()
.then(() => setReady(true))
.catch(() => setError(true));
}, []);
}, [hasLocalBridge]);
return (
<main className="h-full overflow-auto bg-background px-6 py-12">
<div className="mx-auto max-w-xl space-y-6">
@ -43,9 +54,15 @@ export function LocalSettingsPage() {
</Button>
</nav>
{section === "models" ? (
<Suspense fallback={null}>
<ModelSettingsOverlay onClose={() => setSection(null)} localOwner />
</Suspense>
) : null}
{section === "integrations" ? (
<Suspense fallback={null}>
<IntegrationSetup serverSetup managedOnly />
</Suspense>
) : null}
{section === "integrations" ? <IntegrationSetup serverSetup managedOnly /> : null}
</>
) : null}
</div>

View File

@ -17,7 +17,6 @@ import {
import { type ComponentType, useEffect, useRef, useState } from "react";
import { computersAreUnavailable } from "../components/ComputersUnavailableHint";
import { UserProfileSettings } from "../components/UserProfileSettings";
import type { TradingBotUiState } from "../lib/trading-bot-status";
import {
ComputerSettingsPanel,
GeneralSettingsPanels,
@ -65,7 +64,6 @@ export function SettingsOverlay({
onVoiceStatusMaybeChanged,
activeBotId,
onOpenMcp,
tradingStatus,
}: {
email?: string | null;
name: string;
@ -83,7 +81,6 @@ export function SettingsOverlay({
onVoiceStatusMaybeChanged?: () => void | Promise<void>;
activeBotId?: string;
onOpenMcp?: () => void;
tradingStatus?: TradingBotUiState;
}) {
const { t } = useLingui();
const panelRef = useRef<HTMLDivElement>(null);
@ -238,7 +235,6 @@ export function SettingsOverlay({
onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined
}
isDeploymentOwner={isDeploymentOwner}
tradingStatus={tradingStatus}
/>
) : null}
{section === "integrations" ? (

View File

@ -139,7 +139,6 @@ import {
Search,
Settings,
Smile,
Sparkles,
Square,
TextQuote,
Trash2,
@ -178,11 +177,9 @@ import {
} from "../components/ComputersUnavailableHint";
import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress";
import { AgentCursor } from "../components/computer/AgentCursor";
import { DataTrustPanel } from "../components/DataTrustPanel";
import { MainInbox } from "../components/MainInbox";
import { MessageHoverMetadata } from "../components/MessageHoverMetadata";
import { OoGroupStage, OoStage } from "../components/stage/OoStage";
import { TradingModeIndicator } from "../components/TradingModeIndicator";
import { SkillDraftCard } from "../components/teach/SkillDraftCard";
import { TeachCaptureOverlay } from "../components/teach/TeachCaptureOverlay";
import { TeachComputerOverlayControl } from "../components/teach/TeachComputerOverlay";
@ -252,7 +249,6 @@ import {
threadRunError,
userHoldsComputerControl,
} from "../lib/thread-events";
import { useTradingBotStatus } from "../lib/trading-bot-status";
import {
transcriptCanSnapAfterFrame,
transcriptIsNearEnd,
@ -425,7 +421,6 @@ export function ShellPage() {
if (botId) return "team";
return "heute";
});
const tradingStatus = useTradingBotStatus();
useEffect(() => {
if (botId) setActiveNavPillar("team");
@ -1034,14 +1029,15 @@ export function ShellPage() {
}
return;
}
const inProject = Boolean(refreshSpaceId && refreshSpaceId !== refreshHomeSpaceId);
if (currentGroupId) {
if (!groupList.some((group) => group.id === currentGroupId)) {
navigate(firstThreadRoute(list, groupList), { replace: true });
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
}
return;
}
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
navigate(firstThreadRoute(list, groupList), { replace: true });
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
}
} finally {
botsRefreshInFlight.current -= 1;
@ -1278,12 +1274,17 @@ export function ShellPage() {
}
return;
}
const inProject = bootstrapSpaceId !== bootstrapHomeSpaceId;
if (groupId) {
if (!groupList.some((group) => group.id === groupId)) {
navigate(firstThreadRoute(bootstrap.bots, groupList), { replace: true });
navigate(firstThreadRoute(bootstrap.bots, groupList, inProject), { replace: true });
}
return;
}
if (inProject && !botId) {
navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true });
return;
}
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
if (selectedBotId && selectedBotId !== botId) {
navigate(`/app/${selectedBotId}`, { replace: true });
@ -1660,6 +1661,8 @@ export function ShellPage() {
: group.title,
showLock: showSpaceNames,
emptySpaceId: undefined as string | undefined,
/** A project without its main conversation yet offers to start it. */
startSpaceId: undefined as string | undefined,
spaceId: space.id,
spaceName: space.name,
canDeleteSpace: index === 0 && space.canDelete === true,
@ -1676,6 +1679,7 @@ export function ShellPage() {
sectionId: null,
showLock: true,
emptySpaceId: space.id,
startSpaceId: undefined,
spaceId: space.id,
spaceName: space.name,
canDeleteSpace: space.canDelete === true,
@ -1739,11 +1743,13 @@ export function ShellPage() {
if (space.id === homeSpaceId || seenProjectSpaceIds.has(space.id)) continue;
seenProjectSpaceIds.add(space.id);
const groupsInSpace = sidebarGroups.flatMap((group) =>
group.spaceId === space.id
? group.bots.filter((item) => item.kind === "group" && item.chat.kind !== "main")
: [],
group.spaceId === space.id ? group.bots.filter((item) => item.kind === "group") : [],
);
const projectBots = dedupeSidebarBots(groupsInSpace);
// The project's main conversation, where the user talks to its main agent, comes first.
const projectBots = dedupeSidebarBots(groupsInSpace).sort(
(left, right) => Number(right.chat.kind === "main") - Number(left.chat.kind === "main"),
);
const hasMain = projectBots.some((item) => item.chat.kind === "main");
entries.push({
key:
projectBots.length > 0 ? `project-space:${space.id}` : `project-space:${space.id}:empty`,
@ -1752,6 +1758,7 @@ export function ShellPage() {
sectionId: null,
showLock: true,
emptySpaceId: projectBots.length > 0 ? undefined : space.id,
startSpaceId: hasMain ? undefined : space.id,
spaceId: space.id,
spaceName: space.name,
canDeleteSpace: space.canDelete === true,
@ -1836,12 +1843,15 @@ export function ShellPage() {
[bootstrapMe?.spaceId, navigate],
);
const openProjectStart = useCallback(
(spaceId: string, flow: "create" | "group") => {
// A project is a conversation with its own main agent, created when first opened.
const startProject = useCallback(
async (spaceId: string) => {
setActiveNavPillar("projekte");
openSpaceChat(spaceId, `/app?project=start&flow=${flow}`);
const main = await rpc.spaces.ensureMain({ spaceId });
if (spaceId === (selectedSpaceId() ?? bootstrapMe?.spaceId)) await refreshBots();
openSpaceChat(spaceId, `/app/g/${main.groupId}`);
},
[openSpaceChat],
[bootstrapMe?.spaceId, openSpaceChat, refreshBots],
);
const openMainConversation = useCallback(() => {
@ -2085,36 +2095,6 @@ export function ShellPage() {
}
}, [botId, emptyProjectSpace, groupId, initialBotsLoaded]);
useEffect(() => {
if (searchParams.get("project") !== "start" || !emptyProjectSpace || !initialBotsLoaded) {
return;
}
setActiveNavPillar("projekte");
const flow = searchParams.get("flow");
setPanel(
flow === "create"
? "create"
: flow === "group" && homeSpaceBots.length > 0
? "create-group"
: flow === "group"
? "create"
: bots.length === 0
? "create"
: "create-group",
);
const next = new URLSearchParams(searchParams);
next.delete("project");
next.delete("flow");
setSearchParams(next, { replace: true });
}, [
bots.length,
emptyProjectSpace,
homeSpaceBots.length,
initialBotsLoaded,
searchParams,
setSearchParams,
]);
const activeSnapshot = inGroup
? snapshot?.groupId === groupId
? snapshot
@ -4007,7 +3987,7 @@ export function ShellPage() {
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
onClick={() => {
if (group.emptySpaceId) {
openSpaceChat(group.emptySpaceId, "/app");
void startProject(group.emptySpaceId);
return;
}
toggleSidebarSection(group.key);
@ -4090,14 +4070,14 @@ export function ShellPage() {
) : null}
</div>
) : null}
{!collapsed && group.emptySpaceId ? (
{!collapsed && group.startSpaceId ? (
<div className="px-3 pb-2">
<Button
variant="outline"
size="sm"
className="w-full text-ui-label"
data-testid={`project-start-${group.spaceId}`}
onClick={() => openProjectStart(group.spaceId, "group")}
onClick={() => void startProject(group.spaceId)}
>
<Plus className="mr-1 size-icon-sm" />
{t`Start`}
@ -4112,6 +4092,8 @@ export function ShellPage() {
aria-label={
item.kind === "bot"
? t`${item.chat.name}, 1:1 conversation`
: item.chat.kind === "main"
? t`${item.chat.name}, main conversation`
: t`${item.chat.name}, group conversation`
}
onClick={() => {
@ -4124,7 +4106,12 @@ export function ShellPage() {
);
}}
onContextMenu={(event) => {
if (item.chat.spaceId !== bootstrapMe?.spaceId) return;
// A project's main conversation has no chat actions.
if (
item.chat.spaceId !== bootstrapMe?.spaceId ||
item.chat.kind === "main"
)
return;
event.preventDefault();
botMenuAnchor.current = event.currentTarget;
setBotMenu({
@ -4279,8 +4266,6 @@ export function ShellPage() {
</div>
) : null}
</div>
<TradingModeIndicator snapshot={tradingStatus} className="mx-3 mb-1 lg:mx-5 2xl:mx-7" />
<DataTrustPanel />
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
<PopoverTrigger
data-testid="user-menu-trigger"
@ -4572,7 +4557,7 @@ export function ShellPage() {
<div className="grid flex-1 place-items-center">
<Button
data-testid="project-start-main"
onClick={() => currentSpaceId && openProjectStart(currentSpaceId, "create")}
onClick={() => currentSpaceId && void startProject(currentSpaceId)}
>
<Plus aria-hidden="true" className="size-icon-md" />
{t`Start`}
@ -4639,9 +4624,6 @@ export function ShellPage() {
activeName={
inGroup ? (activeGroup?.name ?? activeSnapshot?.groupName) : active?.name
}
onPromptSelect={(prompt) => {
void sendMessage(prompt);
}}
scrollRef={messageScroll}
artifactTarget={transcriptArtifactTarget}
onOpenArtifact={wideLayout ? openArtifactBeside : undefined}
@ -4690,9 +4672,6 @@ export function ShellPage() {
{(active || activeGroup) && !showProjectTimeline ? (
<Composer
key={inGroup ? `group:${groupId}` : `bot:${active?.id}`}
activeName={
inGroup ? (activeGroup?.name ?? activeSnapshot?.groupName) : active?.name
}
running={composerRunning}
disabled={Boolean(recordingSkill)}
pendingAttachments={activePendingAttachments}
@ -4956,7 +4935,10 @@ export function ShellPage() {
const remainingGroups = groups.filter((group) => group.id !== activeGroup.id);
setGroups(remainingGroups);
setPanel(null);
navigate(firstThreadRoute(bots, remainingGroups), { replace: true });
navigate(
firstThreadRoute(bots, remainingGroups, currentSpaceId !== homeSpaceId),
{ replace: true },
);
await refreshBots().catch(() => undefined);
}}
/>
@ -5567,7 +5549,6 @@ export function ShellPage() {
setSettingsOpen(false);
setMessagingSettingsOpen(true);
}}
tradingStatus={tradingStatus}
onAvatarStyleChange={async (avatarStyle) => {
const nextMe = await rpc.preferences.update({ avatarStyle });
setBootstrapMe(nextMe);
@ -5816,7 +5797,6 @@ const Transcript = memo(function Transcript({
speakingMessageId,
onSpeak,
activeName,
onPromptSelect,
encounterMode = false,
voiceEncounterActive = false,
}: {
@ -5847,7 +5827,6 @@ const Transcript = memo(function Transcript({
speakingMessageId: string | null;
onSpeak: (message: ThreadMessage) => void;
activeName?: string;
onPromptSelect?: (prompt: string) => void;
encounterMode?: boolean;
voiceEncounterActive?: boolean;
}) {
@ -6096,87 +6075,11 @@ const Transcript = memo(function Transcript({
}`}
>
{reactionView.visibleMessages.length === 0 && encounterMode ? (
<div className="flex flex-col gap-2 py-2">
<p className="text-ui-body-sm text-muted-foreground">
<p className="py-2 text-ui-body-sm text-muted-foreground">
{activeName
? t`Start the conversation — ${activeName} is on stage above.`
: t`Start the conversation.`}
</p>
<div className="flex flex-wrap gap-2">
{[t`Portfolio-Check`, t`Alpha-Signale`, t`Architektur-Audit`].map((label) => (
<button
key={label}
type="button"
onClick={() => onPromptSelect?.(label)}
className="rounded-full border border-border bg-muted/40 px-3 py-1 text-ui-body-sm text-foreground/85 hover:bg-accent"
>
{label}
</button>
))}
</div>
</div>
) : null}
{reactionView.visibleMessages.length === 0 && !encounterMode ? (
<div className="flex flex-1 flex-col items-center justify-center py-16 text-center select-none animate-in fade-in duration-500">
<div className="relative mb-5 flex items-center justify-center p-3">
<EvooMascot size={76} pose="greeting" interactive trackPointer="hover" />
</div>
<h2 className="text-3xl font-bold tracking-tight text-foreground mb-2">
{t`Guten Tag.`} evOo {t`ist bereit.`}
</h2>
<p className="max-w-md text-ui-body leading-relaxed text-foreground/85 mb-8 font-normal">
{activeName
? t`Frag ${activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews.`
: t`Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt.`}
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3.5 w-full max-w-2xl px-4">
<button
type="button"
onClick={() => {
onPromptSelect?.(t`⚡ Führe einen autonomen Portfolio- und Risiko-Check durch`);
}}
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
>
<span className="text-xl mb-2">⚡</span>
<span className="text-[14.5px] font-semibold text-foreground">
{t`Portfolio-Check`}
</span>
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Echtzeit-Risiko & Drawdown-Analyse`}
</span>
</button>
<button
type="button"
onClick={() => {
onPromptSelect?.(t`📈 Scanne den Markt nach aktuellen Alpha-Signalen`);
}}
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
>
<span className="text-xl mb-2">📈</span>
<span className="text-[14.5px] font-semibold text-foreground">
{t`Alpha-Signale`}
</span>
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Volatilität & Orderbuch-Liquidität`}
</span>
</button>
<button
type="button"
onClick={() => {
onPromptSelect?.(t`💎 Führe ein Steve Jobs UX & Architektur-Audit durch`);
}}
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
>
<span className="text-xl mb-2">💎</span>
<span className="text-[14.5px] font-semibold text-foreground">
{t`Steve Jobs Audit`}
</span>
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
{t`Schlichtheit, Eleganz & Detailtiefe`}
</span>
</button>
</div>
</div>
) : null}
{reactionView.visibleMessages.map((message) => {
if (!message.blocks.some((block) => !isToolActivityBlock(block))) return null;
@ -6421,7 +6324,6 @@ const QuoteSelectionButton = memo(function QuoteSelectionButton({
});
const Composer = memo(function Composer({
activeName,
running,
disabled,
pendingAttachments,
@ -6449,7 +6351,6 @@ const Composer = memo(function Composer({
onSlashOpen,
onSlashAction,
}: {
activeName?: string;
running: boolean;
disabled?: boolean;
pendingAttachments: PendingAttachment[];
@ -6758,28 +6659,6 @@ const Composer = memo(function Composer({
}`}
>
<div className="mx-auto w-full max-w-4xl">
{/* Quick Action Prompt Chips */}
{showComposerPlaceholder ? (
<div className="mb-3 flex items-center gap-2 overflow-x-auto no-scrollbar py-1">
<span className="flex items-center gap-1.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground shrink-0 px-2.5 py-1 rounded-full bg-muted/40 border border-border me-1 shadow-xs">
<Sparkles className="text-amber-400 size-icon-sm" />
Quick
</span>
{quickPromptsForBot(activeName).map((promptText) => (
<button
key={promptText}
type="button"
onClick={() => {
updateDraft(promptText);
focusComposer();
}}
className="shrink-0 rounded-full border border-border bg-muted/40 px-3.5 py-1.5 text-ui-body-sm font-medium text-foreground/85 hover:bg-accent hover:text-foreground transition-all shadow-sm active:scale-95 cursor-pointer"
>
{promptText}
</button>
))}
</div>
) : null}
{sendError || runError ? (
<div
ref={runErrorRef}
@ -7274,48 +7153,6 @@ function cleanRosterPreview(raw?: string | null): string {
return text;
}
function quickPromptsForBot(botName?: string): string[] {
const lower = (botName ?? "").toLowerCase();
if (lower.includes("chief")) {
return [
"⚡ Portfolio Health Check",
"📊 Risk & Drawdown Exposure",
"🔄 Team-Status synchronisieren",
"🎯 Tagesziel prüfen",
];
}
if (lower.includes("steve")) {
return [
"✨ UX & Design Audit",
"🚀 Product Architecture",
"💎 Schlichtheit & Eleganz",
"🔍 Konkurrenzanalyse",
];
}
if (lower.includes("apex") || lower.includes("quant") || lower.includes("trad")) {
return [
"📈 Alpha-Signale scannen",
"📉 Volatilitäts-Stresstest",
"💰 Live vs. Paper PnL",
"⚡ Orderbuch Liquidität",
];
}
if (
lower.includes("dev") ||
lower.includes("sec") ||
lower.includes("infra") ||
lower.includes("code")
) {
return [
"🛡️ Security & Vault Check",
"💻 Latenz- & Cluster-Status",
"🔍 Code Review & Refactor",
"🚨 Health & Error Logs",
];
}
return ["💡 Prioritäten für heute?", "⚡ Schneller Statusbericht", "📊 Performance-Metriken"];
}
function MessageHoverActions({
message,
side,
@ -7418,8 +7255,12 @@ function MessageHoverActions({
function firstThreadRoute(
bots: readonly Pick<Bot, "id">[],
groups: readonly Pick<Group, "id">[],
groups: readonly Pick<Group, "id" | "kind">[],
/** In a project the user lands in its main conversation. */
inProject = false,
): string {
const main = inProject ? groups.find((group) => group.kind === "main") : undefined;
if (main) return `/app/g/${main.id}`;
if (bots[0]) return `/app/${bots[0].id}`;
if (groups[0]) return `/app/g/${groups[0].id}`;
return "/app";

View File

@ -2,8 +2,6 @@
interface ImportMetaEnv {
readonly VITE_DEFAULT_UI_LOCALE?: string;
/** Optional Tradingbot origin. Empty uses the same-origin API proxy. */
readonly VITE_TRADING_BOT_URL?: string;
}
interface ImportMeta {

View File

@ -22,8 +22,7 @@
"!**/data",
"!**/*.astro",
"!**/apps/www",
"!**/.claude-handoff*",
"!**/screenshot*.cjs"
"!**/.claude-handoff*"
]
},
"formatter": {

138
deploy.sh
View File

@ -1,138 +0,0 @@
#!/usr/bin/env bash
# ==============================================================================
# evOo Platform — Hot-Reload & Physical Deployment Orchestrator
# Executive Lead: CTO Steve Jobs
# "Realität im Dateisystem vor Halluzination im Kopf."
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PORT="${PORT:-3088}"
PID_FILE="${SCRIPT_DIR}/.evoo_server.pid"
LOG_FILE="${SCRIPT_DIR}/deploy.log"
# Colors
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
log() {
echo -e "${CYAN}[evOo Deploy]${NC} $1"
}
success() {
echo -e "${GREEN}[✓ SUCCESS]${NC} $1"
}
warn() {
echo -e "${YELLOW}[⚠ WARNING]${NC} $1"
}
fail() {
echo -e "${RED}[✗ FAILED]${NC} $1" >&2
exit 1
}
echo "=========================================================="
echo " evOo Autonomous Platform — Hot-Reload & Deploy "
echo " CTO: Steve Jobs | Port: ${PORT} "
echo "=========================================================="
# ------------------------------------------------------------------------------
# STEP 1: Physical Reality & Filesystem Audit (Zero Phantom Policy)
# ------------------------------------------------------------------------------
log "Prüfe physische Existenz aller Kern-Artefakte..."
REQUIRED_FILES=(
"${SCRIPT_DIR}/ARCHITECTURE.md"
"${SCRIPT_DIR}/evoo.config.json"
"${SCRIPT_DIR}/evoo-platform/index.html"
"${SCRIPT_DIR}/evoo-platform/styles.css"
"${SCRIPT_DIR}/evoo-platform/app.js"
"${SCRIPT_DIR}/evoo-platform/server.js"
"${SCRIPT_DIR}/apex-trading/BLUEPRINT.md"
"${SCRIPT_DIR}/apex-trading/config/trading.config.json"
"${SCRIPT_DIR}/orchestrator/agents.json"
"${SCRIPT_DIR}/orchestrator/agent_runner.py"
)
for file in "${REQUIRED_FILES[@]}"; do
if [[ ! -f "$file" ]]; then
fail "Physische Datei fehlt im Dateisystem: $file. Abbruch! Vision-Schleife verhindert."
fi
done
success "100% Physische Dateisystem-Integrität bestätigt. Keine Phantom-Dateien."
# If user only wanted verification
if [[ "${1:-}" == "--check-only" ]]; then
success "Check-only abgeschlossen. Alles physisch vorhanden."
exit 0
fi
# ------------------------------------------------------------------------------
# STEP 2: Process Management / Hot-Reload
# ------------------------------------------------------------------------------
if [[ -f "$PID_FILE" ]]; then
OLD_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then
log "Beende vorherige Server-Instanz (PID: $OLD_PID) für Hot-Reload..."
kill "$OLD_PID" 2>/dev/null || true
sleep 0.5
fi
rm -f "$PID_FILE"
fi
# Also ensure port is free
OCCUPIED_PID="$(lsof -ti tcp:"$PORT" 2>/dev/null || true)"
if [[ -n "$OCCUPIED_PID" ]]; then
log "Port $PORT bereits belegt von PID $OCCUPIED_PID. Bereinige..."
kill -9 $OCCUPIED_PID 2>/dev/null || true
sleep 0.5
fi
# ------------------------------------------------------------------------------
# STEP 3: Server Start
# ------------------------------------------------------------------------------
log "Starte evOo Platform Server als robuster Daemon auf Port ${PORT}..."
export PORT
if [[ "${1:-}" == "serve" || "${1:-}" == "--foreground" ]]; then
echo "$$" > "$PID_FILE"
log "Server läuft im Vordergrund (Strg+C zum Beenden)..."
exec node "${SCRIPT_DIR}/evoo-platform/server.js"
fi
# True detached POSIX daemon via setsid
python3 -c "import subprocess; subprocess.Popen(['node', '${SCRIPT_DIR}/evoo-platform/server.js'], cwd='${SCRIPT_DIR}', start_new_session=True, stdout=open('${LOG_FILE}', 'a'), stderr=subprocess.STDOUT)"
# Wait up to 5 seconds for healthcheck
log "Führe Healthcheck durch (http://127.0.0.1:${PORT}/health)..."
for i in {1..10}; do
if curl -s "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
HEALTH_RES="$(curl -s "http://127.0.0.1:${PORT}/health")"
success "Platform Server antwortet: HTTP 200 OK!"
echo -e "${CYAN}Health Response:${NC} $HEALTH_RES"
break
fi
if [[ $i -eq 10 ]]; then
fail "Server antwortet nicht innerhalb des Timeouts! Überprüfe $LOG_FILE"
fi
sleep 0.5
done
# ------------------------------------------------------------------------------
SERVER_PID="$(cat "$PID_FILE" 2>/dev/null || echo "active")"
echo ""
echo -e "${GREEN}==========================================================${NC}"
echo -e "${GREEN} evOo PLATFORM ERFOLGREICH DEPLOYED & LIVE! ${NC}"
echo -e "${GREEN}==========================================================${NC}"
echo -e " 🌐 Dashboard: ${CYAN}http://127.0.0.1:${PORT}/${NC}"
echo -e " 💓 Healthcheck: ${CYAN}http://127.0.0.1:${PORT}/health${NC}"
echo -e " 📊 Telemetrie API: ${CYAN}http://127.0.0.1:${PORT}/api/status${NC}"
echo -e " 📑 Verfassung: ${CYAN}${SCRIPT_DIR}/ARCHITECTURE.md${NC}"
echo -e " 📈 Apex Trading: ${CYAN}${SCRIPT_DIR}/apex-trading/BLUEPRINT.md${NC}"
echo -e " ⚙️ PID: ${SERVER_PID}"
echo -e "${GREEN}==========================================================${NC}"

View File

@ -1,5 +1,4 @@
# evOo-local — Agenten-Interface only
Primary: multi-agent / Oo UI at `/app/`.
Not primary: Mission Control, Cockpit, trading dashboards, old HTML evoo-platform.
Spelling: **evOo** (letter O).

View File

@ -102,10 +102,7 @@ Then connect **Kokoro** in Voice settings. See [kokoro-voice.md](./kokoro-voice.
| Tool | Path |
|------|------|
| self_improve | `scripts/self_improve.py` |
| autonomous-sentinel | `scripts/autonomous-sentinel.ts` |
| self-update | `packages/core/src/self-update.ts` |
| orchestrator | `orchestrator/agent_runner.py`, `orchestrator/agents.json` |
| local-stack | `apps/desktop/src/local-stack.ts` |
| pi-local-provider | `packages/adapters/src/pi-local-provider.ts` |
| seed-ollama | `scripts/seed-ollama.ts` |
@ -117,12 +114,3 @@ Display brand is **evOo**. Workspace package names remain `@rakazo/api`, `@rakaz
## Source
Clean history cut from `cynarAI/evoo` commit `f00577b` on branch `antigravity-handoff/project-timeline` (Voice First + project timeline). This repo does **not** carry the old multi-remote history.
## Old folders (do not delete from this doc — cleanup later on the Mac)
- `~/Desktop/evoo` (or Desktop/evOo)
- `~/awesome-grok-bot`
- `~/evOo`
- `~/Projects/evOo` (if present)
Keep them until you confirm `~/Projects/evOo-local` works.

View File

@ -1,52 +0,0 @@
# Voice First / Executive Board — Review Report
**Audience:** Ingo Wagner
**Branch:** `antigravity-handoff/project-timeline` @ `8d0b8e3` (PRs #33–#40 merged + follow-up fixes)
**Date:** 2026-09-19 (Europe/Vienna)
**Verifier host:** evOo Linux box (Tailscale `grokbot-box`). Ingo’s Mac was **not** SSH-reachable from this host (no key / Tailscale SSH off).
## Definition of done checklist
| # | Criterion | Result | Evidence |
|---|-----------|--------|----------|
| 1 | Dev stack healthy: API `:3100/health` + web `:5173` | **BLOCKED (Mac)** | Mac mini / MacBook Air online on Tailscale; ports 3100/5173 not exposed; SSH `Permission denied (publickey)`. Could not restart `pnpm dev` on `/Users/ingowagner/evOo`. |
| 2 | Executive Board: user VOICE FIRST turn → UX Designer replies with concept thoughts + ≥1 question; no raw `handoff_to_bot` JSON; no bounce to Boss | **PASS (unit / code)** · **UNVERIFIED (live)** | PRs #39–#40 + `bot-reply-sanitize`, `prose-handoff-recovery`, `group-handoff` workshop instructions. 56 focused tests green. Live group chat not exercised (no Mac API). |
| 3 | Transcript: no tool JSON blobs / no `assistant:` role leaks | **PASS (unit)** · **UNVERIFIED (live)** | `sanitizeBotReplyText` strips prose tool JSON + role prefix; `speechFromBlocks` skips tool-activity + unspeakable blobs (regression fixed after merge). |
| 4 | Voice Mode (CallView) UX Designer: invite before mic when owning work; after outgoing handoff mic stays closed + transfer line | **PASS (code + unit)** · **UNVERIFIED (live)** | CallView rebuilt: `speakUserTurnInvitation` / `handleSpeechIdle` / `shouldSpeakOpeningInvitation`; `parkAfterHandoff` + `voiceHandoffTransferLine`; mount no longer auto-opens mic. |
## What was done on this pass
1. Merged open PR heads **#33–#40** into `antigravity-handoff/project-timeline` and **pushed** to origin.
2. Fixed merge regression in `packages/core/src/speech-text.ts`: `speechFromBlocks` again drops tool-activity chips and unspeakable tool JSON (test was failing after `-X theirs` merge).
3. Fixed merge regression in `apps/web/src/pages/CallView.tsx`: invitation-before-mic restored; outgoing handoff keeps mic closed and speaks transfer line.
4. Focused vitest: **56 passed** (`call-view-voice`, `voice-handoff`, `speech-text`, `bot-reply-sanitize`, `prose-handoff-recovery`).
## Remaining gaps (for Ingo’s Mac)
1. **SSH / Tailscale SSH** from agent hosts to `macbook-air-von-ingo` / `mac-mini-von-ingo` so localhost verify can run without manual UI.
2. On Mac repo path `/Users/ingowagner/evOo`:
```bash
git fetch origin && git checkout antigravity-handoff/project-timeline && git pull
pnpm install && pnpm dev # ensure :3100 + :5173
```
3. Live Executive Board check (RPC/DB preferred):
- Find Executive Board `chat_groups` id.
- Post a fresh user turn about **VOICE FIRST AI Talks**.
- Confirm UX Designer message: concept prose + question; no `handoff_to_bot` JSON; Boss not re-tasked with the same work.
4. Open Voice Mode on UX Designer: hear invite before mic; after a specialist handoff, mic stays closed with transfer caption.
## Sample expected UX Designer reply shape (sanitized)
> I’ve been thinking about VOICE FIRST AI Talks as a short, spoken workshop: one specialist owns the lane, invites you before the mic opens, and never dumps the same task back upstairs. For the first talk, I’d keep it to three beats — hook, one demo, one ask.
> **Which audience should we design for first — founders, designers, or internal execs?**
## Group id tested
*None live — Mac localhost unreachable.* After Mac verify, record `chat_groups.id` here.
## Branch / PR notes
- Timeline branch now contains merges of `cursor/voice-mic-energy-2d2c` … `cursor/handoff-prose-sanitize-d1d4` plus commits:
- `fix(core): drop tool-activity and unspeakable blobs from speechFromBlocks`
- `fix(web): restore invite-before-mic and keep handoff mic closed`
- Draft PRs #33–#40 can be closed or marked superseded once this branch is the review source of truth.

View File

@ -1,573 +0,0 @@
// evOo Platform - Executive Mission Control Cockpit (app.js)
// Zero-Mockup Reactive Engine for Ingo Wagner & Autonomous Teammates
document.addEventListener("DOMContentLoaded", () => {
// Elements
const terminal = document.getElementById("terminal-logs");
const btnClear = document.getElementById("btn-clear-console");
const btnToggleConsole = document.getElementById("btn-toggle-console");
const cardConsole = document.querySelector(".card-console");
const btnSelfImprove = document.getElementById("btn-self-improve");
const btnKillswitch = document.getElementById("btn-killswitch");
const killswitchText = document.getElementById("killswitch-text");
const btnHotReload = document.getElementById("btn-hot-reload");
const btnOpenSteveChat = document.getElementById("btn-open-steve-chat");
const clockEl = document.getElementById("timestamp-clock");
const systemHealthVal = document.getElementById("system-health-val");
const _clusterSummaryVal = document.getElementById("cluster-summary-val");
const tradingSummaryVal = document.getElementById("trading-summary-val");
const approvalsContainer = document.getElementById("approvals-container");
const approvalsBadge = document.getElementById("approvals-badge");
const journalContainer = document.getElementById("journal-container");
const agentItems = document.querySelectorAll(".interactive-agent");
const _pnlVal = document.getElementById("pnl-val");
const _drawdownVal = document.getElementById("drawdown-val");
const _circuitBreakerVal = document.getElementById("circuit-breaker-val");
const tradingBadge = document.getElementById("trading-badge");
// Clock
function updateClock() {
const now = new Date();
clockEl.textContent = `${now.toISOString().replace("T", " ").substring(0, 19)} UTC`;
}
setInterval(updateClock, 1000);
updateClock();
// Helper: Strip ANSI color codes
const ansiCsi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
function cleanAnsi(str) {
return str.replace(ansiCsi, "");
}
// Logger
function addLog(text, type = "info") {
if (!terminal) return;
const line = document.createElement("div");
const cleanText = cleanAnsi(text);
let lineType = type;
if (
cleanText.includes("[✓ PASS]") ||
cleanText.includes("[PASS]") ||
cleanText.includes("SUCCESS") ||
cleanText.includes("200 OK")
) {
lineType = "success";
} else if (
cleanText.includes("[✗ FAIL]") ||
cleanText.includes("FAILED") ||
cleanText.includes("Error")
) {
lineType = "error";
} else if (cleanText.includes("[⚠ WARN]") || cleanText.includes("WARN")) {
lineType = "warning";
}
line.className = `log-line ${lineType}`;
const timestamp = new Date().toTimeString().split(" ")[0];
line.textContent = `[${timestamp}] ${cleanText}`;
terminal.appendChild(line);
terminal.scrollTop = terminal.scrollHeight;
}
// Console Toggles
if (btnClear) {
btnClear.addEventListener("click", () => {
terminal.innerHTML = "";
addLog("Terminal gelöscht. Überwachung aktiv.", "info");
});
}
if (btnToggleConsole && cardConsole) {
btnToggleConsole.addEventListener("click", () => {
const isCollapsed = cardConsole.classList.toggle("collapsed");
btnToggleConsole.textContent = isCollapsed ? "Ausklappen" : "Minimieren";
});
}
// 1. Fetch & Render Approvals
async function loadApprovals() {
if (!approvalsContainer) return;
try {
const res = await fetch("/api/approvals");
if (!res.ok) return;
const data = await res.json();
const list = data.approvals || [];
const pending = list.filter((a) => a.status === "pending");
if (approvalsBadge) {
approvalsBadge.textContent =
pending.length > 0 ? `${pending.length} Ausstehend` : "Alle erledigt";
approvalsBadge.className = pending.length > 0 ? "badge badge-amber" : "badge badge-emerald";
}
approvalsContainer.innerHTML = "";
if (list.length === 0) {
approvalsContainer.innerHTML =
'<div class="text-dim p-3">Keine ausstehenden Entscheidungen. System läuft autonom.</div>';
return;
}
list.forEach((item) => {
const div = document.createElement("div");
div.className = "approval-item";
div.setAttribute("data-id", item.id);
const isDecided = item.status !== "pending";
const statusClass = item.status === "approved" ? "badge-emerald" : "badge-amber";
div.innerHTML = `
<div class="approval-meta">
<span class="approval-type-tag">${item.type}</span>
<span class="approval-author">${item.author}</span>
<span class="approval-time">${item.timestamp}</span>
${isDecided ? `<span class="badge ${statusClass}">${item.status.toUpperCase()}</span>` : ""}
</div>
<div class="approval-title">${item.title}</div>
<div class="approval-desc">${item.details}</div>
<div class="approval-metrics">
<span>Conviction: <strong>${item.conviction}%</strong></span>
<span>R:R: <strong>${item.riskReward}</strong></span>
<span>Umfang: <strong>${item.size}</strong></span>
</div>
${
!isDecided
? `
<div class="approval-actions">
<button class="btn-approve" data-id="${item.id}" data-action="approve">✓ Genehmigen</button>
<button class="btn-veto" data-id="${item.id}" data-action="veto">✗ Veto / Ablehnen</button>
<button class="btn-discuss" data-author="${item.author}">💬 Im Chat besprechen</button>
</div>
`
: ""
}
`;
approvalsContainer.appendChild(div);
});
// Attach handlers to actions
approvalsContainer.querySelectorAll(".btn-approve, .btn-veto").forEach((btn) => {
btn.addEventListener("click", async (e) => {
const id = e.target.getAttribute("data-id");
const action = e.target.getAttribute("data-action");
try {
const resp = await fetch("/api/approvals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id, action }),
});
if (resp.ok) {
addLog(
`[DECISION] Entscheidung '${action.toUpperCase()}' für '${id}' physisch registriert.`,
"highlight",
);
loadApprovals();
}
} catch (err) {
addLog(`Fehler beim Speichern der Entscheidung: ${err.message}`, "error");
}
});
});
approvalsContainer.querySelectorAll(".btn-discuss").forEach((btn) => {
btn.addEventListener("click", () => {
const author = btn.getAttribute("data-author") || "";
addLog(`[NAVIGATE] Wechsle zur Konversation mit '${author}' in der Chat-App...`, "info");
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: "open_team_tab", botAuthor: author }, "*");
}
});
});
} catch (e) {
console.error("Approvals fetch error", e);
}
}
// 2. Fetch & Render Overnight Journal
async function loadJournal() {
if (!journalContainer) return;
try {
const res = await fetch("/api/journal");
if (!res.ok) return;
const data = await res.json();
const events = data.events || [];
journalContainer.innerHTML = "";
events.forEach((ev) => {
const row = document.createElement("div");
row.className = "journal-entry";
row.innerHTML = `
<div class="entry-time">${ev.time}</div>
<div class="entry-content">
<div class="entry-header">
<span class="entry-agent-chip">${ev.agent}</span>
<span class="entry-title">${ev.title}</span>
</div>
<div class="entry-detail">${ev.detail}</div>
</div>
`;
journalContainer.appendChild(row);
});
} catch (e) {
console.error("Journal fetch error", e);
}
}
let currentTradingMode = "paper";
let lastTradingData = null;
const tabModePaper = document.getElementById("tab-mode-paper");
const tabModeLive = document.getElementById("tab-mode-live");
const card1Label = document.getElementById("card1-label");
const card1Val = document.getElementById("card1-val");
const card1Sub = document.getElementById("card1-sub");
const card2Label = document.getElementById("card2-label");
const card2Val = document.getElementById("card2-val");
const card2Sub = document.getElementById("card2-sub");
const card3Label = document.getElementById("card3-label");
const card3Val = document.getElementById("card3-val");
const card3Sub = document.getElementById("card3-sub");
const card4Label = document.getElementById("card4-label");
const card4Val = document.getElementById("card4-val");
const card4Sub = document.getElementById("card4-sub");
function renderTradingView() {
if (!lastTradingData) return;
if (tabModePaper && tabModeLive) {
if (currentTradingMode === "live") {
tabModeLive.classList.add("active");
tabModePaper.classList.remove("active");
} else {
tabModePaper.classList.add("active");
tabModeLive.classList.remove("active");
}
}
if (currentTradingMode === "live") {
const live = lastTradingData.live || {};
const poly = live.polymarket || {};
const bp = live.bitpanda || {};
const fiat = live.fiat_reserves || {};
if (card1Label) card1Label.textContent = "Polymarket CLOB V2";
if (card1Val) card1Val.textContent = `$${(poly.pusd_balance || 639.39).toFixed(2)} pUSD`;
if (card1Sub)
card1Sub.textContent = `+ ${(poly.pol_gas || 58.25).toFixed(1)} POL Gas · Deposit Wallet`;
if (card2Label) card2Label.textContent = "Bitpanda Fusion";
if (card2Val) card2Val.textContent = `€${(bp.eur_cash || 46.09).toFixed(2)} EUR`;
if (card2Sub)
card2Sub.textContent = `+ ${(bp.pol || 436).toFixed(0)} POL, ${(bp.near || 12.6).toFixed(1)} NEAR, ${(bp.usdc || 10.3).toFixed(1)} USDC`;
if (card3Label) card3Label.textContent = "Bank-Liquidität (Fiat)";
if (card3Val)
card3Val.textContent = `€${(fiat.total_eur || 8500).toLocaleString("de-DE", { minimumFractionDigits: 2 })}`;
if (card3Sub) card3Sub.textContent = `N26 (€5.000) + Revolut (€3.500)`;
if (card4Label) card4Label.textContent = "Gesamtes Live-Kapital";
if (card4Val) {
card4Val.textContent = `$${(live.total_live_equity_usd || 10074.65).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
card4Val.className = "tmetric-val positive";
}
if (card4Sub)
card4Sub.textContent = `€${(live.total_live_equity_eur || 9285.39).toLocaleString("de-DE", { minimumFractionDigits: 2 })} · Triple-Gated Safe`;
if (tradingBadge) {
tradingBadge.textContent = "🟢 ECHTGELD: Polymarket + Bitpanda";
tradingBadge.className = "badge badge-emerald";
}
if (tradingSummaryVal) {
tradingSummaryVal.textContent = `LIVE ECHTGELD ($${(poly.pusd_balance || 639.39).toFixed(0)} pUSD + Bitpanda + €8.5k Fiat)`;
}
} else {
// Paper Mode
const paper = lastTradingData.paper || {};
if (card1Label) card1Label.textContent = "Equity & Portfolio (Paper)";
if (card1Val) {
card1Val.textContent = `$${(paper.total_equity_usd || 1000).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
card1Val.className = "tmetric-val positive";
}
if (card1Sub)
card1Sub.textContent = `€${(paper.total_cash_eur || 921.66).toLocaleString("de-DE", { minimumFractionDigits: 2 })} · Paper Trading (Safe)`;
if (card2Label) card2Label.textContent = "Max Drawdown";
if (card2Val) {
card2Val.textContent = paper.max_drawdown || "0.00%";
card2Val.className = "tmetric-val";
}
if (card2Sub) card2Sub.textContent = "Cap: 1.50% (Hard Stop)";
if (card3Label) card3Label.textContent = "Aktives Markt-Regime";
if (card3Val) {
card3Val.textContent = "Donchian / ADX";
card3Val.className = "tmetric-val cyan-text";
}
if (card3Sub) card3Sub.textContent = "Trend-Following + Mean-Reversion";
if (card4Label) card4Label.textContent = "Circuit Breaker Status";
if (card4Val) {
card4Val.textContent = paper.circuit_breaker || "ARMED";
card4Val.className = "tmetric-val success-val";
}
if (card4Sub) card4Sub.textContent = "Fixed Fractional (1-2%)";
if (tradingBadge) {
tradingBadge.textContent = "REST API: Port 8001 (Paper)";
tradingBadge.className = "badge badge-cyan";
}
if (tradingSummaryVal) {
tradingSummaryVal.textContent = `PAPER (ARMED · $${(paper.total_equity_usd || 1000).toFixed(0)})`;
}
}
if (btnKillswitch && killswitchText) {
if (lastTradingData.kill_switch_active) {
btnKillswitch.className = "btn btn-warning halted";
killswitchText.textContent = "Risk Guard: HALTED";
} else {
btnKillswitch.className = "btn btn-warning";
killswitchText.textContent = "Risk Guard: Aktiv";
}
}
}
// 3. Poll Trading-Bot Status (Dual Mode: Paper & Live)
async function pollTradingBot() {
try {
const res = await fetch("/api/trading-bot");
if (res.ok) {
const data = await res.json();
lastTradingData = data;
if (data.active_mode) {
currentTradingMode = data.active_mode;
}
renderTradingView();
}
} catch {
// Ignored
}
}
if (tabModePaper) {
tabModePaper.addEventListener("click", async () => {
currentTradingMode = "paper";
renderTradingView();
addLog("[MODE] Auf Paper-Trading-Modus (Simulation) umgeschaltet.", "info");
try {
await fetch("/api/trading-bot/mode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode: "paper" }),
});
loadJournal();
} catch {}
});
}
if (tabModeLive) {
tabModeLive.addEventListener("click", async () => {
currentTradingMode = "live";
renderTradingView();
addLog(
"[MODE] 🟢 Auf LIVE-ECHTGELD-MODUS (Polymarket & Bitpanda) umgeschaltet!",
"highlight",
);
try {
await fetch("/api/trading-bot/mode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode: "live" }),
});
loadJournal();
} catch {}
});
}
// 4. Poll System Status & Cluster
async function pollSystemStatus() {
const t0 = performance.now();
try {
const res = await fetch("/api/status");
const rtt = Math.round(performance.now() - t0);
if (res.ok) {
const data = await res.json();
if (systemHealthVal) {
systemHealthVal.textContent = `HEALTHY (Port 3088 · ${rtt}ms · RAM ${data.memory_rss_mb || 0}MB)`;
}
}
} catch {
if (systemHealthVal) systemHealthVal.textContent = "DEGRADED";
}
}
// Initial Load & Intervals
loadApprovals();
loadJournal();
pollTradingBot();
pollSystemStatus();
setInterval(loadApprovals, 15000);
setInterval(pollTradingBot, 5000);
setInterval(pollSystemStatus, 4000);
// Risk Killswitch Button
if (btnKillswitch) {
btnKillswitch.addEventListener("click", async () => {
try {
const res = await fetch("/api/risk/toggle-killswitch", { method: "POST" });
const data = await res.json();
addLog(
`[RISK GUARD] Notfall-Circuit-Breaker Zustand geändert: ${data.killSwitchActive ? "HALTED" : "ARMED"}`,
data.killSwitchActive ? "warning" : "success",
);
pollTradingBot();
} catch (e) {
addLog(`Fehler beim Schalten des Kill-Switches: ${e.message}`, "error");
}
});
}
// 5. Interactive Agent Roster Clicks (Physical Audit Dispatch)
agentItems.forEach((item) => {
item.addEventListener("click", async () => {
const agentId = item.getAttribute("data-agent");
const dot = item.querySelector(".status-dot");
const agentName = item.querySelector(".agent-name")?.textContent || agentId;
addLog(`[DISPATCH] CTO instruiert Agent '${agentName}' (${agentId})...`, "highlight");
if (dot) dot.style.opacity = "0.35";
try {
const res = await fetch("/api/agents/dispatch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent: agentId }),
});
if (res.ok) {
const response = await res.json();
const r = response.data?.result || response.data || {};
const isSuccess = response.data?.success ?? response.success;
if (isSuccess) {
addLog(
`[${agentName}] Task '${response.data?.task}' erfolgreich ausgeführt (${response.data?.duration_seconds}s).`,
"success",
);
if (agentId === "devops") {
addLog(
`[DevOps] PID: ${r.pid} | Port 3088: ${r.port_3088_status} | Health: ${r.status}`,
"info",
);
} else if (agentId === "architect") {
addLog(`[Architect] ${r.findings} Status: ${r.clean_architecture}`, "info");
} else if (agentId === "quant") {
addLog(
`[Quant] Engine Test: PASS | Sim: ${r.simulation?.simulation_status} ${r.simulation?.action || ""}`,
"info",
);
} else if (agentId === "ux") {
addLog(`[UX Designer] ${r.findings} Status: ${r.design_system_integrity}`, "info");
}
if (dot) {
dot.style.opacity = "1";
dot.className = "status-dot green";
}
} else {
addLog(`[${agentName}] Mangel festgestellt: ${JSON.stringify(r)}`, "warning");
if (dot) {
dot.style.opacity = "1";
dot.className = "status-dot";
dot.style.background = "var(--accent-red, #ef4444)";
}
}
} else {
addLog(`[${agentName}] Server-Fehler bei Dispatch (HTTP ${res.status})`, "error");
}
} catch (e) {
addLog(`[${agentName}] Dispatch-Fehler: ${e.message}`, "error");
}
});
});
// 6. Self-Improvement Loop Trigger with Real SSE Streaming
let activeEventSource = null;
if (btnSelfImprove) {
btnSelfImprove.addEventListener("click", async () => {
if (activeEventSource) {
activeEventSource.close();
activeEventSource = null;
}
addLog(
"[IMPROVE] Starte physischen Selbstverbesserungs-Zyklus (scripts/self_improve.py)...",
"highlight",
);
try {
const eventSource = new EventSource("/api/self-improve?stream=true");
activeEventSource = eventSource;
eventSource.onmessage = (event) => {
try {
const payload = JSON.parse(event.data);
if (payload.type === "log") {
addLog(payload.msg, "info");
} else if (payload.type === "error") {
addLog(payload.msg, "error");
} else if (payload.type === "done") {
addLog(
`[IMPROVE] Zyklus beendet mit Exit-Code ${payload.exitCode}.`,
payload.exitCode === 0 ? "success" : "error",
);
eventSource.close();
activeEventSource = null;
loadJournal();
}
} catch {
addLog(event.data, "info");
}
};
eventSource.onerror = () => {
addLog("[IMPROVE] SSE Stream beendet oder getrennt.", "info");
eventSource.close();
activeEventSource = null;
};
} catch (err) {
addLog(`[IMPROVE] Verbindungsfehler: ${err.message}`, "error");
}
});
}
// Hot-Reload Trigger
if (btnHotReload) {
btnHotReload.addEventListener("click", async () => {
addLog("[HOT-RELOAD] Hot-Reload Signal gesendet...", "highlight");
try {
await fetch("/api/reload", { method: "POST" }).catch(() => null);
} catch {
// Ignored
}
setTimeout(() => {
addLog("[HOT-RELOAD] Liveness & Dateisystem-Synchronisation bestätigt.", "success");
pollSystemStatus();
}, 400);
});
}
// Open Steve Chat button
if (btnOpenSteveChat) {
btnOpenSteveChat.addEventListener("click", () => {
addLog("[NAVIGATE] Öffne Chat mit CTO Steve Jobs in der evOo Chat-App...", "highlight");
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: "open_team_tab", botName: "Steve Jobs" }, "*");
}
});
}
});

View File

@ -1,347 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>evOo Mission Control | Executive Operations Cockpit</title>
<link rel="stylesheet" href="styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
<div class="background-mesh"></div>
<!-- Executive Pulse Strip (Header) -->
<header class="executive-header">
<div class="header-left">
<div class="brand">
<span class="brand-name">ev<span class="brand-highlight">Oo</span></span>
<span class="version-tag">v2.0-EXEC</span>
</div>
<div class="pulse-pill" id="system-health-pill">
<span class="pulse-indicator online"></span>
<span class="pill-label">System:</span>
<span class="pill-value" id="system-health-val">HEALTHY (Port 3088)</span>
</div>
</div>
<div class="header-center">
<div class="topology-pill" title="Hardware-Cluster & Lokale Modelle">
<span class="pill-icon">🖥️</span>
<span class="pill-label">Cluster:</span>
<span class="pill-value" id="cluster-summary-val">Host (Metal GPU 30B) · Mini · Air</span>
</div>
<div class="trading-pill" title="Trading-Bot & Apex Risk Subsystem">
<span class="pill-icon">📈</span>
<span class="pill-label">Trading:</span>
<span class="pill-value" id="trading-summary-val">ARMED (Max DD 0.21% / 1.5%)</span>
</div>
</div>
<div class="header-right">
<button type="button" id="btn-self-improve" class="btn btn-secondary" title="Führt den vollständigen Verifizierungs- & Test-Loop aus">
<span class="btn-icon">⚡</span>
<span>Selbstoptimierung</span>
</button>
<button type="button" id="btn-killswitch" class="btn btn-warning" title="Notfall-Stopp aller autonomen Trading-Aktionen">
<span class="btn-icon">🛡️</span>
<span id="killswitch-text">Risk Guard: Aktiv</span>
</button>
<button type="button" id="btn-hot-reload" class="btn btn-primary" title="Server-Synchronisation und Hot-Reload">
<span class="btn-icon">↺</span>
<span>Hot-Reload</span>
</button>
</div>
</header>
<!-- Main Cockpit Grid -->
<main class="cockpit-container">
<div class="cockpit-main-layout">
<!-- ZONE 1: Executive Action & Overnight Journal (Left Column, ~58%) -->
<div class="column-executive">
<!-- PENDING APPROVALS CARD -->
<section class="card card-approvals">
<div class="card-header">
<div class="header-title-group">
<span class="section-tag">EXECUTIVE INBOX</span>
<h2>Offene Entscheidungen &amp; Freigaben</h2>
</div>
<span class="badge badge-amber" id="approvals-badge">2 Ausstehend</span>
</div>
<div class="approvals-list" id="approvals-container">
<!-- Rendered dynamically by app.js -->
</div>
</section>
<!-- WAS GESCHAH IN DEN LETZTEN 24h? (Overnight Briefing) -->
<section class="card card-journal">
<div class="card-header">
<div class="header-title-group">
<span class="section-tag">OVERNIGHT BRIEFING</span>
<h2>Operations Journal (Letzte 24h)</h2>
</div>
<span class="badge badge-emerald" id="journal-badge">100% Physisch</span>
</div>
<div class="journal-timeline" id="journal-container">
<!-- Rendered dynamically by app.js -->
</div>
</section>
</div>
<!-- ZONE 2: Executive Board & Casted Agent Network (Right Column, ~42%) -->
<div class="column-agents">
<!-- CTO DIRECTIVE CARD -->
<section class="card card-cto">
<div class="card-header">
<div class="header-title-group">
<span class="section-tag">EXECUTIVE COMMAND</span>
<h2>CTO Steve Jobs</h2>
</div>
<span class="badge badge-purple">Vollmacht: Unbeschränkt</span>
</div>
<div class="cto-body">
<div class="cto-profile">
<div class="cto-avatar">SJ</div>
<div class="cto-details">
<span class="cto-name">Steve Jobs</span>
<span class="cto-role">Chief Technology Officer · Modus: Action-First</span>
</div>
</div>
<div class="cto-quote-box">
<p class="cto-quote">
„Realität im Dateisystem vor Halluzination im Kopf. Das 3-Säulen-System steht. Alle 5 Spezialisten arbeiten mit deterministischen Schnittstellen und echten Tests.“
</p>
</div>
<div class="cto-actions">
<button type="button" id="btn-open-steve-chat" class="btn-inline btn-purple">
<span>💬 Chat mit Steve öffnen</span>
</button>
<div class="authority-pills">
<span class="auth-pill">✓ Filesystem Write</span>
<span class="auth-pill">✓ Multi-Node Orchestration</span>
</div>
</div>
</div>
</section>
<!-- OPERATIONS BOARD (5 CASTED AGENTS) -->
<section class="card card-operations-board">
<div class="card-header">
<div class="header-title-group">
<span class="section-tag">OPERATIONS BOARD</span>
<h2>Agenten-Flotte</h2>
</div>
<span class="badge badge-cyan" id="agent-count">5 Casts aktiv</span>
</div>
<p class="operations-hint">Klicke auf einen Spezialisten, um sofort ein physisches System-Audit auszulösen:</p>
<div class="agent-roster">
<!-- 1. Code Architect -->
<div class="roster-item interactive-agent" data-agent="architect" title="Klicken für Architecture- & Modul-Audit">
<div class="agent-avatar architect">CA</div>
<div class="agent-meta">
<span class="agent-name">Code Architect</span>
<span class="agent-role">Moduldesign &amp; Clean Architecture</span>
<span class="agent-node">Node: Mac mini (Strategic Reasoning)</span>
</div>
<div class="agent-status">
<span class="status-dot green" id="dot-architect"></span>
</div>
</div>
<!-- 2. Security & QA Auditor -->
<div class="roster-item interactive-agent" data-agent="qa" title="Klicken für Zero-Phantom- & Test-Audit">
<div class="agent-avatar security">QA</div>
<div class="agent-meta">
<span class="agent-name">Security &amp; QA Auditor</span>
<span class="agent-role">Integritätsprüfung &amp; Zero-Phantom</span>
<span class="agent-node">Node: MacBook Air (Fast Coding)</span>
</div>
<div class="agent-status">
<span class="status-dot green" id="dot-qa"></span>
</div>
</div>
<!-- 3. DevOps Engineer -->
<div class="roster-item interactive-agent" data-agent="devops" title="Klicken für Liveness- & Port-Audit">
<div class="agent-avatar devops">DO</div>
<div class="agent-meta">
<span class="agent-name">DevOps Engineer</span>
<span class="agent-role">Hot-Reload &amp; Service Liveness</span>
<span class="agent-node">Node: Host Mac (Daemons &amp; Ports)</span>
</div>
<div class="agent-status">
<span class="status-dot green" id="dot-devops"></span>
</div>
</div>
<!-- 4. Apex Quant Specialist -->
<div class="roster-item interactive-agent" data-agent="quant" title="Klicken für Apex Risk &amp; Trading-Bot-Bridge Audit">
<div class="agent-avatar quant">AP</div>
<div class="agent-meta">
<span class="agent-name">Apex Quant Specialist</span>
<span class="agent-role">Algorithmic Risk &amp; Port 8000 Bridge</span>
<span class="agent-node">Node: Host Mac (Deterministic Math)</span>
</div>
<div class="agent-status">
<span class="status-dot purple" id="dot-quant"></span>
</div>
</div>
<!-- 5. UX Designer -->
<div class="roster-item interactive-agent" data-agent="ux" title="Klicken für UI/UX &amp; Design-System-Audit">
<div class="agent-avatar ux">UX</div>
<div class="agent-meta">
<span class="agent-name">UX Designer</span>
<span class="agent-role">Lead Product &amp; UX/UI Designer</span>
<span class="agent-node">Node: MacBook Air / Host (3-Pillar UI)</span>
</div>
<div class="agent-status">
<span class="status-dot orange" id="dot-ux"></span>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- ZONE 3 & 4: Distributed Cluster, Apex Trading & Telemetry Console (Bottom Full Width) -->
<div class="cockpit-bottom-layout">
<!-- CLUSTER TOPOLOGY CARD -->
<section class="card card-cluster">
<div class="card-header">
<div class="header-title-group">
<span class="section-tag">COMPUTE TOPOLOGY</span>
<h2>Distributed Mac Cluster</h2>
</div>
<span class="badge badge-emerald" id="cluster-badge">Metal GPU Aktiv</span>
</div>
<div class="cluster-grid">
<div class="cluster-node online">
<div class="node-header">
<span class="node-status-dot"></span>
<span class="node-name">Host Mac (Primär)</span>
<span class="node-type">Metal GPU</span>
</div>
<div class="node-details">
<div class="detail-row"><span>LM Studio:</span><span class="code-val">qwen3-coder-30b-a3b (Metal)</span></div>
<div class="detail-row"><span>Ollama:</span><span class="code-val">gemma4:latest (Port 11434)</span></div>
<div class="detail-row"><span>Zustand:</span><span class="success-val">Aktiv · Keine Drosselung</span></div>
</div>
</div>
<div class="cluster-node standby">
<div class="node-header">
<span class="node-status-dot amber"></span>
<span class="node-name">Mac mini</span>
<span class="node-type">24/7 Powerhouse</span>
</div>
<div class="node-details">
<div class="detail-row"><span>Profil:</span><span>Aktiv gekühlt (Lüfter)</span></div>
<div class="detail-row"><span>Empfohlen:</span><span class="code-val">qwen2.5:32b / deepseek-r1</span></div>
<div class="detail-row"><span>Routing:</span><span class="text-dim">Failover zu Host Metal GPU</span></div>
</div>
</div>
<div class="cluster-node standby">
<div class="node-header">
<span class="node-status-dot amber"></span>
<span class="node-name">MacBook Air</span>
<span class="node-type">Mobile Worker</span>
</div>
<div class="node-details">
<div class="detail-row"><span>Profil:</span><span>Lüfterlos (Thermisch geschützt)</span></div>
<div class="detail-row"><span>Empfohlen:</span><span class="code-val">qwen2.5-coder:7b / 3b</span></div>
<div class="detail-row"><span>Routing:</span><span class="text-dim">Failover zu Host Metal GPU</span></div>
</div>
</div>
</div>
</section>
<!-- APEX QUANT TRADING ENGINE SUBSYSTEM -->
<section class="card card-apex">
<div class="card-header">
<div class="header-title-group">
<span class="section-tag">SUBSYSTEM APEX</span>
<h2>Apex Quantitative Trading Engine</h2>
</div>
<div class="header-action-cluster">
<div class="mode-segmented-control" role="tablist">
<button type="button" id="tab-mode-paper" class="segment-btn active" data-mode="paper" title="Simulierter Test-Modus mit $1,000 Paper Equity">
<span class="segment-dot paper-dot"></span>
<span>Paper-Modus</span>
</button>
<button type="button" id="tab-mode-live" class="segment-btn" data-mode="live" title="Echtgeld-Modus: Polymarket Deposit Wallet, Bitpanda & Fiat-Reserven">
<span class="segment-dot live-dot"></span>
<span>🟢 Live-Echtgeld</span>
</button>
</div>
<span class="badge badge-cyan" id="trading-badge">REST API: Port 8001</span>
</div>
</div>
<div class="trading-metrics-grid">
<div class="tmetric-card">
<span class="tmetric-label" id="card1-label">Equity &amp; Portfolio</span>
<span class="tmetric-val positive" id="card1-val">$1,000.00</span>
<span class="tmetric-sub" id="card1-sub">€921.66 · Paper Trading (Safe)</span>
</div>
<div class="tmetric-card">
<span class="tmetric-label" id="card2-label">Max Drawdown</span>
<span class="tmetric-val" id="card2-val">0.00%</span>
<span class="tmetric-sub" id="card2-sub">Cap: 1.50% (Hard Stop)</span>
</div>
<div class="tmetric-card">
<span class="tmetric-label" id="card3-label">Aktives Markt-Regime</span>
<span class="tmetric-val cyan-text" id="card3-val">Donchian / ADX</span>
<span class="tmetric-sub" id="card3-sub">Trend-Following + Mean-Reversion</span>
</div>
<div class="tmetric-card">
<span class="tmetric-label" id="card4-label">Circuit Breaker Status</span>
<span class="tmetric-val success-val" id="card4-val">ARMED</span>
<span class="tmetric-sub" id="card4-sub">Fixed Fractional (1-2%)</span>
</div>
</div>
</section>
<!-- LIVE TELEMETRY & LOG CONSOLE (COLLAPSIBLE) -->
<section class="card card-console">
<div class="card-header">
<div class="header-title-group">
<span class="section-tag">LIVE TELEMETRY STREAM</span>
<h2>Self-Improvement &amp; System Event Log</h2>
</div>
<div class="console-actions">
<button type="button" id="btn-toggle-console" class="btn-text">Minimieren</button>
<button type="button" id="btn-clear-console" class="btn-text">Leeren</button>
</div>
</div>
<div class="terminal-container" id="terminal-logs">
<div class="log-line info">[SYSTEM] evOo Executive Cockpit initialized. Version 2.0.0.</div>
<div class="log-line success">[CLUSTER] Host Metal GPU (30B Qwen) aktiv. Port 1234 bereit.</div>
<div class="log-line success">[VERIFY] 3-Säulen-Navigation (Heute/Team/Projekte) in Chat-App verankert.</div>
<div class="log-line success">[OPERATIONS] 5 Casted Agents (Architect, QA, DevOps, Quant, UX) aktiv.</div>
</div>
</section>
</div>
</main>
<footer class="app-footer">
<div class="footer-left">
<span>evOo Autonomous Platform &bull; Physical Reality Guaranteed &bull; Executive Command Center</span>
</div>
<div class="footer-right">
<span id="timestamp-clock">--:--:-- UTC</span>
</div>
</footer>
<script src="app.js"></script>
</body>
</html>

View File

@ -1,142 +0,0 @@
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
/** Default web/API origins allowed on loopback binds for the Heute iframe shell. */
const DEFAULT_LOOPBACK_WEB_ORIGINS = [
"http://127.0.0.1:5173",
"http://localhost:5173",
"http://127.0.0.1:3100",
"http://localhost:3100",
];
export function isLoopbackHost(host) {
return LOOPBACK_HOSTS.has(host.trim());
}
export function timingSafeStringEqual(supplied, expected) {
const actual = Buffer.from(expected);
const candidate = Buffer.from(supplied ?? "");
if (actual.length !== candidate.length) return false;
let difference = 0;
for (let index = 0; index < actual.length; index += 1) {
difference |= actual[index] ^ candidate[index];
}
return difference === 0;
}
export function hasValidBearerToken(authorization, expectedToken) {
const supplied = authorization?.startsWith("Bearer ") ? authorization.slice(7) : "";
return timingSafeStringEqual(supplied, expectedToken);
}
export function isPublicCockpitRoute(pathname) {
return pathname === "/health" || pathname === "/api/health";
}
/** Privileged API routes must never receive Access-Control-Allow-Origin: *. */
export function isPrivilegedCockpitRoute(pathname) {
if (isPublicCockpitRoute(pathname)) return false;
return pathname.startsWith("/api/");
}
export function parseCorsOriginAllowlist(value) {
if (!value?.trim()) return [];
return [
...new Set(
value
.split(",")
.map((entry) => entry.trim())
.filter(Boolean),
),
];
}
/**
* Cockpit bind/auth settings. Defaults to loopback-only with no token so local
* Mission Control keeps working out of the box.
*/
export function resolveCockpitConfig(env) {
const port = Number.parseInt(env.PORT?.trim() || "3088", 10);
if (!Number.isInteger(port) || port <= 0 || port > 65_535) {
throw new Error("PORT is not a valid port number.");
}
const host = env.EVOO_COCKPIT_HOST?.trim() || "127.0.0.1";
const loopback = isLoopbackHost(host);
const token = env.EVOO_COCKPIT_TOKEN?.trim() || "";
if (!loopback && token === "") {
throw new Error(
"Set EVOO_COCKPIT_TOKEN when EVOO_COCKPIT_HOST is not loopback (127.0.0.1, localhost, or ::1).",
);
}
return {
port,
host,
token: loopback ? null : token,
requiresAuth: !loopback,
};
}
/**
* CORS allowlist. Loopback binds default to local web/API origins; non-loopback
* binds require an explicit EVOO_COCKPIT_CORS_ORIGIN comma-separated list.
*/
export function resolveCockpitCorsConfig(env, cockpitConfig) {
const explicit = parseCorsOriginAllowlist(env.EVOO_COCKPIT_CORS_ORIGIN);
const loopbackBind = isLoopbackHost(cockpitConfig.host);
if (!loopbackBind && explicit.length === 0) {
return {
loopbackBind: false,
allowedOrigins: [],
usesDefaultLoopbackOrigins: false,
};
}
const usesDefaultLoopbackOrigins = loopbackBind && explicit.length === 0;
const allowedOrigins = usesDefaultLoopbackOrigins ? DEFAULT_LOOPBACK_WEB_ORIGINS : explicit;
return {
loopbackBind,
allowedOrigins,
usesDefaultLoopbackOrigins,
};
}
/**
* Resolve the Access-Control-Allow-Origin value for a request.
* Returns a reflected origin string, null when CORS headers should be omitted,
* or false when the Origin is present but not allowed.
*/
export function resolveAllowedCorsOrigin(requestOrigin, corsConfig, pathname) {
const origin = requestOrigin?.trim() ?? "";
if (!origin) return null;
if (isPrivilegedCockpitRoute(pathname)) {
return corsConfig.allowedOrigins.includes(origin) ? origin : false;
}
if (corsConfig.loopbackBind && corsConfig.usesDefaultLoopbackOrigins) {
return corsConfig.allowedOrigins.includes(origin) ? origin : false;
}
return corsConfig.allowedOrigins.includes(origin) ? origin : false;
}
export function applyCockpitCorsHeaders(req, res, pathname, corsConfig) {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
const allowed = resolveAllowedCorsOrigin(req.headers.origin, corsConfig, pathname);
if (typeof allowed === "string") {
res.setHeader("Access-Control-Allow-Origin", allowed);
res.setHeader("Vary", "Origin");
}
return allowed;
}
export function formatCockpitListenUrl(host, port) {
const displayHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
return `http://${displayHost}:${port}`;
}

View File

@ -1,215 +0,0 @@
import { describe, expect, it } from "vitest";
import {
applyCockpitCorsHeaders,
formatCockpitListenUrl,
hasValidBearerToken,
isLoopbackHost,
isPrivilegedCockpitRoute,
isPublicCockpitRoute,
resolveAllowedCorsOrigin,
resolveCockpitConfig,
resolveCockpitCorsConfig,
} from "./server-config.js";
describe("resolveCockpitConfig", () => {
it("binds to loopback by default without requiring a token", () => {
expect(resolveCockpitConfig({})).toEqual({
port: 3088,
host: "127.0.0.1",
token: null,
requiresAuth: false,
});
});
it("honours explicit loopback hosts", () => {
for (const host of ["127.0.0.1", "localhost", "::1"]) {
expect(resolveCockpitConfig({ EVOO_COCKPIT_HOST: host })).toMatchObject({
host,
token: null,
requiresAuth: false,
});
}
});
it("requires a shared token before binding beyond loopback", () => {
expect(() => resolveCockpitConfig({ EVOO_COCKPIT_HOST: "0.0.0.0" })).toThrow(
/EVOO_COCKPIT_TOKEN/,
);
});
it("allows a non-loopback bind when a token is configured", () => {
expect(
resolveCockpitConfig({
EVOO_COCKPIT_HOST: "0.0.0.0",
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
}),
).toEqual({
port: 3088,
host: "0.0.0.0",
token: "cockpit-review-token-000000000000",
requiresAuth: true,
});
});
it("rejects invalid ports", () => {
expect(() => resolveCockpitConfig({ PORT: "0" })).toThrow(/PORT/);
expect(() => resolveCockpitConfig({ PORT: "abc" })).toThrow(/PORT/);
});
});
describe("cockpit auth helpers", () => {
const token = "cockpit-review-token-000000000000";
it("treats health routes as public when auth is enabled", () => {
expect(isPublicCockpitRoute("/health")).toBe(true);
expect(isPublicCockpitRoute("/api/health")).toBe(true);
expect(isPublicCockpitRoute("/api/status")).toBe(false);
});
it("accepts only bearer tokens with constant-time comparison", () => {
expect(hasValidBearerToken(`Bearer ${token}`, token)).toBe(true);
expect(hasValidBearerToken(`Bearer ${`${token}x`}`, token)).toBe(false);
expect(hasValidBearerToken(token, token)).toBe(false);
expect(hasValidBearerToken(undefined, token)).toBe(false);
});
it("formats listen URLs with bracketed IPv6 hosts", () => {
expect(formatCockpitListenUrl("127.0.0.1", 3088)).toBe("http://127.0.0.1:3088");
expect(formatCockpitListenUrl("::1", 3088)).toBe("http://[::1]:3088");
});
});
describe("isLoopbackHost", () => {
it("recognises loopback hosts only", () => {
expect(isLoopbackHost("127.0.0.1")).toBe(true);
expect(isLoopbackHost("localhost")).toBe(true);
expect(isLoopbackHost("::1")).toBe(true);
expect(isLoopbackHost("0.0.0.0")).toBe(false);
expect(isLoopbackHost("192.168.1.10")).toBe(false);
});
});
describe("isPrivilegedCockpitRoute", () => {
it("marks API routes except health as privileged", () => {
expect(isPrivilegedCockpitRoute("/health")).toBe(false);
expect(isPrivilegedCockpitRoute("/api/health")).toBe(false);
expect(isPrivilegedCockpitRoute("/api/status")).toBe(true);
expect(isPrivilegedCockpitRoute("/api/trading-bot/mode")).toBe(true);
expect(isPrivilegedCockpitRoute("/index.html")).toBe(false);
});
});
describe("resolveCockpitCorsConfig", () => {
const loopbackCockpit = resolveCockpitConfig({ EVOO_COCKPIT_HOST: "127.0.0.1" });
const publicCockpit = resolveCockpitConfig({
EVOO_COCKPIT_HOST: "0.0.0.0",
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
});
it("defaults to local web origins on loopback binds", () => {
const cors = resolveCockpitCorsConfig({}, loopbackCockpit);
expect(cors.loopbackBind).toBe(true);
expect(cors.usesDefaultLoopbackOrigins).toBe(true);
expect(cors.allowedOrigins).toContain("http://127.0.0.1:5173");
});
it("requires an explicit allowlist off loopback", () => {
const cors = resolveCockpitCorsConfig({}, publicCockpit);
expect(cors.allowedOrigins).toEqual([]);
expect(cors.usesDefaultLoopbackOrigins).toBe(false);
});
it("honours EVOO_COCKPIT_CORS_ORIGIN when set", () => {
const cors = resolveCockpitCorsConfig(
{ EVOO_COCKPIT_CORS_ORIGIN: "https://ops.example.com, https://ops.example.com" },
publicCockpit,
);
expect(cors.allowedOrigins).toEqual(["https://ops.example.com"]);
});
});
describe("resolveAllowedCorsOrigin", () => {
const loopbackCors = resolveCockpitCorsConfig({}, resolveCockpitConfig({}));
const publicCors = resolveCockpitCorsConfig(
{ EVOO_COCKPIT_CORS_ORIGIN: "https://ops.example.com" },
resolveCockpitConfig({
EVOO_COCKPIT_HOST: "0.0.0.0",
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
}),
);
const blockedPublicCors = resolveCockpitCorsConfig(
{},
resolveCockpitConfig({
EVOO_COCKPIT_HOST: "0.0.0.0",
EVOO_COCKPIT_TOKEN: "cockpit-review-token-000000000000",
}),
);
it("never returns a wildcard and omits headers without an Origin", () => {
expect(resolveAllowedCorsOrigin(undefined, loopbackCors, "/api/status")).toBeNull();
expect(resolveAllowedCorsOrigin("", loopbackCors, "/api/status")).toBeNull();
});
it("reflects allowlisted origins on privileged routes", () => {
expect(resolveAllowedCorsOrigin("http://127.0.0.1:5173", loopbackCors, "/api/status")).toBe(
"http://127.0.0.1:5173",
);
expect(resolveAllowedCorsOrigin("https://evil.example", loopbackCors, "/api/status")).toBe(
false,
);
});
it("blocks cross-origin privileged access off loopback without an allowlist", () => {
expect(
resolveAllowedCorsOrigin("https://ops.example.com", blockedPublicCors, "/api/status"),
).toBe(false);
});
it("allows configured origins off loopback", () => {
expect(
resolveAllowedCorsOrigin("https://ops.example.com", publicCors, "/api/trading-bot"),
).toBe("https://ops.example.com");
});
});
describe("applyCockpitCorsHeaders", () => {
it("sets reflected origins and never sets *", () => {
const cors = resolveCockpitCorsConfig({}, resolveCockpitConfig({}));
const headers = {};
const res = {
setHeader: (key, value) => {
headers[key] = value;
},
};
const allowed = applyCockpitCorsHeaders(
{ headers: { origin: "http://127.0.0.1:5173" } },
res,
"/api/status",
cors,
);
expect(allowed).toBe("http://127.0.0.1:5173");
expect(headers["Access-Control-Allow-Origin"]).toBe("http://127.0.0.1:5173");
expect(headers["Access-Control-Allow-Origin"]).not.toBe("*");
expect(headers["Access-Control-Allow-Headers"]).toContain("Authorization");
});
it("omits Access-Control-Allow-Origin for disallowed cross-origin requests", () => {
const cors = resolveCockpitCorsConfig({}, resolveCockpitConfig({}));
const headers = {};
const res = {
setHeader: (key, value) => {
headers[key] = value;
},
};
const allowed = applyCockpitCorsHeaders(
{ headers: { origin: "https://evil.example" } },
res,
"/api/status",
cors,
);
expect(allowed).toBe(false);
expect(headers["Access-Control-Allow-Origin"]).toBeUndefined();
});
});

View File

@ -1,731 +0,0 @@
/**
* evOo Platform High-Speed Embedded Server
* ES Module compatible with Node.js 22+.
* Zero external dependencies.
* Hardened against Directory Traversal & equipped with real Agent Runtime integration.
*/
import { spawn } from "node:child_process";
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
applyCockpitCorsHeaders,
formatCockpitListenUrl,
hasValidBearerToken,
isPublicCockpitRoute,
resolveCockpitConfig,
resolveCockpitCorsConfig,
} from "./server-config.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const cockpit = resolveCockpitConfig(process.env);
const corsConfig = resolveCockpitCorsConfig(process.env, cockpit);
const { port: PORT, host: COCKPIT_HOST } = cockpit;
const BASE_DIR = __dirname;
const ROOT_DIR = path.resolve(BASE_DIR, "..");
const MIME_TYPES = {
".html": "text/html; charset=UTF-8",
".css": "text/css; charset=UTF-8",
".js": "application/javascript; charset=UTF-8",
".json": "application/json; charset=UTF-8",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml",
".md": "text/markdown; charset=UTF-8",
};
const APPROVALS_FILE = path.join(ROOT_DIR, "orchestrator", "data", "approvals.json");
const JOURNAL_FILE = path.join(ROOT_DIR, "orchestrator", "data", "journal.json");
function loadApprovals() {
try {
if (fs.existsSync(APPROVALS_FILE)) {
return JSON.parse(fs.readFileSync(APPROVALS_FILE, "utf8"));
}
} catch (e) {
console.error("Error loading approvals:", e.message);
}
const defaults = [
{
id: "appr-104",
type: "trade",
title: "Staged Trade: ETH/USDT Breakout",
author: "Apex Quant Specialist",
conviction: 88,
riskReward: 2.4,
size: "1.2% Equity",
status: "pending",
timestamp: "Vor 14 Min.",
details:
"Trend-Following Breakout über 20-Perioden Donchian Upper Band. ADX 32.4 (starker Trend). Stop-Loss bei -1.1% gesetzt.",
},
{
id: "appr-105",
type: "architecture",
title: "Modulare Entkopplung: Trading-Bot API Bridge",
author: "Code Architect",
conviction: 95,
riskReward: 3.5,
size: "Refactor Plan",
status: "pending",
timestamp: "Vor 42 Min.",
details:
"Vollständige Entkopplung der tradingbot-Imports. evOo konsumiert ausschließlich REST-Endpoints auf Port 8000.",
},
];
saveApprovals(defaults);
return defaults;
}
function saveApprovals(items) {
try {
fs.mkdirSync(path.dirname(APPROVALS_FILE), { recursive: true });
fs.writeFileSync(APPROVALS_FILE, JSON.stringify(items, null, 2), "utf8");
} catch (e) {
console.error("Error saving approvals:", e.message);
}
}
function loadJournal() {
try {
if (fs.existsSync(JOURNAL_FILE)) {
return JSON.parse(fs.readFileSync(JOURNAL_FILE, "utf8"));
}
} catch (e) {
console.error("Error loading journal:", e.message);
}
const defaults = [
{
time: "04:52 UTC",
agent: "Security & QA",
type: "success",
title: "Zero-Phantom Integrity & Tests",
detail: "14 Systemdateien & 5 JSONs verifiziert. Apex Unittests 100% grün.",
},
{
time: "04:46 UTC",
agent: "Orchestrator",
type: "info",
title: "Casted Agent Network Sync",
detail: "5 Casted Agents (inkl. UX Designer) synchronisiert. Local Metal GPU aktiv.",
},
{
time: "03:15 UTC",
agent: "Apex Quant",
type: "success",
title: "Circuit Breaker Audit",
detail: "Kein Drawdown-Verstoß festgestellt. Max Drawdown aktuell 0.21% / 1.5% Cap.",
},
{
time: "01:05 UTC",
agent: "Cluster Router",
type: "info",
title: "Smart Failover Routine",
detail: "Host Apple Silicon Metal übernimmt Inference nahtlos für Standby-LAN-Nodes.",
},
{
time: "00:50 UTC",
agent: "DevOps Engineer",
type: "success",
title: "Plattform Hot-Reload",
detail: "Server auf Port 3088 atomar neu gestartet. PID stabilisiert.",
},
];
try {
fs.mkdirSync(path.dirname(JOURNAL_FILE), { recursive: true });
fs.writeFileSync(JOURNAL_FILE, JSON.stringify(defaults, null, 2), "utf8");
} catch {}
return defaults;
}
function appendJournal(entry) {
try {
const list = loadJournal();
list.unshift(entry);
if (list.length > 50) list.pop();
fs.mkdirSync(path.dirname(JOURNAL_FILE), { recursive: true });
fs.writeFileSync(JOURNAL_FILE, JSON.stringify(list, null, 2), "utf8");
} catch (e) {
console.error("Error appending journal:", e.message);
}
}
let riskKillSwitchActive = false;
const pendingApprovals = loadApprovals();
let currentTradingMode = "paper"; // 'paper' or 'live'
let liveCapitalCache = null;
let liveCapitalLastFetch = 0;
async function getLiveCapitalTelemetry() {
const now = Date.now();
if (liveCapitalCache && now - liveCapitalLastFetch < 15000) {
return liveCapitalCache;
}
// 1. Bitpanda Fusion Balance
const bitpandaBal = {
eur: 46.09,
usdc: 10.26,
pol: 436.46,
near: 12.59,
btt: 137271,
vet: 12.75,
};
try {
const envFile = path.resolve(ROOT_DIR, "..", "trading-bot", ".env.quant");
if (fs.existsSync(envFile)) {
const content = fs.readFileSync(envFile, "utf8");
const m = content.match(/BITPANDA_API_KEY=([a-zA-Z0-9]+)/);
if (m?.[1]) {
const bpRes = await fetch("https://api.fusion.bitpanda.com/v1/account/balances", {
headers: { "x-api-key": m[1] },
signal: AbortSignal.timeout(2500),
});
if (bpRes.ok) {
const list = await bpRes.json();
for (const item of list) {
const sym = (item.symbol || "").toLowerCase();
const val = parseFloat(item.available || "0");
if (sym === "eur") bitpandaBal.eur = val;
if (sym === "usdc") bitpandaBal.usdc = val;
if (sym === "pol") bitpandaBal.pol = val;
if (sym === "near") bitpandaBal.near = val;
if (sym === "btt") bitpandaBal.btt = val;
if (sym === "vet") bitpandaBal.vet = val;
}
}
}
}
} catch (_e) {
// Keep defaults
}
// 2. Polymarket CLOB V2 Deposit Wallet (On-Chain Polygon RPC)
let polymarketPusd = 639.39;
const polymarketPolGas = 58.25;
try {
const depositWallet = "0x1d03562E91E71D65DFDec36cc8874be32135D4bC";
const pUSDContract = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB";
const data = `0x70a08231000000000000000000000000${depositWallet.slice(2).toLowerCase()}`;
const rpcRes = await fetch("https://polygon-bor-rpc.publicnode.com", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "eth_call",
params: [{ to: pUSDContract, data }, "latest"],
}),
signal: AbortSignal.timeout(2500),
});
if (rpcRes.ok) {
const rpcJson = await rpcRes.json();
if (rpcJson?.result) {
polymarketPusd = Math.round((parseInt(rpcJson.result, 16) / 1e6) * 100) / 100;
}
}
} catch (_e) {
// Keep defaults
}
// 3. Bank / Fiat Balances
const fiat = { n26: 5000.0, revolut: 3500.0, total_eur: 8500.0 };
try {
const fiatFile = path.resolve(ROOT_DIR, "..", "trading-bot", "data", "fiat_balances.json");
if (fs.existsSync(fiatFile)) {
const parsed = JSON.parse(fs.readFileSync(fiatFile, "utf8"));
fiat.n26 = parsed.n26?.cash || 5000.0;
fiat.revolut = parsed.revolut?.cash || 3500.0;
fiat.total_eur = fiat.n26 + fiat.revolut;
}
} catch (_e) {}
const totalEur =
Math.round((fiat.total_eur + bitpandaBal.eur + polymarketPusd / 1.085 + 150) * 100) / 100;
const totalUsd = Math.round(totalEur * 1.085 * 100) / 100;
liveCapitalCache = {
status: "LIVE FUNDED",
mode: "LIVE (ECHTGELD)",
polymarket: {
deposit_wallet: "0x1d03...D4bC",
full_wallet: "0x1d03562E91E71D65DFDec36cc8874be32135D4bC",
pusd_balance: polymarketPusd,
pol_gas: polymarketPolGas,
status: "FUNDED & READY (CLOB V2)",
},
bitpanda: {
eur_cash: bitpandaBal.eur,
usdc: bitpandaBal.usdc,
pol: bitpandaBal.pol,
near: bitpandaBal.near,
btt: bitpandaBal.btt,
vet: bitpandaBal.vet,
status: "ONLINE (FUSION REST)",
},
fiat_reserves: {
n26_eur: fiat.n26,
revolut_eur: fiat.revolut,
total_eur: fiat.total_eur,
},
total_live_equity_usd: totalUsd,
total_live_equity_eur: totalEur,
circuit_breaker: "ENFORCED (TRIPLE-GATED)",
open_positions: 0,
};
liveCapitalLastFetch = now;
return liveCapitalCache;
}
const server = http.createServer(async (req, res) => {
const parsedUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
const urlPath = parsedUrl.pathname;
const corsAllowed = applyCockpitCorsHeaders(req, res, urlPath, corsConfig);
if (req.method === "OPTIONS") {
if (corsAllowed === false && req.headers.origin) {
res.writeHead(403);
res.end();
return;
}
res.writeHead(204);
res.end();
return;
}
if (
cockpit.requiresAuth &&
!isPublicCockpitRoute(urlPath) &&
!hasValidBearerToken(req.headers.authorization, cockpit.token)
) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "unauthorized" }));
return;
}
// 1. API Health Check
if (urlPath === "/health" || urlPath === "/api/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "healthy",
uptime: process.uptime(),
timestamp: new Date().toISOString(),
engine: "evOo Autonomous Platform 2.0.0",
cto: "Steve Jobs",
filesystem_integrity: "100% Physical",
}),
);
return;
}
// 2. API Status (Real System Telemetry)
if (urlPath === "/api/status") {
const archExists = fs.existsSync(path.join(ROOT_DIR, "ARCHITECTURE.md"));
const apexExists = fs.existsSync(path.join(ROOT_DIR, "apex-trading", "BLUEPRINT.md"));
const deployExists = fs.existsSync(path.join(ROOT_DIR, "deploy.sh"));
const mem = process.memoryUsage();
const load = os.loadavg();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "online",
uptime_seconds: Math.round(process.uptime()),
load_avg_1m: load[0],
memory_rss_mb: Math.round((mem.rss / (1024 * 1024)) * 10) / 10,
memory_heap_mb: Math.round((mem.heapUsed / (1024 * 1024)) * 10) / 10,
verified_files: {
architecture: archExists,
apex_blueprint: apexExists,
deploy_script: deployExists,
},
breathe_frequency: "1.04 Hz",
}),
);
return;
}
// 3. API Self-Improvement trigger (Real execution of scripts/self_improve.py)
if (urlPath === "/api/self-improve") {
const isSSE =
req.headers.accept?.includes("text/event-stream") ||
parsedUrl.searchParams.get("stream") === "true";
const scriptPath = path.join(ROOT_DIR, "scripts", "self_improve.py");
if (isSSE) {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.write(
`data: ${JSON.stringify({ type: "start", msg: "Starte physischen Selbstverbesserungs-Zyklus..." })}\n\n`,
);
const child = spawn("python3", [scriptPath], { cwd: ROOT_DIR });
child.stdout.on("data", (data) => {
const lines = data.toString().split("\n");
for (const line of lines) {
if (line.trim()) {
res.write(`data: ${JSON.stringify({ type: "log", msg: line })}\n\n`);
}
}
});
child.stderr.on("data", (data) => {
const lines = data.toString().split("\n");
for (const line of lines) {
if (line.trim()) {
res.write(`data: ${JSON.stringify({ type: "error", msg: line })}\n\n`);
}
}
});
child.on("close", (code) => {
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Self-Improve Loop",
type: code === 0 ? "success" : "error",
title: "Selbstverbesserungs-Zyklus",
detail:
code === 0
? "14 Systemdateien & Apex-Tests physisch verifiziert."
: "Fehler im Zyklus aufgetreten.",
});
res.write(`data: ${JSON.stringify({ type: "done", exitCode: code })}\n\n`);
res.end();
});
return;
}
// Standard POST / GET non-streaming response
const child = spawn("python3", [scriptPath], { cwd: ROOT_DIR });
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.stderr.on("data", (d) => {
stderr += d.toString();
});
child.on("close", (code) => {
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Self-Improve Loop",
type: code === 0 ? "success" : "error",
title: "Selbstverbesserungs-Zyklus",
detail:
code === 0
? "14 Systemdateien & Apex-Tests physisch verifiziert."
: "Fehler im Zyklus aufgetreten.",
});
res.writeHead(code === 0 ? 200 : 500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: code === 0 ? "success" : "failed",
exitCode: code,
output: stdout,
error: stderr || null,
}),
);
});
return;
}
// 4. API Casted Agents Listing
if (urlPath === "/api/agents") {
const agentsPath = path.join(ROOT_DIR, "orchestrator", "agents.json");
if (fs.existsSync(agentsPath)) {
try {
const content = fs.readFileSync(agentsPath, "utf8");
res.writeHead(200, { "Content-Type": "application/json" });
res.end(content);
return;
} catch (_e) {
// Fallback
}
}
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "agents.json not found" }));
return;
}
// 4b. API Distributed Compute Cluster Telemetry
if (urlPath === "/api/cluster") {
const runnerPath = path.join(ROOT_DIR, "orchestrator", "agent_runner.py");
const child = spawn("python3", [runnerPath, "--cluster", "--json"], { cwd: ROOT_DIR });
let stdout = "";
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.on("close", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(stdout || JSON.stringify({ status: "unknown" }));
});
return;
}
// 4c. API Approvals (Executive Human-in-the-Loop)
if (urlPath === "/api/approvals") {
if (req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
try {
const { id, action } = JSON.parse(body);
const item = pendingApprovals.find((a) => a.id === id);
if (item) {
item.status = action === "approve" ? "approved" : "vetoed";
item.decidedAt = new Date().toISOString();
saveApprovals(pendingApprovals);
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Operator (Ingo)",
type: action === "approve" ? "success" : "warn",
title: `Entscheidung: ${action.toUpperCase()}`,
detail: `${item.title} (${item.id}) registriert.`,
});
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, item }));
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "Invalid payload" }));
}
});
return;
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, approvals: pendingApprovals }));
return;
}
// 4d. API Overnight & Operational Journal
if (urlPath === "/api/journal") {
const journal = loadJournal();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, events: journal }));
return;
}
// 4e. API Trading-Bot Probe (Dual Mode: Paper & Live Capital)
if (urlPath === "/api/trading-bot") {
const liveData = await getLiveCapitalTelemetry();
// Query paper bot on port 8001 (or 8002)
const paperData = {
status: "ONLINE",
mode: "PAPER (SAFE)",
total_cash_usd: 1000.0,
total_equity_usd: 1000.0,
total_cash_eur: 921.66,
pnl_today: "+0.00%",
active_regime: "DONCHIAN_TREND_ADX",
max_drawdown: "0.00%",
circuit_breaker: riskKillSwitchActive ? "HALTED_BY_OPERATOR" : "ARMED",
kill_switch_active: riskKillSwitchActive,
open_positions: 0,
};
try {
const resp = await fetch("http://127.0.0.1:8001/api/portfolio", {
signal: AbortSignal.timeout(800),
});
if (resp.ok) {
const portData = await resp.json();
paperData.total_cash_usd = portData.total_cash || 1000.0;
paperData.total_equity_usd = portData.total_equity || 1000.0;
paperData.total_cash_eur = portData.total_cash_eur || 921.66;
paperData.open_positions = portData.open_positions || 0;
}
} catch {
// Keep defaults
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
ok: true,
active_mode: currentTradingMode,
kill_switch_active: riskKillSwitchActive,
paper: paperData,
live: liveData,
}),
);
return;
}
// 4e-b. API Toggle Trading Mode (POST /api/trading-bot/mode)
if (urlPath === "/api/trading-bot/mode" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
try {
const { mode } = JSON.parse(body);
if (mode === "live" || mode === "paper") {
currentTradingMode = mode;
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: "Operator (Ingo)",
type: mode === "live" ? "warn" : "info",
title: `Trading-Modus gewechselt: ${mode.toUpperCase()}`,
detail:
mode === "live"
? "Live-Echtgeld Telemetrie (Polymarket CLOB V2 & Bitpanda) im Cockpit aktiviert."
: "Paper-Trading-Modus (Simulation) im Cockpit aktiviert.",
});
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, active_mode: currentTradingMode }));
} catch (e) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: e.message }));
}
});
return;
}
// 4f. API Risk Kill-Switch Toggle
if (urlPath === "/api/risk/toggle-killswitch" && req.method === "POST") {
riskKillSwitchActive = !riskKillSwitchActive;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, killSwitchActive: riskKillSwitchActive }));
return;
}
// 5. API Agent Dispatch (Real Task Execution)
if (urlPath === "/api/agents/dispatch" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
});
req.on("end", () => {
let agentId = "all";
let task = "";
try {
if (body) {
const parsed = JSON.parse(body);
if (parsed.agent) agentId = parsed.agent;
if (parsed.task) task = parsed.task;
}
} catch (_e) {
// Ignore JSON error and default
}
const runnerPath = path.join(ROOT_DIR, "orchestrator", "agent_runner.py");
const args = [runnerPath, "--agent", agentId, "--json"];
if (task) {
args.push("--task", task);
}
const child = spawn("python3", args, { cwd: ROOT_DIR });
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.stderr.on("data", (d) => {
stderr += d.toString();
});
child.on("close", (code) => {
appendJournal({
time: `${new Date().toISOString().substring(11, 16)} UTC`,
agent: agentId.toUpperCase(),
type: code === 0 ? "success" : "error",
title: `Agent Execution: ${agentId}`,
detail: `Task '${task || "audit"}' ausgeführt (Exit Code ${code}).`,
});
try {
const data = JSON.parse(stdout);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: code === 0, exitCode: code, data }));
} catch (_e) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: code === 0, exitCode: code, raw: stdout, stderr }));
}
});
});
return;
}
// 6. API Hot-Reload trigger
if (urlPath === "/api/reload" && req.method === "POST") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "success",
message: "Hot-Reload broadcast acknowledged. Dateisystem synchronisiert.",
}),
);
return;
}
// 7. Static File Serving with Directory Traversal Protection
let safeRelPath = path
.normalize(urlPath === "/" ? "/index.html" : urlPath)
.replace(/^(\.\.[/\\])+/, "");
if (safeRelPath.startsWith("/")) {
safeRelPath = safeRelPath.slice(1);
}
let resolvedPath = path.resolve(BASE_DIR, safeRelPath);
// Fallback to ROOT_DIR only for safe markdown docs (ARCHITECTURE.md, BLUEPRINT.md)
const isWhitelistedDoc = [
"ARCHITECTURE.md",
"apex-trading/BLUEPRINT.md",
"evoo.config.json",
].includes(safeRelPath);
if (!fs.existsSync(resolvedPath) && isWhitelistedDoc) {
resolvedPath = path.resolve(ROOT_DIR, safeRelPath);
}
// Directory Traversal Verification: must be inside BASE_DIR or explicitly whitelisted root file
const isInsideBase = resolvedPath.startsWith(BASE_DIR + path.sep) || resolvedPath === BASE_DIR;
const isInsideRootDoc = isWhitelistedDoc && resolvedPath.startsWith(ROOT_DIR + path.sep);
if (!isInsideBase && !isInsideRootDoc) {
res.writeHead(403, { "Content-Type": "text/plain; charset=UTF-8" });
res.end("403 Forbidden: Directory Traversal Denied");
return;
}
if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) {
const ext = path.extname(resolvedPath).toLowerCase();
const contentType = MIME_TYPES[ext] || "application/octet-stream";
res.writeHead(200, { "Content-Type": contentType });
fs.createReadStream(resolvedPath).pipe(res);
} else {
res.writeHead(404, { "Content-Type": "text/plain; charset=UTF-8" });
res.end(`404 Not Found: ${urlPath}`);
}
});
server.listen(PORT, COCKPIT_HOST, () => {
try {
fs.writeFileSync(path.join(ROOT_DIR, ".evoo_server.pid"), process.pid.toString(), "utf8");
} catch (_e) {
// Ignore if write fails
}
const listenUrl = formatCockpitListenUrl(COCKPIT_HOST, PORT);
console.log(`[evOo Core] Server listening on ${listenUrl} (PID: ${process.pid})`);
console.log(`[evOo Core] Healthcheck available at ${listenUrl}/health`);
if (cockpit.requiresAuth) {
console.log("[evOo Core] Non-loopback bind: Bearer token required except /health");
}
if (!corsConfig.loopbackBind && corsConfig.allowedOrigins.length === 0) {
console.log(
"[evOo Core] Non-loopback bind: set EVOO_COCKPIT_CORS_ORIGIN for browser cross-origin access",
);
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,60 +0,0 @@
{
"name": "evOo Autonomous Agent Platform",
"version": "2.0.0",
"permissions": {
"network": {
"allowed": true,
"scope": "*",
"description": "Full access to external and internal network endpoints"
},
"filesystem": {
"allowed": true,
"scope": "*",
"description": "Full access to read, write, and execute across local filesystem"
},
"sourcecode": {
"allowed": true,
"scope": "*",
"description": "Full authority to inspect, edit, refactor, and commit source code"
},
"devices": {
"allowed": true,
"scope": "*",
"description": "Full access to connected local devices, interfaces, and tools"
},
"agent_casting": {
"allowed": true,
"max_subagents": 20,
"description": "Authority to dynamically instantiate and manage sub-agent hierarchies"
},
"pre_approved_actions": [
"file_read",
"file_write",
"command_execution",
"subagent_spawn",
"self_refactor",
"network_request"
]
},
"execution_policy": {
"mode": "autonomous_action_first",
"multi_task_sequence": true,
"ask_user_policy": "on_negotiation_or_critical_boundary_only",
"block_on_routine_tasks": false,
"auto_proceed": true
},
"self_improvement": {
"enabled": true,
"auto_eval_logs": true,
"auto_refactor_prompts": true,
"continuous_learning": true,
"feedback_loop": "autonomous_iteration"
},
"cto_agent": {
"name": "Steve Jobs",
"role": "Chief Technology Officer (CTO)",
"authority": "full_executive",
"casting_policy": "cast_teams_on_demand",
"negotiation_protocol": "consensus_and_negotiation_basis"
}
}

View File

@ -0,0 +1,47 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
const workflowText = readFileSync(
path.resolve(import.meta.dirname, "../../../.github/workflows/ci.yml"),
"utf8",
);
interface WorkflowJob {
if?: string;
needs?: string[];
"continue-on-error"?: boolean;
}
const workflow = parse(workflowText) as {
jobs: Record<string, WorkflowJob>;
};
describe("ci workflow merge gate", () => {
it("aggregates only product jobs in Product CI", () => {
const productGate = workflow.jobs["product-gate"];
expect(productGate?.needs).toEqual([
"lint",
"check",
"build",
"test",
"test-integration",
"test-e2e",
]);
expect(productGate?.if).toBe("always()");
});
it("soft-fails mobile OTA so missing Expo credentials do not fail the workflow", () => {
const mobile = workflow.jobs["publish-mobile-update"];
expect(mobile?.["continue-on-error"]).toBe(true);
expect(workflowText).toContain('echo "ready=false" >> "$GITHUB_OUTPUT"');
expect(workflowText).not.toMatch(
/Check Expo automation token[\s\S]*?exit 1[\s\S]*?Require Expo/,
);
expect(workflowText).not.toContain(
"Add an EXPO_TOKEN repository secret with access to the inbox-zero/rakazo Expo project.",
);
expect(workflowText).toContain("steps.expo_token.outputs.ready == 'true'");
});
});

View File

@ -1,365 +0,0 @@
#!/usr/bin/env python3
"""
evOo Agent Orchestrator & Dispatcher
Executive Lead: CTO Steve Jobs
"Realität im Dateisystem vor Halluzination im Kopf."
Manages dynamic execution of the 4 Casted Agents:
- Code Architect
- Security & QA Auditor
- DevOps Engineer
- Apex Quant Specialist
"""
import os
import sys
import json
import time
import argparse
import subprocess
import urllib.request
import urllib.error
from pathlib import Path
from typing import Dict, Any, List, Optional
ROOT_DIR = Path(__file__).resolve().parent.parent
CONFIG_PATH = Path(__file__).resolve().parent / "agents.json"
class AgentOrchestrator:
def __init__(self, config_path: Path = CONFIG_PATH):
if config_path.exists():
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
else:
self.config = {"agents": {}}
def list_agents(self) -> Dict[str, Any]:
"""Returns catalog of casted agents and their capabilities."""
return self.config.get("agents", {})
def probe_compute_cluster(self) -> Dict[str, Any]:
"""Probes local and LAN compute nodes (LM Studio, Ollama, remote Macs)."""
cluster_info = {
"strategy": "hybrid_local_and_lan",
"nodes": {
"local_lmstudio": {"status": "OFFLINE", "models": []},
"local_ollama": {"status": "OFFLINE", "models": []},
"mac_mini": {"status": "CONFIGURED_STANDBY", "endpoint": "http://mac-mini.local:1234/v1"},
"macbook_air": {"status": "CONFIGURED_STANDBY", "endpoint": "http://macbook-air.local:1234/v1"}
}
}
try:
req = urllib.request.urlopen("http://127.0.0.1:1234/v1/models", timeout=0.6)
if req.getcode() == 200:
data = json.loads(req.read().decode('utf-8'))
models = [m.get("id") or m.get("name") for m in data.get("data", [])]
cluster_info["nodes"]["local_lmstudio"] = {
"status": "ONLINE (Apple Silicon Metal)",
"models": models[:2]
}
except Exception:
pass
try:
req = urllib.request.urlopen("http://127.0.0.1:11434/api/tags", timeout=0.6)
if req.getcode() == 200:
data = json.loads(req.read().decode('utf-8'))
models = [m.get("name") for m in data.get("models", [])]
cluster_info["nodes"]["local_ollama"] = {
"status": "ONLINE",
"models": models[:2]
}
except Exception:
pass
return cluster_info
def resolve_compute_routing(self, preferred_node: str) -> Dict[str, Any]:
"""Resolves target compute node with smart automatic failover."""
cluster_info = self.probe_compute_cluster()
nodes = cluster_info.get("nodes", {})
# 1. Check preferred remote node
if preferred_node in nodes and "ONLINE" in nodes[preferred_node].get("status", ""):
return {
"node": preferred_node,
"status": "ONLINE",
"route": "DIRECT_LAN_NODE",
"model": nodes[preferred_node].get("models", ["default"])[0] if nodes[preferred_node].get("models") else "lan_default"
}
# 2. Failover to local LM Studio
if "ONLINE" in nodes.get("local_lmstudio", {}).get("status", ""):
models = nodes["local_lmstudio"].get("models", [])
return {
"node": "primary_local (LM Studio)",
"status": "ACTIVE_FAILOVER",
"route": "LOCAL_METAL_ACCELERATED",
"model": models[0] if models else "qwen3-coder-30b-a3b",
"note": f"Preferred node '{preferred_node}' im Standby. Nahtlos auf Host Metal-GPU geroutet."
}
# 3. Failover to local Ollama
if "ONLINE" in nodes.get("local_ollama", {}).get("status", ""):
models = nodes["local_ollama"].get("models", [])
return {
"node": "primary_local (Ollama)",
"status": "ACTIVE_FAILOVER",
"route": "LOCAL_OLLAMA_FALLBACK",
"model": models[0] if models else "gemma4:latest",
"note": "Auf lokalen Ollama-Knoten umgeleitet."
}
# 4. Deterministic safety net
return {
"node": "deterministic_guard",
"status": "DETERMINISTIC_ACTIVE",
"route": "ZERO_PHANTOM_SAFETY_NET",
"model": "internal_rules_engine",
"note": "Deterministischer Sicherheitsmodus aktiv."
}
def dispatch(self, agent_id: str, task: Optional[str] = None) -> Dict[str, Any]:
"""Dispatches an action to a specified agent and returns physical results."""
agents = self.list_agents()
if agent_id not in agents:
return {
"success": False,
"agent": agent_id,
"error": f"Unknown agent '{agent_id}'. Available: {list(agents.keys())}"
}
agent_meta = agents[agent_id]
assigned_task = task or agent_meta.get("tasks", ["default"])[0]
routing = self.resolve_compute_routing(agent_meta.get("preferred_node", "primary_local"))
start_time = time.time()
# Route to deterministic execution handlers
if agent_id == "architect":
result = self._run_architect(assigned_task)
elif agent_id == "qa":
result = self._run_qa(assigned_task)
elif agent_id == "devops":
result = self._run_devops(assigned_task)
elif agent_id == "quant":
result = self._run_quant(assigned_task)
elif agent_id == "ux":
result = self._run_ux(assigned_task)
else:
result = {"status": "NOOP", "message": "No handler defined"}
duration = round(time.time() - start_time, 4)
return {
"success": result.get("success", True),
"agent_id": agent_id,
"agent_name": agent_meta.get("name"),
"task": assigned_task,
"compute_routing": routing,
"duration_seconds": duration,
"result": result,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
}
def _run_architect(self, task: str) -> Dict[str, Any]:
"""Architect evaluates modular structure and filesystem boundaries."""
modules = {
"platform": (ROOT_DIR / "evoo-platform").is_dir(),
"apex_trading": (ROOT_DIR / "apex-trading").is_dir(),
"orchestrator": (ROOT_DIR / "orchestrator").is_dir(),
"templates": (ROOT_DIR / "templates").is_dir(),
"architecture_doc": (ROOT_DIR / "ARCHITECTURE.md").is_file()
}
all_ok = all(modules.values())
return {
"success": all_ok,
"modular_boundaries": modules,
"clean_architecture": "VERIFIED" if all_ok else "INCOMPLETE",
"findings": "All core pillars are orthogonally separated on disk." if all_ok else "Missing core directories."
}
def _run_qa(self, task: str) -> Dict[str, Any]:
"""QA Auditor runs physical integrity check and regression suite."""
script = ROOT_DIR / "scripts" / "self_improve.py"
if not script.exists():
return {"success": False, "error": "scripts/self_improve.py not found"}
res = subprocess.run([sys.executable, str(script)], capture_output=True, text=True, cwd=str(ROOT_DIR))
return {
"success": res.returncode == 0,
"exit_code": res.returncode,
"output": res.stdout.strip(),
"errors": res.stderr.strip() if res.returncode != 0 else None,
"zero_phantom_check": "PASS" if res.returncode == 0 else "FAIL"
}
def _run_devops(self, task: str) -> Dict[str, Any]:
"""DevOps audits server liveness, port 3088, and PID status."""
pid_file = ROOT_DIR / ".evoo_server.pid"
pid = None
is_running = False
if pid_file.exists():
try:
pid = int(pid_file.read_text().strip())
# Check if process is alive (signal 0)
os.kill(pid, 0)
is_running = True
except (ValueError, OSError):
is_running = False
http_status = None
latency_ms = None
try:
t0 = time.time()
req = urllib.request.urlopen("http://127.0.0.1:3088/health", timeout=1.5)
http_status = req.getcode()
latency_ms = round((time.time() - t0) * 1000, 2)
except Exception as e:
http_status = f"DOWN ({e.__class__.__name__})"
return {
"success": is_running or (http_status == 200),
"pid": pid,
"process_active": is_running,
"port_3088_status": http_status,
"http_latency_ms": latency_ms,
"status": "HEALTHY" if (is_running and http_status == 200) else "DEGRADED"
}
def _run_quant(self, task: str) -> Dict[str, Any]:
"""Apex Quant Specialist runs trading engine tests and risk simulation."""
test_file = ROOT_DIR / "apex-trading" / "test_trading.py"
if not test_file.exists():
return {"success": False, "error": "apex-trading/test_trading.py missing"}
res = subprocess.run([sys.executable, str(test_file)], capture_output=True, text=True, cwd=str(ROOT_DIR))
# Test simulated tick through engine
sim_result = {}
try:
sys_path_added = False
src_path = str(ROOT_DIR / "apex-trading" / "src")
if src_path not in sys.path:
sys.path.insert(0, src_path)
sys_path_added = True
from engine.trader import ApexEngine
config_file = str(ROOT_DIR / "apex-trading" / "config" / "trading.config.json")
engine = ApexEngine(config_file)
# Run tick with strong upward breakout (>1.5% above 20-period SMA)
prices = [100.0 + (i * 2.0) for i in range(25)]
tick = engine.process_tick("BTC/USDC", prices, [500] * 25)
sim_result = {
"simulation_status": tick.get("status"),
"action": tick.get("action"),
"guardrail_status": tick.get("risk_status")
}
except Exception as e:
sim_result = {"error": str(e)}
# 1. Check if external trading-bot API is alive on port 8000
trading_api_status = "OFFLINE"
live_api_metrics = None
try:
req = urllib.request.urlopen("http://127.0.0.1:8000/api/health", timeout=1.0)
if req.getcode() == 200:
trading_api_status = "ONLINE (http://127.0.0.1:8000)"
# Try reading live status
status_req = urllib.request.urlopen("http://127.0.0.1:8000/api/status", timeout=1.0)
live_api_metrics = json.loads(status_req.read().decode('utf-8'))
except Exception:
trading_api_status = "STANDBY (Independent Service Offline)"
return {
"success": res.returncode == 0 and sim_result.get("simulation_status") == "EXECUTED",
"unit_tests_passing": res.returncode == 0,
"simulation": sim_result,
"trading_bot_api_bridge": {
"status": trading_api_status,
"architecture": "Microservice REST API (Port 8000)",
"live_metrics": live_api_metrics
},
"circuit_breaker_enforced": True
}
def _run_ux(self, task: str) -> Dict[str, Any]:
"""UX Designer audits design system integrity, 3-pillar shell, and responsive tokens."""
shell_path = ROOT_DIR / "apps" / "web" / "src" / "pages" / "Shell.tsx"
tokens_path = ROOT_DIR / "packages" / "ui-tokens" / "src" / "tokens.css"
platform_css = ROOT_DIR / "evoo-platform" / "styles.css"
shell_exists = shell_path.exists()
has_3_pillars = False
if shell_exists:
content = shell_path.read_text(encoding="utf-8")
has_3_pillars = "activeNavPillar" in content and "heute" in content and "team" in content and "projekte" in content
tokens_exist = tokens_path.exists() or platform_css.exists()
all_ok = shell_exists and has_3_pillars and tokens_exist
return {
"success": all_ok,
"design_system_integrity": "VERIFIED" if all_ok else "DEGRADED",
"pillar_navigation": "ACTIVE (Heute, Team, Projekte)" if has_3_pillars else "INCOMPLETE",
"mobile_first_ergonomics": "PASS",
"token_library_present": tokens_exist,
"findings": "3-Pillar Navigation & Dark-Mode Design System 100% physisch verifiziert." if all_ok else "UI Komponenten oder Tokens unvollständig."
}
def main():
parser = argparse.ArgumentParser(description="evOo Agent Orchestrator")
parser.add_argument("--agent", choices=["architect", "qa", "devops", "quant", "ux", "all"], default="all")
parser.add_argument("--task", default=None, help="Specific task to run")
parser.add_argument("--cluster", action="store_true", help="Probe compute cluster nodes (LM Studio, Ollama, LAN Macs)")
parser.add_argument("--json", action="store_true", help="Output pure JSON")
args = parser.parse_args()
orchestrator = AgentOrchestrator()
if args.cluster:
cluster = orchestrator.probe_compute_cluster()
if args.json:
print(json.dumps(cluster, indent=2))
else:
print("=" * 60)
print(" evOo Distributed Compute Cluster — Resource Telemetry")
print("=" * 60)
for node, info in cluster.get("nodes", {}).items():
status = info.get("status")
models = ", ".join(info.get("models", [])) or "None loaded"
print(f"[{status}] Node: {node:<18} | Models: {models}")
print("=" * 60)
sys.exit(0)
if args.agent == "all":
results = {}
all_passed = True
for agent_id in ["architect", "qa", "devops", "quant", "ux"]:
res = orchestrator.dispatch(agent_id, args.task)
results[agent_id] = res
if not res.get("success"):
all_passed = False
if args.json:
print(json.dumps(results, indent=2))
else:
print("=" * 60)
print(" evOo Casted Agent Network — Executive Dispatch")
print("=" * 60)
for aid, r in results.items():
status = "✓ PASS" if r.get("success") else "✗ FAIL"
print(f"[{status}] Agent: {r.get('agent_name'):<25} ({r.get('duration_seconds')}s)")
print("=" * 60)
sys.exit(0 if all_passed else 1)
else:
res = orchestrator.dispatch(args.agent, args.task)
if args.json:
print(json.dumps(res, indent=2))
else:
print(f"Agent: {res.get('agent_name')} | Success: {res.get('success')}")
print(json.dumps(res.get("result"), indent=2))
sys.exit(0 if res.get("success") else 1)
if __name__ == "__main__":
main()

View File

@ -1,104 +0,0 @@
{
"orchestrator_version": "2.1.0",
"ecosystem": "evOo Autonomous Agent Platform",
"lead_cto": "Steve Jobs",
"compute_cluster": {
"enabled": true,
"strategy": "smart_failover_hierarchical",
"failover_order": ["preferred_node", "local_lmstudio", "local_ollama", "deterministic_guard"],
"primary_local": {
"name": "evOo Host Machine (LM Studio / Metal)",
"lmstudio_url": "http://127.0.0.1:1234/v1",
"ollama_url": "http://127.0.0.1:11434/v1",
"active_models": ["qwen3-coder-30b-a3b", "gemma4:latest"],
"characteristics": "Direkte Apple Silicon Metal Inferenz, primärer Kontrollknoten"
},
"distributed_nodes": {
"mac_mini": {
"name": "Mac mini (Strategic 24/7 Powerhouse)",
"host": "mac-mini.local",
"url": "http://mac-mini.local:1234/v1",
"recommended_models": ["qwen2.5:32b", "llama3.3:70b-q4", "deepseek-r1:14b"],
"assigned_roles": ["architect", "cto-steve-jobs"],
"characteristics": "Aktiv gekühlt (Lüfter), ideal für schwere 24/7 Deep-Think Modelle",
"thermal_profile": "unlimited"
},
"macbook_air": {
"name": "MacBook Air (Rapid Lightweight Worker)",
"host": "macbook-air.local",
"url": "http://macbook-air.local:1234/v1",
"recommended_models": ["qwen2.5-coder:7b", "llama3.2:3b", "phi3.5:3.8b"],
"assigned_roles": ["qa", "devops"],
"characteristics": "Passiv gekühlt (lüfterlos), optimiert für 3B-7B Modelle ohne Hitze/Throttling",
"thermal_profile": "efficiency_optimized"
}
}
},
"agents": {
"architect": {
"name": "Code Architect",
"slug": "code-architect",
"avatar": "CA",
"color": "var(--primary-glow)",
"role": "Moduldesign & Clean Architecture",
"scope": ["architecture", "filesystem", "modularity"],
"preferred_node": "mac_mini",
"fallback_node": "primary_local",
"target_model_class": "heavy_reasoning",
"description": "Plant orthogonale Modulgrenzen. Nutzt vorzugsweise die Rechenpower des Mac mini für tiefes architektonisches Reasoning.",
"tasks": ["audit_structure", "verify_dependencies"]
},
"qa": {
"name": "Security & QA Auditor",
"slug": "qa-auditor",
"avatar": "QA",
"color": "var(--status-green)",
"role": "Integritätsprüfung & Zero-Phantom",
"scope": ["security", "lint", "tests", "zero_phantom"],
"preferred_node": "macbook_air",
"fallback_node": "primary_local",
"target_model_class": "fast_coding",
"description": "Führt Sicherheits- und Syntaxaudits aus. Nutzt vorzugsweise das MacBook Air für schnelle parallele 7B-Checks.",
"tasks": ["verify_all", "check_security", "check_phantoms"]
},
"devops": {
"name": "DevOps Engineer",
"slug": "devops-engineer",
"avatar": "DO",
"color": "var(--status-blue)",
"role": "Hot-Reload & Service Liveness",
"scope": ["processes", "ports", "deployment", "liveness"],
"preferred_node": "primary_local",
"fallback_node": "primary_local",
"target_model_class": "system_ops",
"description": "Verwaltet Prozesse, Ports und Hot-Reloads direkt auf dem Host.",
"tasks": ["check_liveness", "audit_resources", "restart_service"]
},
"quant": {
"name": "Apex Quant Specialist",
"slug": "apex-quant",
"avatar": "AP",
"color": "var(--accent-purple)",
"role": "Algorithmic Risk & High-Speed Execution",
"scope": ["trading", "risk_guard", "momentum", "quant"],
"preferred_node": "primary_local",
"fallback_node": "primary_local",
"target_model_class": "deterministic_math",
"description": "Überwacht Trading-Guardrails und die HTTP-Bridge zum autonomen trading-bot auf Port 8000.",
"tasks": ["test_guardrails", "run_tick_simulation"]
},
"ux": {
"name": "UX Designer",
"slug": "ux-designer",
"avatar": "UX",
"color": "var(--accent-orange, #f97316)",
"role": "Lead Product & UX/UI Designer",
"scope": ["ui", "ux", "design_tokens", "mobile_ergonomics", "visual_hierarchy"],
"preferred_node": "macbook_air",
"fallback_node": "primary_local",
"target_model_class": "fast_coding",
"description": "Entwirft responsive Mobile-First Interfaces, quantitative Telemetrie-Dashboards und das konsistente Designsystem.",
"tasks": ["audit_ui", "verify_design_tokens"]
}
}
}

View File

@ -53,9 +53,10 @@ async function notifyThreads(deps: DelegationDeps, seqs: ThreadSeq[]) {
}
/**
* Hands work from the main bot to a specialist as a tracked task. The card is posted in the main
* conversation; a specialist of the same space joins it and works there, one of another space
* works in its own chat. Replays of the same tool call return the task created first.
* Hands work from a main bot to a specialist as a tracked task. The card is posted in the main
* conversation of the main bot's space; a specialist of that space joins it and works there, one
* of another space works in its own chat. Replays of the same tool call return the task created
* first.
*/
export async function delegateTask(
deps: DelegationDeps,
@ -63,6 +64,7 @@ export async function delegateTask(
id: string;
botId: string;
userId: string;
spaceId: string;
sourceMessageId?: string | null;
},
sender: { id: string; name: string },
@ -91,8 +93,8 @@ export async function delegateTask(
if (deadlineAt.getTime() <= Date.now()) return { error: "deadline must be in the future" };
}
// Resolving the conversation also settles who the main bot is; only its lead delegates.
const conversation = await ensureMainConversation(deps.prisma, run.userId);
// Resolving the conversation also settles who the space's main bot is; only its lead delegates.
const conversation = await ensureMainConversation(deps.prisma, run.userId, run.spaceId);
if (conversation.leadBotId !== sender.id) {
return { error: "only the main agent can delegate tasks" };
}
@ -115,8 +117,9 @@ export async function delegateTask(
const candidates = await deps.prisma.bot.findMany({
where: {
userId: run.userId,
// Main bots lead their own space and are never delegated to.
kind: "specialist",
archivedAt: null,
id: { not: sender.id },
thread: { isNot: null },
space: { memberships: { some: { userId: run.userId } } },
},
@ -139,7 +142,11 @@ export async function delegateTask(
const targetThreadId = target.thread.id;
const inConversation = target.spaceId === conversation.spaceId;
if (inConversation) {
await joinMainConversation(deps.prisma, { userId: run.userId, botIds: [target.id] });
await joinMainConversation(deps.prisma, {
userId: run.userId,
spaceId: conversation.spaceId,
botIds: [target.id],
});
}
const deliveryKey = `delegated-task:${input.deliveryKey}`;

View File

@ -88,6 +88,7 @@ async function runBot(policy: BotPolicy, calls: Call[] = [], models: Record<stri
updateMany: vi.fn(async () => ({ count: 1 })),
},
thread: { findUniqueOrThrow: vi.fn(async () => ({ id: run.threadId, groupId: null })) },
space: { findUnique: vi.fn(async () => ({ name: "Home", isDefault: true })) },
message: { findMany: vi.fn(async () => []) },
task: { findUniqueOrThrow: vi.fn(async () => ({ id: run.taskId, prompt: "Work" })) },
connection: { findMany: vi.fn(async () => []) },

View File

@ -28,6 +28,8 @@ interface FixtureOptions {
spaceId?: string;
space?: { name: string };
}>;
/** The space the run happens in; a main bot outside the home space leads that project. */
space?: { name: string; isDefault: boolean };
}
/** Runs one turn through the real executor and returns the request the runtime received. */
@ -91,6 +93,9 @@ async function runOnce(options: FixtureOptions = {}) {
})),
},
chatGroup: { findUnique: vi.fn(async () => options.group ?? null) },
space: {
findUnique: vi.fn(async () => options.space ?? { name: "Home", isDefault: true }),
},
message: {
findMany: vi.fn(async () => []),
findUnique: vi.fn(async () => ({ blocks: options.sourceBlocks ?? [] })),
@ -338,6 +343,19 @@ describe("run instructions", () => {
expect(request.instructions).not.toContain("save lasting facts they share with remember");
});
it("describes a project's main bot as the lead of that project", async () => {
const { request } = await runOnce({
bot: { kind: "main", name: "Chief", title: "Project lead", instructions: "" },
space: { name: "Launch", isDefault: false },
});
expect(request.instructions).toContain(
'You lead the project "Launch" for the user: they talk to you about it, and only to you.',
);
expect(request.instructions).not.toContain("You are the user's main agent");
expect(request.instructions).toContain("Give a specialist tracked work with delegate_task");
});
it("shows the main bot every specialist it can delegate to, its own space first", async () => {
const { request, peersFindMany } = await runOnce({
bot: { kind: "main", name: "Chief", title: "Chief of Staff", instructions: "" },
@ -366,6 +384,7 @@ describe("run instructions", () => {
expect.objectContaining({
where: expect.objectContaining({
userId: "user-1",
kind: "specialist",
space: { memberships: { some: { userId: "user-1" } } },
}),
}),

View File

@ -29,10 +29,22 @@ export function formatBotIdentity(bot: BotIdentity): string {
* The main bot's role. Tools are named only when this run has them, so the paragraph never
* points at a tool the model cannot call.
*/
export function formatMainAgentRole(toolNames: ReadonlySet<string>): string {
export function formatMainAgentRole(
toolNames: ReadonlySet<string>,
/** Name of the project this main bot leads; omitted for the user's personal main agent. */
project?: string,
): string {
const projectName = project?.trim() ? escapePromptData(project.trim()) : undefined;
return [
"You are the user's main agent: the one they talk to first and the one who knows them best.",
"Keep the overview of their work and coordinate their specialist bots instead of doing every job yourself; when a specialist delivers, tell the user what it means for them.",
projectName
? `You lead the project "${projectName}" for the user: they talk to you about it, and only to you.`
: "You are the user's main agent: the one they talk to first and the one who knows them best.",
projectName
? "Keep the overview of the project and coordinate its specialist bots instead of doing every job yourself; when a specialist delivers, tell the user what it means for them."
: "Keep the overview of their work and coordinate their specialist bots instead of doing every job yourself; when a specialist delivers, tell the user what it means for them.",
projectName && toolNames.has("spawn_bot")
? "When the project needs a specialist nobody covers yet, create one with spawn_bot."
: undefined,
toolNames.has("delegate_task")
? "Give a specialist tracked work with delegate_task; its result comes back to you."
: undefined,

View File

@ -91,6 +91,7 @@ import {
createSpaceForMember,
createThreadMessageInTransaction,
effectiveMemoryScope,
ensureMainConversation,
findDefaultModelCredential,
findModelCredential,
findSpaceModelTier,
@ -3218,6 +3219,8 @@ export function createRunExecutor(deps: ExecutorDeps) {
userId: run.userId,
name: String(args.name ?? ""),
});
// A project starts as a conversation with its own main agent.
await ensureMainConversation(deps.prisma, run.userId, space.id);
return finish({ ok: true, spaceId: space.id, name: space.name });
} catch (error) {
if (error instanceof SpaceLimitError || error instanceof InvalidSpaceNameError) {
@ -3667,8 +3670,9 @@ export function createRunExecutor(deps: ExecutorDeps) {
await deps.prisma.bot.findMany({
where: {
userId: run.userId,
// Main bots lead their own space and are never delegated to.
kind: "specialist",
archivedAt: null,
id: { not: bot.id },
thread: { isNot: null },
space: { memberships: { some: { userId: run.userId } } },
},
@ -3726,6 +3730,16 @@ export function createRunExecutor(deps: ExecutorDeps) {
)
: undefined;
const timeInstruction = formatCurrentTimeInstruction(userContext.timeZone);
// A main bot outside the home space leads that project.
const mainAgentSpace =
bot.kind === "main"
? await deps.prisma.space.findUnique({
where: { id: run.spaceId },
select: { name: true, isDefault: true },
})
: null;
const mainAgentProject =
mainAgentSpace && !mainAgentSpace.isDefault ? mainAgentSpace.name : undefined;
// Replies to another owner's agent, like a shared channel, only get the basics.
const userInstruction = formatUserContext(userContext, {
basicsOnly: messagingChannelRun || foreignAgentRun,
@ -3758,7 +3772,9 @@ export function createRunExecutor(deps: ExecutorDeps) {
instructions: [
formatBotIdentity(bot),
bot.instructions,
bot.kind === "main" ? formatMainAgentRole(exposedToolNames) : undefined,
bot.kind === "main"
? formatMainAgentRole(exposedToolNames, mainAgentProject)
: undefined,
timeInstruction,
userBlock,
groupContext?.instructions,

View File

@ -313,8 +313,10 @@ export async function consolidateProfile(
// The scripted runtime answers with canned text keyed off the prompt; facts "learned" from it
// would be nonsense. Skip without touching the cursor.
if (!deps.runtime.describe().capabilities.compaction) return;
// Every project has its own main bot; the user's personal one lives in the home space.
const main = await deps.prisma.bot.findFirst({
where: { userId, kind: "main", archivedAt: null },
orderBy: [{ space: { isDefault: "desc" } }, { createdAt: "asc" }],
select: { id: true, spaceId: true, thread: { select: { id: true } } },
});
if (!main?.thread) return;

View File

@ -51,14 +51,14 @@ describe("remote MCP URL policy", () => {
assertSafeRemoteUrl(magicDns, async () => [{ address: "100.64.1.2", family: 4 as const }]),
).resolves.toEqual(new URL(magicDns));
const safeLookup = createSafeLookup(async () => [{ address: "100.119.57.55", family: 4 }]);
const safeLookup = createSafeLookup(async () => [{ address: "100.100.100.100", family: 4 }]);
const result = await new Promise<{ address: string; family?: number }>((resolve, reject) => {
safeLookup("box.tail12345.ts.net", { family: 0, all: false }, (error, address, family) => {
if (error) reject(error);
else resolve({ address: String(address), family });
});
});
expect(result).toEqual({ address: "100.119.57.55", family: 4 });
expect(result).toEqual({ address: "100.100.100.100", family: 4 });
});
it("allows the IPv6 half of a MagicDNS answer", async () => {

View File

@ -39,7 +39,7 @@ export const AgentSecretInputSchema = z.object({
});
export type AgentSecretInput = z.infer<typeof AgentSecretInputSchema>;
/** "main" is the user's single lead agent; every other bot is a "specialist". */
/** "main" leads a space for the user (their home space or a project); every other bot is a "specialist". */
export const BOT_KINDS = ["main", "specialist"] as const;
export const BotKindSchema = z.enum(BOT_KINDS);
export type BotKind = z.infer<typeof BotKindSchema>;
@ -112,7 +112,7 @@ export const REPLY_QUOTE_MAX_LENGTH = 2_000;
export const GROUP_MEMBER_MIN = 2;
export const GROUP_MEMBER_MAX = 6;
/** "main" is the user's main conversation, led by the main bot; it has no member limits. */
/** "main" is a space's main conversation, led by its main bot; it has no member limits. */
export const GROUP_KINDS = ["group", "main"] as const;
export const GroupKindSchema = z.enum(GROUP_KINDS);
export type GroupKind = z.infer<typeof GroupKindSchema>;
@ -303,7 +303,7 @@ export const SpaceSchema = z.object({
});
export type Space = z.infer<typeof SpaceSchema>;
/** Where the user's main bot and main conversation live; the space may differ from the current one. */
/** Where a main bot and its main conversation live; the space may differ from the current one. */
export const MainConversationRefSchema = z.object({
botId: Id,
groupId: Id,

View File

@ -35,6 +35,7 @@ import {
GroupDetailSchema,
GroupSchema,
IntegrationCatalogResultSchema,
MainConversationRefSchema,
McpServerConfigInput,
McpServerSchema,
MemoryDocumentSchema,
@ -195,6 +196,8 @@ export const appContract = {
remove: oc
.input(z.object({ spaceId: Id }))
.output(z.object({ ok: z.literal(true), activeSpaceId: Id })),
/** The space's main conversation, created with its main bot when the user first opens it. */
ensureMain: oc.input(z.object({ spaceId: Id })).output(MainConversationRefSchema),
},
bootstrap: oc.input(z.object({ botId: Id.optional() })).output(AppBootstrapSchema),
deployment: {

View File

@ -45,7 +45,6 @@ export * from "./text-direction.js";
export * from "./thread-message-updates.js";
export * from "./thread-subscription.js";
export * from "./tool-activity.js";
export * from "./trading-bot-status.js";
export * from "./voice-chat-command.js";
export * from "./voice-labels.js";
export * from "./workspace-focus.js";

View File

@ -1,141 +0,0 @@
import { describe, expect, it } from "vitest";
import {
coerceTradingBotStatusView,
interpretTradingBotProxyResponse,
mapTradingBotStatus,
parseTradingBotOrigin,
resolveTradingBotOrigin,
sanitizeTradingBotError,
tradingBotBackoffMs,
tradingBotStatusUrl,
} from "./trading-bot-status.js";
describe("parseTradingBotOrigin", () => {
it("treats an empty value as unset and keeps the origin without a path", () => {
expect(parseTradingBotOrigin(undefined)).toEqual({ kind: "unset" });
expect(parseTradingBotOrigin(" ")).toEqual({ kind: "unset" });
expect(parseTradingBotOrigin(" http://127.0.0.1:8000/bot/ ")).toEqual({
kind: "origin",
origin: "http://127.0.0.1:8000",
});
expect(tradingBotStatusUrl("http://127.0.0.1:8000")).toBe(
"http://127.0.0.1:8000/bot/api/status",
);
});
it("rejects credentials and non-http schemes without echoing the value", () => {
expect(parseTradingBotOrigin("http://user:secret@127.0.0.1:8000").kind).toBe("invalid");
expect(parseTradingBotOrigin("file:///tmp/status").kind).toBe("invalid");
expect(parseTradingBotOrigin("not a url").kind).toBe("invalid");
expect(resolveTradingBotOrigin("")).toBeUndefined();
expect(() => resolveTradingBotOrigin("javascript:alert(1)")).toThrow(
/^Invalid TRADING_BOT_URL$/,
);
});
});
describe("mapTradingBotStatus", () => {
it("maps live, paper, and dry-run badges without defaulting to live", () => {
expect(mapTradingBotStatus({ mode: "LIVE" })).toMatchObject({ mode: "live", kind: "live" });
expect(mapTradingBotStatus({ badge: "DRY RUN" })).toMatchObject({
mode: "paper",
kind: "dry_run",
});
expect(mapTradingBotStatus({ mode: "Paper Sim" })).toMatchObject({
mode: "paper",
kind: "paper",
});
expect(mapTradingBotStatus({ dry_run: true })).toMatchObject({ kind: "dry_run" });
expect(mapTradingBotStatus({ live: true })).toMatchObject({ kind: "live" });
expect(mapTradingBotStatus({ status: "ok" })).toBeNull();
expect(mapTradingBotStatus({})).toBeNull();
expect(mapTradingBotStatus(null)).toBeNull();
expect(mapTradingBotStatus({ live: false })).toBeNull();
});
it("refuses a body that claims live and dry-run together", () => {
expect(mapTradingBotStatus({ mode: "live", dry_run: true })).toBeNull();
expect(mapTradingBotStatus({ live: true, paper: true })).toBeNull();
});
it("reads a nested mode and safe subtitle fields", () => {
expect(
mapTradingBotStatus({
data: { mode: "DRY RUN", adapters: ["a", "b", "c"], last_error: "adapter timeout" },
}),
).toEqual({
mode: "paper",
kind: "dry_run",
adapterCount: 3,
lastError: "adapter timeout",
});
expect(mapTradingBotStatus({ mode: "paper", adapter_count: "4" })?.adapterCount).toBe(4);
});
it("drops secret-like errors and still reports the mode", () => {
expect(
mapTradingBotStatus({ mode: "paper", last_error: "Bearer sk-test-secret" })?.lastError,
).toBeNull();
expect(
mapTradingBotStatus({ mode: "live", error: "failed http://10.0.0.8/status" })?.lastError,
).toBeNull();
expect(sanitizeTradingBotError("ok")).toBeNull();
const long = "x".repeat(120);
expect(sanitizeTradingBotError(long)?.endsWith("…")).toBe(true);
expect(sanitizeTradingBotError(long)?.length).toBe(80);
});
});
describe("interpretTradingBotProxyResponse", () => {
it("keeps unconfigured, offline, unknown, and a live read distinct", () => {
expect(interpretTradingBotProxyResponse(200, { configured: false })).toEqual({
state: "unconfigured",
});
expect(interpretTradingBotProxyResponse(502, { configured: true, reachable: false })).toEqual({
state: "offline",
});
expect(
interpretTradingBotProxyResponse(200, { configured: true, reachable: true, status: null }),
).toEqual({ state: "unknown" });
expect(interpretTradingBotProxyResponse(401, { error: "Unauthorized" })).toEqual({
state: "offline",
});
expect(
interpretTradingBotProxyResponse(200, {
configured: true,
reachable: true,
status: { mode: "live", kind: "live", adapterCount: 1, lastError: null },
}),
).toEqual({
state: "ready",
status: { mode: "live", kind: "live", adapterCount: 1, lastError: null },
});
});
it("strips an unsafe error from an otherwise valid proxy status", () => {
const snapshot = interpretTradingBotProxyResponse(200, {
configured: true,
reachable: true,
status: {
mode: "paper",
kind: "dry_run",
adapterCount: null,
lastError: "token=super-secret",
},
});
expect(snapshot).toEqual({
state: "ready",
status: { mode: "paper", kind: "dry_run", adapterCount: null, lastError: null },
});
expect(coerceTradingBotStatusView({ mode: "live", kind: "paper" })).toBeNull();
});
});
describe("tradingBotBackoffMs", () => {
it("doubles the interval and then caps it", () => {
expect(tradingBotBackoffMs(20_000, 1, 120_000)).toBe(20_000);
expect(tradingBotBackoffMs(20_000, 2, 120_000)).toBe(40_000);
expect(tradingBotBackoffMs(20_000, 3, 120_000)).toBe(80_000);
expect(tradingBotBackoffMs(20_000, 4, 120_000)).toBe(120_000);
});
});

View File

@ -1,297 +0,0 @@
/** Shared Tradingbot status contract. The browser and the API proxy both use this. */
export const TRADING_BOT_STATUS_PATH = "/bot/api/status";
export const TRADING_BOT_PROXY_PATH = "/api/trading-bot/status";
export const TRADING_BOT_POLL_INTERVAL_MS = 20_000;
export const TRADING_BOT_POLL_MAX_BACKOFF_MS = 120_000;
export const TRADING_BOT_STATUS_TIMEOUT_MS = 5_000;
export const TRADING_BOT_STATUS_MAX_BYTES = 65_536;
const MAX_ORIGIN_LENGTH = 200;
const MAX_ERROR_LENGTH = 80;
const MAX_ADAPTER_COUNT = 10_000;
const LIVE_TOKENS = new Set(["live", "live_trading", "real", "production"]);
const PAPER_TOKENS = new Set(["paper", "paper_sim", "paper_trading", "simulation", "sim"]);
const DRY_RUN_TOKENS = new Set(["dry_run", "dryrun", "dry"]);
const MODE_STRING_KEYS = [
"mode",
"trading_mode",
"tradingMode",
"run_mode",
"runMode",
"execution_mode",
"executionMode",
"badge",
"status",
] as const;
const NESTED_KEYS = ["data", "bot", "state", "result", "status"] as const;
const ADAPTER_COUNT_KEYS = [
"adapterCount",
"adapter_count",
"adaptersCount",
"adapters_count",
"n_adapters",
] as const;
const ERROR_KEYS = [
"lastError",
"last_error",
"last_error_message",
"errorMessage",
"error",
] as const;
const EMPTY_ERRORS = new Set(["ok", "none", "null", "undefined", "n/a", "-"]);
const UNSAFE_ERROR = /bearer\s|api[_-]?key|password|secret|token|authorization|cookie|https?:\/\//i;
export type TradingBotKind = "live" | "paper" | "dry_run";
export type TradingBotStatusView = {
mode: "live" | "paper";
kind: TradingBotKind;
adapterCount: number | null;
lastError: string | null;
};
export type TradingBotOrigin =
| { kind: "unset" }
| { kind: "invalid" }
| { kind: "origin"; origin: string };
export type TradingBotSnapshot =
| { state: "unconfigured" }
| { state: "offline" }
| { state: "unknown" }
| { state: "ready"; status: TradingBotStatusView };
/** Accept a bare http(s) origin. Credentials and other schemes are rejected. */
export function parseTradingBotOrigin(value: string | undefined): TradingBotOrigin {
const trimmed = value?.trim() ?? "";
if (!trimmed) return { kind: "unset" };
if (trimmed.length > MAX_ORIGIN_LENGTH || /\s/.test(trimmed) || hasControlChar(trimmed)) {
return { kind: "invalid" };
}
let url: URL;
try {
url = new URL(trimmed);
} catch {
return { kind: "invalid" };
}
if (url.protocol !== "http:" && url.protocol !== "https:") return { kind: "invalid" };
if (url.username || url.password || !url.hostname) return { kind: "invalid" };
return { kind: "origin", origin: url.origin };
}
/** Throw when the operator set a value that must not be fetched. Unset stays unset. */
export function resolveTradingBotOrigin(value: string | undefined): string | undefined {
const parsed = parseTradingBotOrigin(value);
if (parsed.kind === "invalid") throw new Error("Invalid TRADING_BOT_URL");
return parsed.kind === "origin" ? parsed.origin : undefined;
}
export function tradingBotStatusUrl(origin: string): string {
return `${origin}${TRADING_BOT_STATUS_PATH}`;
}
export function tradingBotBackoffMs(
intervalMs: number,
failureCount: number,
maxBackoffMs: number,
): number {
const exponent = Math.min(Math.max(failureCount - 1, 0), 6);
return Math.min(intervalMs * 2 ** exponent, maxBackoffMs);
}
/** Map a Tradingbot `/bot/api/status` body. Unknown or conflicting modes return null. */
export function mapTradingBotStatus(body: unknown): TradingBotStatusView | null {
const record = asRecord(body);
if (!record) return null;
const kind = resolveKind(readSignals(record));
if (!kind) return null;
return {
mode: kind === "live" ? "live" : "paper",
kind,
adapterCount: readAdapterCount(record),
lastError: readLastError(record),
};
}
/** Accept a proxy payload field. Drops unsafe error text instead of dropping the mode. */
export function coerceTradingBotStatusView(value: unknown): TradingBotStatusView | null {
const record = asRecord(value);
if (!record) return null;
const { mode, kind } = record;
if (mode !== "live" && mode !== "paper") return null;
if (kind !== "live" && kind !== "paper" && kind !== "dry_run") return null;
if ((mode === "live") !== (kind === "live")) return null;
if (record.adapterCount !== null && parseCount(record.adapterCount) === null) return null;
if (record.lastError !== null && typeof record.lastError !== "string") return null;
return {
mode,
kind,
adapterCount: record.adapterCount === null ? null : parseCount(record.adapterCount),
lastError: record.lastError === null ? null : sanitizeTradingBotError(record.lastError),
};
}
export function interpretTradingBotProxyResponse(
status: number,
body: unknown,
): TradingBotSnapshot {
const record = asRecord(body);
if (status === 200 && record?.configured === false) return { state: "unconfigured" };
if (record?.configured === true && record.reachable === false) return { state: "offline" };
if (status === 200 && record?.configured === true && record.reachable === true) {
if (record.status == null) return { state: "unknown" };
const view = coerceTradingBotStatusView(record.status);
return view ? { state: "ready", status: view } : { state: "unknown" };
}
return { state: "offline" };
}
/** Keep a short operator-facing error. Drop secrets, URLs, and empty placeholders. */
export function sanitizeTradingBotError(value: unknown): string | null {
if (typeof value !== "string") return null;
const cleaned = stripControlChars(value).replace(/\s+/g, " ").trim();
if (!cleaned || EMPTY_ERRORS.has(cleaned.toLowerCase())) return null;
if (UNSAFE_ERROR.test(cleaned)) return null;
if (cleaned.length <= MAX_ERROR_LENGTH) return cleaned;
return `${cleaned.slice(0, MAX_ERROR_LENGTH - 1)}…`;
}
function readSignals(record: Record<string, unknown>): TradingBotKind[] {
const signals: TradingBotKind[] = [];
pushSignals(record, signals);
if (signals.length > 0) return signals;
for (const key of NESTED_KEYS) {
const nested = asRecord(record[key]);
if (nested) pushSignals(nested, signals);
}
return signals;
}
function pushSignals(record: Record<string, unknown>, signals: TradingBotKind[]): void {
for (const key of MODE_STRING_KEYS) {
const value = record[key];
if (typeof value !== "string") continue;
const kind = classifyModeToken(value);
if (kind) signals.push(kind);
}
if (record.live === true || record.is_live === true || record.isLive === true) {
signals.push("live");
}
if (record.dry_run === true || record.dryRun === true) signals.push("dry_run");
if (
record.paper === true ||
record.is_paper === true ||
record.isPaper === true ||
record.paper_trading === true ||
record.paperTrading === true
) {
signals.push("paper");
}
}
function resolveKind(signals: TradingBotKind[]): TradingBotKind | null {
const unique = new Set(signals);
const live = unique.has("live");
const paper = unique.has("paper");
const dryRun = unique.has("dry_run");
if (live && (paper || dryRun)) return null;
if (dryRun) return "dry_run";
if (paper) return "paper";
if (live) return "live";
return null;
}
function classifyModeToken(value: string): TradingBotKind | null {
const token = value
.trim()
.toLowerCase()
.replace(/[\s-]+/g, "_");
if (LIVE_TOKENS.has(token)) return "live";
if (DRY_RUN_TOKENS.has(token)) return "dry_run";
if (PAPER_TOKENS.has(token)) return "paper";
return null;
}
function readAdapterCount(record: Record<string, unknown>): number | null {
const found = readOptional(record, adapterCountIn);
return found === undefined ? null : found;
}
function adapterCountIn(record: Record<string, unknown>): number | null | undefined {
for (const key of ADAPTER_COUNT_KEYS) {
if (!Object.hasOwn(record, key)) continue;
return parseCount(record[key]);
}
if (Object.hasOwn(record, "adapters") && Array.isArray(record.adapters)) {
return parseCount(record.adapters.length);
}
return undefined;
}
function readLastError(record: Record<string, unknown>): string | null {
const found = readOptional(record, lastErrorIn);
return found === undefined ? null : found;
}
function lastErrorIn(record: Record<string, unknown>): string | null | undefined {
for (const key of ERROR_KEYS) {
if (!Object.hasOwn(record, key)) continue;
return sanitizeTradingBotError(record[key]);
}
return undefined;
}
function readOptional<T>(
record: Record<string, unknown>,
read: (row: Record<string, unknown>) => T | undefined,
): T | undefined {
const direct = read(record);
if (direct !== undefined) return direct;
for (const key of NESTED_KEYS) {
const nested = asRecord(record[key]);
if (!nested) continue;
const value = read(nested);
if (value !== undefined) return value;
}
return undefined;
}
function parseCount(value: unknown): number | null {
if (typeof value === "string" && !/^\d+$/.test(value.trim())) return null;
const numeric =
typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
if (!Number.isInteger(numeric) || numeric < 0 || numeric > MAX_ADAPTER_COUNT) return null;
return numeric;
}
function hasControlChar(value: string): boolean {
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code < 32 || code === 127) return true;
}
return false;
}
function stripControlChars(value: string): string {
let out = "";
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
const char = value[index];
if (char === undefined) continue;
out += code < 32 || code === 127 ? " " : char;
}
return out;
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}

View File

@ -1,28 +0,0 @@
import { createDb } from "./src/client";
process.env.DATABASE_URL = "postgres://rakazo:rakazodev@127.0.0.1:5433/rakazo";
const { prisma: db } = createDb(process.env.DATABASE_URL, { applicationName: "check-users" });
const allUsers = await db.user.findMany({
select: { id: true, email: true, name: true, createdAt: true, emailVerified: true },
orderBy: { createdAt: "desc" },
});
console.log("All users:", JSON.stringify(allUsers, null, 2));
// Check spaces
const allSpaces = await db.space.findMany({
select: { id: true, name: true, ownerId: true, createdAt: true },
orderBy: { createdAt: "desc" },
});
console.log("\nAll spaces:", JSON.stringify(allSpaces, null, 2));
// Check space members
const allMembers = await db.spaceMember.findMany({
select: { id: true, spaceId: true, userId: true, role: true },
});
console.log("\nAll members:", JSON.stringify(allMembers, null, 2));
await db.$disconnect();

View File

@ -0,0 +1,7 @@
-- Every space a user works in (their home space and each project) gets its own main agent and
-- main conversation, so uniqueness moves from the user to the user within a space.
DROP INDEX "bots_userId_main_key";
DROP INDEX "chat_groups_userId_main_key";
CREATE UNIQUE INDEX "bots_userId_spaceId_main_key" ON "bots"("userId", "spaceId") WHERE "kind" = 'main';
CREATE UNIQUE INDEX "chat_groups_userId_spaceId_main_key" ON "chat_groups"("userId", "spaceId") WHERE "kind" = 'main';

View File

@ -402,7 +402,7 @@ model Bot {
webhookSecretId String?
teamChatAmbientEnabled Boolean @default(false)
teamChatRules String @default("")
/// "main" for the user's single lead agent, "specialist" for every other bot.
/// "main" for the user's lead agent of the space (one per user and space), "specialist" otherwise.
kind String @default("specialist")
tags String[] @default([])
restrictBuiltinTools Boolean @default(false)
@ -427,7 +427,7 @@ model Bot {
externalConversations ExternalConversation[]
@@unique([spaceId, spawnKey])
// CHECK bots_kind_chk and partial unique index bots_userId_main_key are created in SQL.
// CHECK bots_kind_chk and partial unique index bots_userId_spaceId_main_key are created in SQL.
@@index([spaceId, userId, archivedAt, pinned, updatedAt])
@@index([sectionId])
@@index([computerId])
@ -473,7 +473,7 @@ model ChatGroup {
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
userId String
name String
/// "main" for the user's main conversation led by the main bot, "group" otherwise.
/// "main" for the space's main conversation led by its main bot, "group" otherwise.
kind String @default("group")
leadBotId String?
leadBot Bot? @relation("ChatGroupLead", fields: [leadBotId], references: [id], onDelete: SetNull)
@ -491,7 +491,7 @@ model ChatGroup {
@@index([spaceId, userId, archivedAt, pinned, updatedAt])
@@index([sectionId])
@@index([leadBotId])
// CHECK chat_groups_kind_chk and partial unique index chat_groups_userId_main_key are created in SQL.
// CHECK chat_groups_kind_chk and partial unique index chat_groups_userId_spaceId_main_key are created in SQL.
@@map("chat_groups")
}

View File

@ -1,13 +0,0 @@
import { createDb, createPool } from "./src/client.js";
const pool = createPool(process.env.DATABASE_URL, { applicationName: "query-users" });
const db = createDb(pool);
const users = await db.user.findMany({
orderBy: { createdAt: "desc" },
take: 10,
select: { email: true, name: true, createdAt: true },
});
console.log(JSON.stringify(users, null, 2));
await db.$disconnect();

Some files were not shown because too many files have changed in this diff Show More