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:
commit
c7d543132d
@ -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.
|
|
||||||
20
.env.example
20
.env.example
@ -210,26 +210,6 @@ RAKAZO_UPDATER_IMAGE_TAG=local
|
|||||||
RAKAZO_UPDATER_URL=
|
RAKAZO_UPDATER_URL=
|
||||||
RAKAZO_UPDATER_TOKEN=
|
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).
|
# 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.
|
# Overridden by localStorage key rakazo.uiLocale when the user picks a language in Settings.
|
||||||
# VITE_DEFAULT_UI_LOCALE=en
|
# VITE_DEFAULT_UI_LOCALE=en
|
||||||
|
|||||||
28
.github/workflows/ci.yml
vendored
28
.github/workflows/ci.yml
vendored
@ -246,6 +246,7 @@ jobs:
|
|||||||
publish-mobile-update:
|
publish-mobile-update:
|
||||||
name: Publish compatible mobile update
|
name: Publish compatible mobile update
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
|
continue-on-error: true
|
||||||
needs: [lint, check, build, test, test-integration, test-e2e]
|
needs: [lint, check, build, test, test-integration, test-e2e]
|
||||||
runs-on: [self-hosted, evoo-ci, Linux]
|
runs-on: [self-hosted, evoo-ci, Linux]
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
@ -344,23 +345,30 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "publish=$publish" >> "$GITHUB_OUTPUT"
|
echo "publish=$publish" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Require Expo automation token
|
- name: Check Expo automation token
|
||||||
|
id: expo_token
|
||||||
if: steps.ota.outputs.publish == 'true'
|
if: steps.ota.outputs.publish == 'true'
|
||||||
env:
|
env:
|
||||||
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
if [[ -z "$EXPO_TOKEN" ]]; then
|
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
|
fi
|
||||||
|
echo "ready=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
|
- 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
|
id: pnpm-setup
|
||||||
with:
|
with:
|
||||||
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }}
|
dest: ${{ runner.temp }}/setup-pnpm-${{ github.run_id }}-${{ github.job }}
|
||||||
- name: Isolate pnpm store and PATH
|
- 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
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@ -377,25 +385,25 @@ jobs:
|
|||||||
mkdir -p "$store"
|
mkdir -p "$store"
|
||||||
echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV"
|
echo "npm_config_store_dir=${store}" >> "$GITHUB_ENV"
|
||||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
- 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:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
- name: Set up Expo
|
- 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
|
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # v9
|
||||||
with:
|
with:
|
||||||
eas-version: latest
|
eas-version: latest
|
||||||
packager: pnpm
|
packager: pnpm
|
||||||
token: ${{ secrets.EXPO_TOKEN }}
|
token: ${{ secrets.EXPO_TOKEN }}
|
||||||
- name: Install dependencies
|
- 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
|
run: pnpm install --frozen-lockfile
|
||||||
- name: Verify mobile project
|
- 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
|
run: pnpm --filter @rakazo/mobile check
|
||||||
- name: Publish production update
|
- 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
|
working-directory: apps/mobile
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,7 +47,6 @@ apps/web/src/locales/**/*.js
|
|||||||
apps/web/scripts/msgids.json
|
apps/web/scripts/msgids.json
|
||||||
playwright-report-www
|
playwright-report-www
|
||||||
*.pid
|
*.pid
|
||||||
.evoo_server.pid
|
|
||||||
|
|
||||||
# Large voice / model vendor blobs (recreate Kokoro separately)
|
# Large voice / model vendor blobs (recreate Kokoro separately)
|
||||||
vendor/
|
vendor/
|
||||||
|
|||||||
@ -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`.
|
|
||||||
@ -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.
|
|
||||||
@ -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
|
|
||||||
@ -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)
|
|
||||||
@ -9,6 +9,6 @@ branch of Ingo Wagner’s evOo stack.
|
|||||||
| Branch | `antigravity-handoff/project-timeline` |
|
| Branch | `antigravity-handoff/project-timeline` |
|
||||||
| Commit | `f00577bb05e3cf68381fafb9cb32929becd1e352` |
|
| Commit | `f00577bb05e3cf68381fafb9cb32929becd1e352` |
|
||||||
| Packaged | 2026-09-20 |
|
| 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.
|
Chaotic multi-remote history from older checkouts was intentionally not carried over.
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
|----|----|
|
|----|----|
|
||||||
| Persönliche Multi-Agenten-App auf dem eigenen Rechner | Personal multi-agent app on your own machine |
|
| 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 |
|
| 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
|
## Quick start
|
||||||
|
|
||||||
@ -53,10 +53,7 @@ Full local notes: **[docs/LOCAL-SETUP.md](./docs/LOCAL-SETUP.md)**
|
|||||||
|
|
||||||
## Antigravity tools kept on this branch
|
## Antigravity tools kept on this branch
|
||||||
|
|
||||||
- `scripts/self_improve.py`
|
|
||||||
- `scripts/autonomous-sentinel.ts`
|
|
||||||
- `packages/core/src/self-update.ts`
|
- `packages/core/src/self-update.ts`
|
||||||
- `orchestrator/` (agent_runner + agents.json)
|
|
||||||
- `apps/desktop/src/local-stack.ts`
|
- `apps/desktop/src/local-stack.ts`
|
||||||
- `packages/adapters/src/pi-local-provider.ts`
|
- `packages/adapters/src/pi-local-provider.ts`
|
||||||
- `scripts/seed-ollama.ts`
|
- `scripts/seed-ollama.ts`
|
||||||
|
|||||||
2
VOICE.md
2
VOICE.md
@ -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 |
|
| 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` |
|
| 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.
|
**Companion tone:** geek-peer (fachsimpeln when wanted) or quiet-doer (execute without condescension) — inferred per user, not one voice for everyone.
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
```
|
|
||||||
@ -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.
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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.")
|
|
||||||
@ -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}")
|
|
||||||
@ -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
|
|
||||||
@ -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()
|
|
||||||
@ -109,7 +109,6 @@ import {
|
|||||||
settleWithTimeout,
|
settleWithTimeout,
|
||||||
TEAM_CHAT_STARTUP_SHUTDOWN_MS,
|
TEAM_CHAT_STARTUP_SHUTDOWN_MS,
|
||||||
} from "./team-chat-startup.js";
|
} from "./team-chat-startup.js";
|
||||||
import { mountTradingBotStatus } from "./trading-bot-proxy.js";
|
|
||||||
import { mountVoiceHttpRoutes } from "./voice.js";
|
import { mountVoiceHttpRoutes } from "./voice.js";
|
||||||
import { mountWebhookHttpRoutes } from "./webhook.js";
|
import { mountWebhookHttpRoutes } from "./webhook.js";
|
||||||
|
|
||||||
@ -530,13 +529,6 @@ export async function createApp(
|
|||||||
resetUrl: email ? new URL("/reset-password", env.webOrigin).href : null,
|
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") {
|
if (localEmailEmulator && env.nodeEnv === "development") {
|
||||||
app.get(
|
app.get(
|
||||||
"/api/dev/emails",
|
"/api/dev/emails",
|
||||||
|
|||||||
@ -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", () => {
|
it("loads an optional integrations catalog mirror", () => {
|
||||||
expect(loadEnv(base).integrationsCatalogUrl).toBeUndefined();
|
expect(loadEnv(base).integrationsCatalogUrl).toBeUndefined();
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import {
|
|||||||
resolveEncryptionKey,
|
resolveEncryptionKey,
|
||||||
resolveScreenProxySecret,
|
resolveScreenProxySecret,
|
||||||
resolveSupervisorToken,
|
resolveSupervisorToken,
|
||||||
resolveTradingBotOrigin,
|
|
||||||
} from "@rakazo/core";
|
} from "@rakazo/core";
|
||||||
|
|
||||||
export { resolveCloudAgentProvider, resolveSandboxProvider } from "@rakazo/adapters";
|
export { resolveCloudAgentProvider, resolveSandboxProvider } from "@rakazo/adapters";
|
||||||
@ -93,8 +92,6 @@ export interface AppEnv {
|
|||||||
updaterToken: string | undefined;
|
updaterToken: string | undefined;
|
||||||
/** Current application image tag; used for compose manual-upgrade command selection. */
|
/** Current application image tag; used for compose manual-upgrade command selection. */
|
||||||
imageTag: string | undefined;
|
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 {
|
export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv {
|
||||||
@ -182,7 +179,6 @@ export function loadEnv(source: NodeJS.ProcessEnv = process.env): AppEnv {
|
|||||||
updaterUrl,
|
updaterUrl,
|
||||||
updaterToken,
|
updaterToken,
|
||||||
imageTag: optional(source.RAKAZO_IMAGE_TAG),
|
imageTag: optional(source.RAKAZO_IMAGE_TAG),
|
||||||
tradingBotUrl: resolveTradingBotOrigin(source.TRADING_BOT_URL),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,13 +43,20 @@ export async function assertNotMainConversation(
|
|||||||
if (group?.kind === "main") throw new ORPCError("BAD_REQUEST", { message });
|
if (group?.kind === "main") throw new ORPCError("BAD_REQUEST", { message });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The main conversation carries the main bot's name. */
|
/** A main conversation carries its main bot's name. */
|
||||||
export async function renameMainConversation(prisma: PrismaClient, userId: string, name: string) {
|
export async function renameMainConversation(
|
||||||
await prisma.chatGroup.updateMany({ where: { userId, kind: "main" }, data: { name } });
|
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.
|
* join it first, so the member mention rules route the message to them.
|
||||||
*/
|
*/
|
||||||
export async function joinAddressedBots(
|
export async function joinAddressedBots(
|
||||||
@ -71,5 +78,9 @@ export async function joinAddressedBots(
|
|||||||
.map((bot) => bot.id);
|
.map((bot) => bot.id);
|
||||||
if (addressed.length === 0) return;
|
if (addressed.length === 0) return;
|
||||||
// Subscribers pick up the group.updated event with the send that follows.
|
// 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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { getScanStatus, startOnboardingScan } from "./onboarding-scan.js";
|
import { getScanStatus, startOnboardingScan } from "./onboarding-scan.js";
|
||||||
|
|
||||||
const createSpaceForMember = vi.hoisted(() => vi.fn());
|
const createSpaceForMember = vi.hoisted(() => vi.fn());
|
||||||
|
const ensureMainConversation = vi.hoisted(() => vi.fn());
|
||||||
vi.mock("@rakazo/db", async (original) => ({
|
vi.mock("@rakazo/db", async (original) => ({
|
||||||
...(await original<typeof db>()),
|
...(await original<typeof db>()),
|
||||||
createSpaceForMember,
|
createSpaceForMember,
|
||||||
|
ensureMainConversation,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const owner = { userId: "user-owner", spaceId: "space-owner", isDeploymentOwner: true };
|
const owner = { userId: "user-owner", spaceId: "space-owner", isDeploymentOwner: true };
|
||||||
@ -72,6 +74,7 @@ describe("onboarding scan", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
createSpaceForMember.mockReset();
|
createSpaceForMember.mockReset();
|
||||||
|
ensureMainConversation.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides a scan from every account except the one that started it", async () => {
|
it("hides a scan from every account except the one that started it", async () => {
|
||||||
@ -127,6 +130,8 @@ describe("onboarding scan", () => {
|
|||||||
name: "app",
|
name: "app",
|
||||||
});
|
});
|
||||||
expect(spaces).toEqual([{ id: "space-1", name: "app", workspacePath: 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.
|
// Scanning again finds the linked project instead of creating a second one.
|
||||||
const again = await startOnboardingScan({ actor: owner, scanHome }, prisma);
|
const again = await startOnboardingScan({ actor: owner, scanHome }, prisma);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { Actor } from "@rakazo/contracts";
|
|||||||
import type { PrismaClient } from "@rakazo/db";
|
import type { PrismaClient } from "@rakazo/db";
|
||||||
import {
|
import {
|
||||||
createSpaceForMember,
|
createSpaceForMember,
|
||||||
|
ensureMainConversation,
|
||||||
InvalidSpaceNameError,
|
InvalidSpaceNameError,
|
||||||
resolveProjectWorkspaceDirectory,
|
resolveProjectWorkspaceDirectory,
|
||||||
SpaceLimitError,
|
SpaceLimitError,
|
||||||
@ -676,6 +677,7 @@ async function createProjects(
|
|||||||
spaceId: space.id,
|
spaceId: space.id,
|
||||||
workspacePath: folder,
|
workspacePath: folder,
|
||||||
});
|
});
|
||||||
|
await ensureMainConversation(prisma, actor.userId, space.id);
|
||||||
created.push({ spaceId: space.id, name: space.name, path: workspace.path });
|
created.push({ spaceId: space.id, name: space.name, path: workspace.path });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,7 +79,7 @@ async function requireBotThread(deps: OnboardingDeps, actor: Actor, botId: strin
|
|||||||
});
|
});
|
||||||
if (!bot?.thread) throw new IsolationError();
|
if (!bot?.thread) throw new IsolationError();
|
||||||
if (bot.kind === "main") {
|
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: { id: conversation.threadId } };
|
||||||
}
|
}
|
||||||
return { bot, thread: bot.thread };
|
return { bot, thread: bot.thread };
|
||||||
|
|||||||
@ -88,7 +88,15 @@ import {
|
|||||||
verifyMcpInstall,
|
verifyMcpInstall,
|
||||||
} from "@rakazo/adapters";
|
} from "@rakazo/adapters";
|
||||||
import type { Auth } from "@rakazo/auth";
|
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 {
|
import {
|
||||||
appContract,
|
appContract,
|
||||||
IntegrationProviderIdSchema,
|
IntegrationProviderIdSchema,
|
||||||
@ -131,6 +139,7 @@ import {
|
|||||||
listSpaceUserBotIds,
|
listSpaceUserBotIds,
|
||||||
newestModelCredentialOrder,
|
newestModelCredentialOrder,
|
||||||
newestVoiceCredentialOrder,
|
newestVoiceCredentialOrder,
|
||||||
|
ownMainAgentsBlockingSpaceDeletion,
|
||||||
Prisma,
|
Prisma,
|
||||||
parseComputerMode,
|
parseComputerMode,
|
||||||
projectWorkspaceDto,
|
projectWorkspaceDto,
|
||||||
@ -491,6 +500,26 @@ export interface RouterDeps {
|
|||||||
* keep the deletion claim renewed forever and block stale-claim recovery. */
|
* keep the deletion claim renewed forever and block stale-claim recovery. */
|
||||||
const SPACE_TEARDOWN_TIMEOUT_MS = 120_000;
|
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 {
|
function spaceTeardownTimeoutMs(): number {
|
||||||
const override = Number(process.env.SPACE_TEARDOWN_TIMEOUT_MS ?? "");
|
const override = Number(process.env.SPACE_TEARDOWN_TIMEOUT_MS ?? "");
|
||||||
return Number.isFinite(override) && override > 0 ? override : 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
|
const workspace = workspacePath
|
||||||
? await wireProjectWorkspace(deps.prisma, { spaceId: space.id, workspacePath })
|
? await wireProjectWorkspace(deps.prisma, { spaceId: space.id, workspacePath })
|
||||||
: null;
|
: 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 {
|
return {
|
||||||
id: space.id,
|
id: space.id,
|
||||||
name: space.name,
|
name: space.name,
|
||||||
@ -609,12 +645,26 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
hasContent: false,
|
hasContent: false,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
workspace,
|
workspace,
|
||||||
bots: [],
|
bots: bots.map(spaceBotDto),
|
||||||
groups: [],
|
groups: groups.map(spaceGroupDto),
|
||||||
externalConversations: [],
|
externalConversations: [],
|
||||||
botSections: [],
|
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 }) => {
|
remove: authed.spaces.remove.handler(async ({ context, input }) => {
|
||||||
let claimId: string | null = null;
|
let claimId: string | null = null;
|
||||||
let claimActive = false;
|
let claimActive = false;
|
||||||
@ -627,6 +677,41 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
spaceId: input.spaceId,
|
spaceId: input.spaceId,
|
||||||
};
|
};
|
||||||
try {
|
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
|
// Claim emptiness before external teardown. Bot and group creation
|
||||||
// take the same lifecycle lock and reject the Space until deletion
|
// take the same lifecycle lock and reject the Space until deletion
|
||||||
// finishes or this claim is released.
|
// 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) {
|
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 bots = await repos.listBots(context.actor);
|
||||||
const bot = bots.find((b) => b.id === input.botId);
|
const bot = bots.find((b) => b.id === input.botId);
|
||||||
@ -1513,19 +1602,7 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
input.groupId,
|
input.groupId,
|
||||||
"The main conversation can't be deleted.",
|
"The main conversation can't be deleted.",
|
||||||
);
|
);
|
||||||
const removed = await groupRepos.removeGroup(context.actor, input.groupId);
|
await removeGroupWithArtifacts(deps, groupRepos, 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);
|
|
||||||
}
|
|
||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@ -4942,6 +5019,11 @@ async function spaceNavigationDto(
|
|||||||
orderBy: { createdAt: "asc" },
|
orderBy: { createdAt: "asc" },
|
||||||
});
|
});
|
||||||
const spaceIds = memberships.map((membership) => membership.spaceId);
|
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 inactiveSpaceIds = spaceIds.filter((spaceId) => spaceId !== actor.spaceId);
|
||||||
const [
|
const [
|
||||||
currentBots,
|
currentBots,
|
||||||
@ -4962,13 +5044,14 @@ async function spaceNavigationDto(
|
|||||||
// Active-only navigation lists miss archived content in other spaces; count any
|
// 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
|
// bot/group in the actor's spaces (including another member's) so a shared
|
||||||
// non-empty space cannot look empty for onboarding redirects.
|
// 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({
|
deps.prisma.bot.findMany({
|
||||||
where: { spaceId: { in: spaceIds } },
|
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
||||||
select: { spaceId: true },
|
select: { spaceId: true },
|
||||||
distinct: ["spaceId"],
|
distinct: ["spaceId"],
|
||||||
}),
|
}),
|
||||||
deps.prisma.chatGroup.findMany({
|
deps.prisma.chatGroup.findMany({
|
||||||
where: { spaceId: { in: spaceIds } },
|
where: { spaceId: { in: spaceIds }, NOT: ownProjectMain },
|
||||||
select: { spaceId: true },
|
select: { spaceId: true },
|
||||||
distinct: ["spaceId"],
|
distinct: ["spaceId"],
|
||||||
}),
|
}),
|
||||||
@ -5014,7 +5097,20 @@ async function spaceNavigationDto(
|
|||||||
(membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore),
|
(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.
|
// A linked folder is on the owner's host; other members see neither its path nor remote.
|
||||||
workspace: actor.isDeploymentOwner ? projectWorkspaceDto(membership.space) : null,
|
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,
|
id: bot.id,
|
||||||
spaceId: bot.spaceId,
|
spaceId: bot.spaceId,
|
||||||
kind: bot.kind,
|
kind: bot.kind,
|
||||||
@ -5028,8 +5124,11 @@ async function spaceNavigationDto(
|
|||||||
preview: bot.preview,
|
preview: bot.preview,
|
||||||
status: bot.status,
|
status: bot.status,
|
||||||
updatedAt: bot.updatedAt,
|
updatedAt: bot.updatedAt,
|
||||||
})),
|
};
|
||||||
groups: spaceGroups.map((group) => ({
|
}
|
||||||
|
|
||||||
|
function spaceGroupDto(group: SpaceGroup): SpaceGroup {
|
||||||
|
return {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
spaceId: group.spaceId,
|
spaceId: group.spaceId,
|
||||||
kind: group.kind,
|
kind: group.kind,
|
||||||
@ -5040,14 +5139,6 @@ async function spaceNavigationDto(
|
|||||||
preview: group.preview,
|
preview: group.preview,
|
||||||
unread: group.unread,
|
unread: group.unread,
|
||||||
updatedAt: group.updatedAt,
|
updatedAt: group.updatedAt,
|
||||||
})),
|
|
||||||
externalConversations: externalConversations.filter(
|
|
||||||
(conversation) => conversation.spaceId === membership.spaceId,
|
|
||||||
),
|
|
||||||
botSections: sectionsFor(membership.spaceId),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
main,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -28,7 +28,11 @@ export default function NewSpace() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.dismissAll();
|
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) {
|
} catch (reason) {
|
||||||
setError(reason instanceof Error ? reason.message : t("Could not create space"));
|
setError(reason instanceof Error ? reason.message : t("Could not create space"));
|
||||||
setPending(false);
|
setPending(false);
|
||||||
|
|||||||
@ -728,7 +728,7 @@ export type MobileBot = Pick<
|
|||||||
| "modelId"
|
| "modelId"
|
||||||
| "thinkingLevel"
|
| "thinkingLevel"
|
||||||
> &
|
> &
|
||||||
Partial<Pick<Bot, "parentBotId" | "spaceId">>;
|
Partial<Pick<Bot, "parentBotId" | "spaceId" | "kind">>;
|
||||||
|
|
||||||
export type MobileBotSection = BotSection;
|
export type MobileBotSection = BotSection;
|
||||||
|
|
||||||
|
|||||||
@ -83,7 +83,7 @@ describe("display and warnings", () => {
|
|||||||
expect(apiBaseWarning("http://127.0.0.1:3100")).toBeNull();
|
expect(apiBaseWarning("http://127.0.0.1:3100")).toBeNull();
|
||||||
expect(apiBaseWarning("http://192.168.1.20:3100")).toBeNull();
|
expect(apiBaseWarning("http://192.168.1.20:3100")).toBeNull();
|
||||||
expect(apiBaseWarning("http://100.64.0.1: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://100.127.255.255:3100")).toBeNull();
|
||||||
expect(apiBaseWarning("http://app.example.com")).toMatch(/https/i);
|
expect(apiBaseWarning("http://app.example.com")).toMatch(/https/i);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -111,6 +111,29 @@ describe("spaceInboxItems", () => {
|
|||||||
expect(items.map((item) => item.type)).toEqual(["bot", "group"]);
|
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", () => {
|
it("keeps empty spaces beside a populated space", () => {
|
||||||
const personal = space({ id: "personal", isDefault: true, bots: [bot] });
|
const personal = space({ id: "personal", isDefault: true, bots: [bot] });
|
||||||
expect(spaceInboxItems([personal, space()]).map((item) => item.type)).toEqual([
|
expect(spaceInboxItems([personal, space()]).map((item) => item.type)).toEqual([
|
||||||
|
|||||||
@ -32,7 +32,10 @@ export function canDeleteInboxSpace(
|
|||||||
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
|
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
|
||||||
return spaces.flatMap((space): InboxSpaceItem[] => {
|
return spaces.flatMap((space): InboxSpaceItem[] => {
|
||||||
const chats = [
|
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 })),
|
...space.groups.map((chat) => ({ type: "group" as const, group: chat, ...chat })),
|
||||||
];
|
];
|
||||||
const items: InboxSpaceItem[] = [];
|
const items: InboxSpaceItem[] = [];
|
||||||
|
|||||||
@ -224,7 +224,6 @@ test("shell surfaces stay readable in light and dark themes", async ({ page }, t
|
|||||||
const sidePanel = page.getByTestId("side-panel");
|
const sidePanel = page.getByTestId("side-panel");
|
||||||
await expect(sidebar.locator("[data-main-conversation]")).toBeVisible();
|
await expect(sidebar.locator("[data-main-conversation]")).toBeVisible();
|
||||||
await expect(sidebar.getByRole("button", { name: "Researcher, 1:1 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 inEachTheme(page, async (theme) => {
|
||||||
await captureScreenshot(page, testInfo, `shell-contrast-${theme}`);
|
await captureScreenshot(page, testInfo, `shell-contrast-${theme}`);
|
||||||
|
|||||||
@ -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();
|
|
||||||
});
|
|
||||||
@ -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 }) => {
|
test("ordinary browsers cannot use local settings", async ({ page }) => {
|
||||||
await page.goto("/desktop-settings");
|
await expect(async () => {
|
||||||
await expect(page.getByRole("alert")).toContainText("Could not open local settings");
|
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();
|
await expect(page.getByRole("button", { name: "Models", exact: true })).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 homeSpaceId = await page.evaluate(() => window.localStorage.getItem("rakazo:space-id"));
|
||||||
const chief = await rpc<{ id: string }>(page, "bots/ensureMain", {});
|
const chief = await rpc<{ id: string }>(page, "bots/ensureMain", {});
|
||||||
const sidebar = page.locator("aside").first();
|
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();
|
await expect(mainEntry).toBeVisible();
|
||||||
// The main bot is reached through the main conversation, not listed again.
|
// The main bot is reached through the main conversation, not listed again.
|
||||||
await expect(sidebar.locator(`[data-roster-bot-id="${chief.id}"]`)).toHaveCount(0);
|
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" });
|
const dialog = page.getByRole("dialog", { name: "New project" });
|
||||||
await dialog.getByLabel("Name").fill("Launch");
|
await dialog.getByLabel("Name").fill("Launch");
|
||||||
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
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
|
await expect
|
||||||
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
|
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
|
||||||
.not.toBe(homeSpaceId);
|
.not.toBe(homeSpaceId);
|
||||||
|
|||||||
@ -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.getByRole("combobox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
|
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.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 captureScreenshot(page, testInfo, "create-chat-sidepanel-closed");
|
||||||
|
|
||||||
await page.getByTestId("toggle-bots-sidebar").click();
|
await page.getByTestId("toggle-bots-sidebar").click();
|
||||||
|
|||||||
87
apps/web/e2e/project-main-agent.spec.ts
Normal file
87
apps/web/e2e/project-main-agent.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
@ -23,7 +23,6 @@ test("sidebar has expected navigation items and no integrations", async ({ page
|
|||||||
// - textbox "Search" (search input)
|
// - textbox "Search" (search input)
|
||||||
// - button "Today"
|
// - button "Today"
|
||||||
// - text "Talk to your Oos" (conversations section header)
|
// - text "Talk to your Oos" (conversations section header)
|
||||||
// - status "Not configured" (trading bot URL unset)
|
|
||||||
// - NO "Integration(en)" button or text
|
// - NO "Integration(en)" button or text
|
||||||
|
|
||||||
// Search: it's a textbox, not a button
|
// 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");
|
const conversationsText = sidebar.getByText("Talk to your Oos");
|
||||||
await expect(conversationsText).toBeVisible({ timeout: 5000 });
|
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
|
// No "Integration(en)" button
|
||||||
const integrationBtn = sidebar.getByRole("button", { name: /Integration(en)?/i });
|
const integrationBtn = sidebar.getByRole("button", { name: /Integration(en)?/i });
|
||||||
await expect(integrationBtn).toHaveCount(0, { timeout: 5000 });
|
await expect(integrationBtn).toHaveCount(0, { timeout: 5000 });
|
||||||
|
|||||||
@ -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();
|
const stamp = Date.now();
|
||||||
await signup(page, `proj-create-${stamp}@rakazo.test`, "password12", "Project Create");
|
await signup(page, `proj-create-${stamp}@rakazo.test`, "password12", "Project Create");
|
||||||
await completeOnboarding(page);
|
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.getByLabel("Name").fill("Customer project");
|
||||||
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
await dialog.getByRole("button", { name: "Create project", exact: true }).click();
|
||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await page.waitForURL(/\/app/);
|
// The new project opens its conversation with the project's main agent.
|
||||||
await page.waitForLoadState("networkidle");
|
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
|
||||||
await expect(projekte.getByText("Customer project", { exact: true })).toBeVisible({
|
await expect(projekte.getByText("Customer project", { exact: true })).toBeVisible({
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
});
|
});
|
||||||
await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount(
|
await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount(
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
await expect(page.getByTestId("project-start-main")).toBeVisible();
|
await expect(
|
||||||
await expect(page.getByTestId("project-start-main")).toHaveText(/Start/);
|
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]) {
|
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 supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group");
|
||||||
const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1];
|
const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1];
|
||||||
expect(supportSpaceId).toBeTruthy();
|
expect(supportSpaceId).toBeTruthy();
|
||||||
await supportSpace.getByRole("button", { name: "Select Customer support" }).click();
|
// A project created from chat comes with its own main agent, reached through its conversation.
|
||||||
await page.waitForURL(/\/app(?:\/)?$/);
|
await supportSpace.getByRole("button", { name: "Chief, main conversation" }).click();
|
||||||
|
await page.waitForURL(/\/app\/g\/[^/]+$/);
|
||||||
await expect
|
await expect
|
||||||
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
|
.poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id")))
|
||||||
.toBe(supportSpaceId);
|
.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(projekte.getByText("Customer support", { exact: true })).toBeVisible();
|
||||||
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
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: /^Chief/ })).toHaveCount(1);
|
||||||
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible();
|
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0);
|
||||||
await captureScreenshot(page, testInfo, "spaces-sidebar");
|
await captureScreenshot(page, testInfo, "spaces-sidebar");
|
||||||
|
|
||||||
await page.evaluate((id) => {
|
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();
|
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();
|
const stamp = Date.now();
|
||||||
await signup(page, `spaces-delete-${stamp}@rakazo.test`, "password12", "Space Owner");
|
await signup(page, `spaces-delete-${stamp}@rakazo.test`, "password12", "Space Owner");
|
||||||
await completeOnboarding(page);
|
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.getByLabel("Name").fill("Temporary");
|
||||||
await dialog.getByRole("button", { name: "Create space", exact: true }).click();
|
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.
|
// Onboarding runs once per user: a new space opens directly in the conversation with its
|
||||||
await page.waitForURL(/\/app(?:\/)?$/);
|
// own main agent, its only bot.
|
||||||
|
await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 });
|
||||||
const projekte = page.locator(".sidebar-projekte");
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible();
|
await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible();
|
||||||
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0);
|
await expect(
|
||||||
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toBeVisible();
|
projectSpace(projekte, "Temporary").getByRole("button", { name: "Chief, main conversation" }),
|
||||||
expect(await rpc<unknown[]>(page, "bots/list", {})).toEqual([]);
|
).toBeVisible();
|
||||||
await captureScreenshot(page, testInfo, "new-space-empty");
|
await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0);
|
||||||
|
expect(await rpc<Array<{ kind: string }>>(page, "bots/list", {})).toEqual([
|
||||||
// The empty space stays selectable instead of trapping the user.
|
expect.objectContaining({ kind: "main" }),
|
||||||
await expect(projekte.getByRole("button", { name: "Select Temporary" }).first()).toBeVisible();
|
]);
|
||||||
|
await captureScreenshot(page, testInfo, "new-space-main-conversation");
|
||||||
|
|
||||||
// Deletion is discoverable through the visible Space actions menu.
|
// Deletion is discoverable through the visible Space actions menu.
|
||||||
await projectSpace(projekte, "Temporary")
|
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 deleteSpaceDialog.getByRole("button", { name: "Delete", exact: true }).click();
|
||||||
await expect(sidebar.getByText("Temporary", { exact: true })).toHaveCount(0);
|
await expect(sidebar.getByText("Temporary", { exact: true })).toHaveCount(0);
|
||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
|
// The project's main agent went with it.
|
||||||
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
await expect(sidebar.getByRole("button", { name: /^Chief/ })).toHaveCount(1);
|
||||||
|
|
||||||
// A stale onboarding URL recovers without duplicating the bot.
|
// 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" });
|
const dialog = page.getByRole("dialog", { name: "New space" });
|
||||||
await dialog.getByLabel("Name").fill("Side");
|
await dialog.getByLabel("Name").fill("Side");
|
||||||
await dialog.getByRole("button", { name: "Create space", exact: true }).click();
|
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");
|
const projekte = page.locator(".sidebar-projekte");
|
||||||
await expect(projekte.getByText("Side", { exact: true })).toBeVisible();
|
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 expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await page.goto("/app");
|
await page.goto("/app");
|
||||||
await expect(page).not.toHaveURL(/\/onboarding/);
|
await expect(page).not.toHaveURL(/\/onboarding/);
|
||||||
await expect(projekte.getByRole("button", { name: "Select Side" }).first()).toBeVisible();
|
// The project's main conversation stays, so the project opens there.
|
||||||
await expect(page.getByTestId("project-start-main")).toBeVisible();
|
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 projectSpace(projekte, "Side").getByRole("button", { name: "Actions for Side" }).click();
|
||||||
await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible();
|
await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible();
|
||||||
|
|||||||
@ -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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -14,7 +14,6 @@ import {
|
|||||||
showSessionUnavailable,
|
showSessionUnavailable,
|
||||||
} from "./lib/session-gate";
|
} from "./lib/session-gate";
|
||||||
import { IntegrationSetupPage } from "./pages/IntegrationSetup";
|
import { IntegrationSetupPage } from "./pages/IntegrationSetup";
|
||||||
import { LocalSettingsPage } from "./pages/LocalSettings";
|
|
||||||
import { McpOAuthCallbackPage } from "./pages/McpOAuthCallback";
|
import { McpOAuthCallbackPage } from "./pages/McpOAuthCallback";
|
||||||
import { ShellPage } from "./pages/Shell";
|
import { ShellPage } from "./pages/Shell";
|
||||||
|
|
||||||
@ -30,9 +29,18 @@ const OnboardingPage = lazyWithRecovery("pages/Onboarding", () =>
|
|||||||
const WelcomePage = lazyWithRecovery("pages/Welcome", () =>
|
const WelcomePage = lazyWithRecovery("pages/Welcome", () =>
|
||||||
import("./pages/Welcome").then((module) => ({ default: module.WelcomePage })),
|
import("./pages/Welcome").then((module) => ({ default: module.WelcomePage })),
|
||||||
);
|
);
|
||||||
|
const LocalSettingsPage = lazyWithRecovery("pages/LocalSettings", () =>
|
||||||
|
import("./pages/LocalSettings").then((module) => ({ default: module.LocalSettingsPage })),
|
||||||
|
);
|
||||||
|
|
||||||
export function App() {
|
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 />;
|
return <SessionApp />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { isEmptyProjectSpace, projectGroupChatsInSpace } from "./project-empty-state.js";
|
import { isEmptyProjectSpace, projectMainGroup } from "./project-empty-state.js";
|
||||||
|
|
||||||
describe("projectGroupChatsInSpace", () => {
|
describe("projectMainGroup", () => {
|
||||||
it("returns project groups in a space without the main conversation", () => {
|
it("finds the main conversation of a space", () => {
|
||||||
const groups = [
|
const groups = [
|
||||||
{ id: "g1", kind: "group" as const, spaceId: "space-a" },
|
{ id: "g1", kind: "group" as const, spaceId: "space-a" },
|
||||||
{ id: "g2", kind: "main" 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);
|
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);
|
expect(isEmptyProjectSpace("project", spaces, [], "home")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,8 +32,8 @@ describe("isEmptyProjectSpace", () => {
|
|||||||
expect(isEmptyProjectSpace("project", spaces, groups, "home")).toBe(false);
|
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" }];
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import type { Group, Space } from "@rakazo/contracts";
|
import type { Group, Space } from "@rakazo/contracts";
|
||||||
|
|
||||||
/** Project-scoped group chats in a space (excludes the main conversation). */
|
/** The project's own main conversation, where the user talks to its main agent. */
|
||||||
export function projectGroupChatsInSpace(
|
export function projectMainGroup<T extends Pick<Group, "kind" | "spaceId">>(
|
||||||
groups: readonly Pick<Group, "id" | "kind" | "spaceId">[],
|
groups: readonly T[],
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
): readonly Pick<Group, "id">[] {
|
): T | undefined {
|
||||||
return groups.filter((group) => group.spaceId === spaceId && group.kind !== "main");
|
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(
|
export function isEmptyProjectSpace(
|
||||||
spaceId: string | null | undefined,
|
spaceId: string | null | undefined,
|
||||||
spaces: readonly Pick<Space, "id" | "isDefault">[],
|
spaces: readonly Pick<Space, "id" | "isDefault">[],
|
||||||
@ -18,5 +18,5 @@ export function isEmptyProjectSpace(
|
|||||||
if (!spaceId || !homeSpaceId || spaceId === homeSpaceId) return false;
|
if (!spaceId || !homeSpaceId || spaceId === homeSpaceId) return false;
|
||||||
const space = spaces.find((item) => item.id === spaceId);
|
const space = spaces.find((item) => item.id === spaceId);
|
||||||
if (!space || space.isDefault) return false;
|
if (!space || space.isDefault) return false;
|
||||||
return projectGroupChatsInSpace(groups, spaceId).length === 0;
|
return !groups.some((group) => group.spaceId === spaceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -46,11 +46,6 @@ msgstr "{0} (über 10 MiB)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr "{0} Adapter"
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr "{0}, 1:1-Unterhaltung"
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr "{0}, Gruppenunterhaltung"
|
msgstr "{0}, Gruppenunterhaltung"
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr "{0}, Hauptunterhaltung"
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} arbeitet"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr "/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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr "1:1-Unterhaltung"
|
msgstr "1:1-Unterhaltung"
|
||||||
@ -545,10 +529,6 @@ msgstr "Schreiben erlauben"
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "Einmal erlaubt"
|
msgstr "Einmal erlaubt"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr "Alpha-Signale"
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "Du hast bereits ein Konto?"
|
msgstr "Du hast bereits ein Konto?"
|
||||||
@ -620,10 +600,6 @@ msgstr "Genehmigen"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "Genehmige diesen Server, damit dein Agent seine Tools verwenden kann."
|
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
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Archivieren"
|
msgstr "Archivieren"
|
||||||
@ -2078,10 +2054,6 @@ msgstr "Entwerfe die Spezifikation…"
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "Bild ziehen, ablegen oder einfügen"
|
msgstr "Bild ziehen, ablegen oder einfügen"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Dry run"
|
|
||||||
msgstr "Probelauf"
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplizieren"
|
msgstr "Duplizieren"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "z. B. Hivenet Agent, Presales, Zeiterfassungs-Bot"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr "Frühere Nachricht"
|
msgstr "Frühere Nachricht"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "Zuerst bearbeiten"
|
msgstr "Zuerst bearbeiten"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "Passwort vergessen?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr "Vorwärts"
|
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
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Kostenlos"
|
msgstr "Kostenlos"
|
||||||
@ -2396,10 +2360,6 @@ msgstr "Updates bündeln"
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr "Gruppen"
|
msgstr "Gruppen"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr "Guten Tag."
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr "Gewohnheiten"
|
msgstr "Gewohnheiten"
|
||||||
@ -2598,10 +2558,6 @@ msgstr "Intervalleinheit"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "Isoliert"
|
msgstr "Isoliert"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr "ist bereit."
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
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."
|
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?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr "Ich höre zu — was ist die Geschichte?"
|
msgstr "Ich höre zu — was ist die Geschichte?"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr "Live-Trading"
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "Frühere Nachrichten laden"
|
msgstr "Frühere Nachrichten laden"
|
||||||
@ -3247,7 +3199,6 @@ msgstr "Es werden keine Nutzungsdaten an Dritte gesendet"
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "Noch keine"
|
msgstr "Noch keine"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr "Nicht konfiguriert"
|
msgstr "Nicht konfiguriert"
|
||||||
@ -3309,10 +3260,6 @@ msgstr "OAuth-Verbindung fehlgeschlagen"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr "OAuth abgelaufen"
|
msgstr "OAuth abgelaufen"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr "Offline"
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr "An"
|
msgstr "An"
|
||||||
@ -3433,10 +3380,6 @@ msgstr "Seite"
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr "PagerDuty-Vorfall"
|
msgstr "PagerDuty-Vorfall"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr "Paper"
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "Zurückstellen"
|
msgstr "Zurückstellen"
|
||||||
@ -3517,10 +3460,6 @@ msgstr "Feile an den Details…"
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr "Frage Geräte ab…"
|
msgstr "Frage Geräte ab…"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr "Portfolio-Check"
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr "POST an"
|
msgstr "POST an"
|
||||||
@ -4030,10 +3969,6 @@ msgstr "Prüfe das Risiko…"
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr "Szene"
|
msgstr "Szene"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr "Bleibe in meinem Fachgebiet…"
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "Schritte"
|
msgstr "Schritte"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr "Steve-Jobs-Audit"
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr "Tools"
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr "Verfolge den Pfad…"
|
msgstr "Verfolge den Pfad…"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr "Trading"
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr "Nicht verfügbar"
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr "Deinstallieren"
|
msgstr "Deinstallieren"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr "Unbekannt"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr "Verknüpfung aufheben"
|
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}."
|
msgid "Voice set to {0}."
|
||||||
msgstr "Stimme auf {0} gesetzt."
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr "Wartet"
|
msgstr "Wartet"
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0} (over 10 MiB)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr "{0} adapters"
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr "{0}, 1:1 conversation"
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr "{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
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} is working"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr "/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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr "1:1 conversation"
|
msgstr "1:1 conversation"
|
||||||
@ -545,10 +529,6 @@ msgstr "Allow writing"
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "Allowed once"
|
msgstr "Allowed once"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr "Alpha-Signale"
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "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."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "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
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Archive"
|
msgstr "Archive"
|
||||||
@ -2078,10 +2054,6 @@ msgstr "Drafting the spec…"
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "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
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplicate"
|
msgstr "Duplicate"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "e.g. Hivenet Agent, Presales, Timesheets bot"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr "Earlier message"
|
msgstr "Earlier message"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "Edit first"
|
msgstr "Edit first"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "Forgot password?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr "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
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Free"
|
msgstr "Free"
|
||||||
@ -2396,10 +2360,6 @@ msgstr "Group updates"
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr "Groups"
|
msgstr "Groups"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr "Guten Tag."
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr "Habits"
|
msgstr "Habits"
|
||||||
@ -2598,10 +2558,6 @@ msgstr "Interval unit"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "Isolated"
|
msgstr "Isolated"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr "ist bereit."
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
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."
|
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?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr "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
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "Load earlier messages"
|
msgstr "Load earlier messages"
|
||||||
@ -3247,7 +3199,6 @@ msgstr "No usage data is sent to third parties"
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "None yet"
|
msgstr "None yet"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr "Not configured"
|
msgstr "Not configured"
|
||||||
@ -3309,10 +3260,6 @@ msgstr "OAuth connection failed"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr "OAuth expired"
|
msgstr "OAuth expired"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr "Offline"
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr "On"
|
msgstr "On"
|
||||||
@ -3433,10 +3380,6 @@ msgstr "Page"
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr "PagerDuty incident"
|
msgstr "PagerDuty incident"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr "Paper"
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "Park"
|
msgstr "Park"
|
||||||
@ -3517,10 +3460,6 @@ msgstr "Polishing the details…"
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr "Polling devices…"
|
msgstr "Polling devices…"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr "Portfolio-Check"
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr "POST to"
|
msgstr "POST to"
|
||||||
@ -4030,10 +3969,6 @@ msgstr "Scanning exposure…"
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr "Scene"
|
msgstr "Scene"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr "Staying in my specialty…"
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "Steps"
|
msgstr "Steps"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr "Steve Jobs Audit"
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr "Tools"
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr "Tracing the path…"
|
msgstr "Tracing the path…"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr "Trading"
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr "Unavailable"
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr "Uninstall"
|
msgstr "Uninstall"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr "Unknown"
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr "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}."
|
msgid "Voice set to {0}."
|
||||||
msgstr "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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr "Waiting"
|
msgstr "Waiting"
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0} (más de 10 MiB)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr ""
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} está trabajando"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr ""
|
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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -545,10 +529,6 @@ msgstr ""
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "Permitido una vez"
|
msgstr "Permitido una vez"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "¿Ya tienes una cuenta?"
|
msgstr "¿Ya tienes una cuenta?"
|
||||||
@ -620,10 +600,6 @@ msgstr "Aprobar"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "Aprueba este servidor para que tu agente use sus herramientas."
|
msgstr "Aprueba este servidor para que tu agente use sus herramientas."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Architektur-Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Archivar"
|
msgstr "Archivar"
|
||||||
@ -2078,10 +2054,6 @@ msgstr ""
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "Arrastra, suelta o pega una imagen"
|
msgstr "Arrastra, suelta o pega una imagen"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Dry run"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplicar"
|
msgstr "Duplicar"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "p. ej. Hivenet Agent, Presales, bot de hojas de horas"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr "Mensaje anterior"
|
msgstr "Mensaje anterior"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "Editar primero"
|
msgstr "Editar primero"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "¿Olvidaste la contraseña?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Gratis"
|
msgstr "Gratis"
|
||||||
@ -2396,10 +2360,6 @@ msgstr ""
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2598,10 +2558,6 @@ msgstr "Unidad del intervalo"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "Aislado"
|
msgstr "Aislado"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
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."
|
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?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "Cargar mensajes anteriores"
|
msgstr "Cargar mensajes anteriores"
|
||||||
@ -3247,7 +3199,6 @@ msgstr ""
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "Ninguno aún"
|
msgstr "Ninguno aún"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3309,10 +3260,6 @@ msgstr "Falló la conexión OAuth"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr "OAuth expirado"
|
msgstr "OAuth expirado"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3433,10 +3380,6 @@ msgstr ""
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr "Incidente de PagerDuty"
|
msgstr "Incidente de PagerDuty"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "Aparcar"
|
msgstr "Aparcar"
|
||||||
@ -3517,10 +3460,6 @@ msgstr ""
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr "POST a"
|
msgstr "POST a"
|
||||||
@ -4030,10 +3969,6 @@ msgstr ""
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr ""
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "Pasos"
|
msgstr "Pasos"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr ""
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr "No disponible"
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr "Desvincular"
|
msgstr "Desvincular"
|
||||||
@ -4836,14 +4759,6 @@ msgstr ""
|
|||||||
msgid "Voice set to {0}."
|
msgid "Voice set to {0}."
|
||||||
msgstr ""
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0} (10 MiB से अधिक)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr ""
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} काम कर रहा है"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr ""
|
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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -545,10 +529,6 @@ msgstr ""
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "एक बार अनुमति दी गई"
|
msgstr "एक बार अनुमति दी गई"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "पहले से खाता है?"
|
msgstr "पहले से खाता है?"
|
||||||
@ -620,10 +600,6 @@ msgstr "अनुमोदित करें"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "अपने एजेंट को इसके टूल उपयोग करने देने के लिए इस सर्वर को अनुमोदित करें।"
|
msgstr "अपने एजेंट को इसके टूल उपयोग करने देने के लिए इस सर्वर को अनुमोदित करें।"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Architektur-Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "आर्काइव करें"
|
msgstr "आर्काइव करें"
|
||||||
@ -2078,10 +2054,6 @@ msgstr ""
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "छवि खींचें, छोड़ें या पेस्ट करें"
|
msgstr "छवि खींचें, छोड़ें या पेस्ट करें"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Dry run"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "डुप्लिकेट"
|
msgstr "डुप्लिकेट"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "उदा. Hivenet Agent, Presales, टाइमशीट बॉट"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr "पुराना संदेश"
|
msgstr "पुराना संदेश"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "पहले संपादित करें"
|
msgstr "पहले संपादित करें"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "पासवर्ड भूल गए?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2396,10 +2360,6 @@ msgstr ""
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2598,10 +2558,6 @@ msgstr "अंतराल इकाई"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "पृथक"
|
msgstr "पृथक"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।"
|
msgstr "इसकी बातचीत, फ़ाइलें और रूटीन स्थायी रूप से डिलीट कर दिए जाएंगे। इसके बनाए बॉट आपकी सूची में बने रहेंगे।"
|
||||||
@ -2697,10 +2653,6 @@ msgstr ""
|
|||||||
msgid "Listening — what's the story?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "पुराने संदेश लोड करें"
|
msgstr "पुराने संदेश लोड करें"
|
||||||
@ -3247,7 +3199,6 @@ msgstr ""
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "अभी तक कोई नहीं"
|
msgstr "अभी तक कोई नहीं"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3309,10 +3260,6 @@ msgstr "OAuth कनेक्शन विफल"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3433,10 +3380,6 @@ msgstr ""
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "पार्क करें"
|
msgstr "पार्क करें"
|
||||||
@ -3517,10 +3460,6 @@ msgstr ""
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4030,10 +3969,6 @@ msgstr ""
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr ""
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "चरण"
|
msgstr "चरण"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr ""
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr ""
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4836,14 +4759,6 @@ msgstr ""
|
|||||||
msgid "Voice set to {0}."
|
msgid "Voice set to {0}."
|
||||||
msgstr ""
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0} (10 MiB 초과)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr ""
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} 작업 중"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr ""
|
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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -545,10 +529,6 @@ msgstr ""
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "한 번 허용됨"
|
msgstr "한 번 허용됨"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "이미 계정이 있나요?"
|
msgstr "이미 계정이 있나요?"
|
||||||
@ -620,10 +600,6 @@ msgstr "승인"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "이 서버를 승인하면 Bot이 제공되는 도구를 사용할 수 있습니다."
|
msgstr "이 서버를 승인하면 Bot이 제공되는 도구를 사용할 수 있습니다."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Architektur-Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "보관"
|
msgstr "보관"
|
||||||
@ -2078,10 +2054,6 @@ msgstr ""
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "이미지를 끌어다 놓거나 붙여넣으세요"
|
msgstr "이미지를 끌어다 놓거나 붙여넣으세요"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Dry run"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "복제"
|
msgstr "복제"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "예: Hivenet Agent, Presales, 근무표 봇"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr "이전 메시지"
|
msgstr "이전 메시지"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "수정 후 보내기"
|
msgstr "수정 후 보내기"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "비밀번호를 잊으셨나요?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "무료"
|
msgstr "무료"
|
||||||
@ -2396,10 +2360,6 @@ msgstr ""
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2598,10 +2558,6 @@ msgstr "간격 단위"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "이 Bot만 사용"
|
msgstr "이 Bot만 사용"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다."
|
msgstr "대화, 파일, 자동 실행이 영구 삭제됩니다. 이 Bot이 만든 다른 Bot은 목록에 남습니다."
|
||||||
@ -2697,10 +2653,6 @@ msgstr ""
|
|||||||
msgid "Listening — what's the story?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "이전 메시지 불러오기"
|
msgstr "이전 메시지 불러오기"
|
||||||
@ -3247,7 +3199,6 @@ msgstr ""
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "아직 없음"
|
msgstr "아직 없음"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3309,10 +3260,6 @@ msgstr "OAuth 연결 실패"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3433,10 +3380,6 @@ msgstr ""
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr "PagerDuty 인시던트"
|
msgstr "PagerDuty 인시던트"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "보류"
|
msgstr "보류"
|
||||||
@ -3517,10 +3460,6 @@ msgstr ""
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr "POST 대상"
|
msgstr "POST 대상"
|
||||||
@ -4030,10 +3969,6 @@ msgstr ""
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr ""
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "실행 단계"
|
msgstr "실행 단계"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr ""
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr "사용할 수 없음"
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr "연결 해제"
|
msgstr "연결 해제"
|
||||||
@ -4836,14 +4759,6 @@ msgstr ""
|
|||||||
msgid "Voice set to {0}."
|
msgid "Voice set to {0}."
|
||||||
msgstr ""
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0} (acima de 10 MiB)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr ""
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} está trabalhando"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr ""
|
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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -545,10 +529,6 @@ msgstr ""
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "Permitido uma vez"
|
msgstr "Permitido uma vez"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "Você já tem cadastro?"
|
msgstr "Você já tem cadastro?"
|
||||||
@ -620,10 +600,6 @@ msgstr "Aprovar"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "Aprove este servidor para permitir que seu agente use suas ferramentas."
|
msgstr "Aprove este servidor para permitir que seu agente use suas ferramentas."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Architektur-Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Arquivar"
|
msgstr "Arquivar"
|
||||||
@ -2078,10 +2054,6 @@ msgstr ""
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "Arraste, solte ou cole uma imagem"
|
msgstr "Arraste, solte ou cole uma imagem"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Dry run"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplicar"
|
msgstr "Duplicar"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "ex.: Hivenet Agent, Presales, bot de planilhas"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "Editar primeiro"
|
msgstr "Editar primeiro"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "Esqueceu a senha?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2396,10 +2360,6 @@ msgstr ""
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2598,10 +2558,6 @@ msgstr "Unidade do intervalo:"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "Isolado"
|
msgstr "Isolado"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
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."
|
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?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "Carregar mensagens anteriores..."
|
msgstr "Carregar mensagens anteriores..."
|
||||||
@ -3247,7 +3199,6 @@ msgstr ""
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "Nenhuma ainda"
|
msgstr "Nenhuma ainda"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3309,10 +3260,6 @@ msgstr "Falha na conexão OAuth"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3433,10 +3380,6 @@ msgstr ""
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "Suspender"
|
msgstr "Suspender"
|
||||||
@ -3517,10 +3460,6 @@ msgstr ""
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4030,10 +3969,6 @@ msgstr ""
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr ""
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "Passos"
|
msgstr "Passos"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr ""
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr ""
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4836,14 +4759,6 @@ msgstr ""
|
|||||||
msgid "Voice set to {0}."
|
msgid "Voice set to {0}."
|
||||||
msgstr ""
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0} (более 10 МБ)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr ""
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} работает"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr ""
|
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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -545,10 +529,6 @@ msgstr ""
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "Разрешено один раз"
|
msgstr "Разрешено один раз"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "У вас уже есть аккаунт?"
|
msgstr "У вас уже есть аккаунт?"
|
||||||
@ -620,10 +600,6 @@ msgstr "Утвердить"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "Одобрите этот сервер, чтобы ваш агент мог использовать его инструменты."
|
msgstr "Одобрите этот сервер, чтобы ваш агент мог использовать его инструменты."
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Architektur-Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Архивировать"
|
msgstr "Архивировать"
|
||||||
@ -2078,10 +2054,6 @@ msgstr ""
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "Перетащите или вставьте изображение"
|
msgstr "Перетащите или вставьте изображение"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Dry run"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Дублировать"
|
msgstr "Дублировать"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "напр. Hivenet Agent, Presales, бот табелей"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr "Предыдущее сообщение"
|
msgstr "Предыдущее сообщение"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "Сначала отредактируйте"
|
msgstr "Сначала отредактируйте"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "Забыли пароль?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "Бесплатно"
|
msgstr "Бесплатно"
|
||||||
@ -2396,10 +2360,6 @@ msgstr "Обновления группы"
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr "Группы"
|
msgstr "Группы"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2598,10 +2558,6 @@ msgstr "Единица интервала"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "изолированный"
|
msgstr "изолированный"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке."
|
msgstr "Его диалог, файлы и задачи будут безвозвратно удалены. Созданные им боты останутся в вашем списке."
|
||||||
@ -2697,10 +2653,6 @@ msgstr ""
|
|||||||
msgid "Listening — what's the story?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "Загрузить более ранние сообщения"
|
msgstr "Загрузить более ранние сообщения"
|
||||||
@ -3247,7 +3199,6 @@ msgstr ""
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "Пока нет"
|
msgstr "Пока нет"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3309,10 +3260,6 @@ msgstr "Соединение OAuth не удалось"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr "Срок действия OAuth истек"
|
msgstr "Срок действия OAuth истек"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3433,10 +3380,6 @@ msgstr ""
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr "Инцидент PagerDuty"
|
msgstr "Инцидент PagerDuty"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "Приостановить"
|
msgstr "Приостановить"
|
||||||
@ -3517,10 +3460,6 @@ msgstr ""
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr "ОТПРАВИТЬ в"
|
msgstr "ОТПРАВИТЬ в"
|
||||||
@ -4030,10 +3969,6 @@ msgstr ""
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr ""
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "Шаги"
|
msgstr "Шаги"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr "Инструменты"
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr "Недоступно"
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr "Удалить"
|
msgstr "Удалить"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr "Отсоединить"
|
msgstr "Отсоединить"
|
||||||
@ -4836,14 +4759,6 @@ msgstr ""
|
|||||||
msgid "Voice set to {0}."
|
msgid "Voice set to {0}."
|
||||||
msgstr ""
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0} (10 MiB üzeri)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr ""
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} çalışıyor"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr ""
|
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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -545,10 +529,6 @@ msgstr ""
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "Bir kez izin verildi"
|
msgstr "Bir kez izin verildi"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "Zaten hesabın var mı?"
|
msgstr "Zaten hesabın var mı?"
|
||||||
@ -620,10 +600,6 @@ msgstr "Onayla"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
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."
|
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
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "Arşivle"
|
msgstr "Arşivle"
|
||||||
@ -2078,10 +2054,6 @@ msgstr ""
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "Görüntüyü sürükleyip bırakın veya yapıştırın"
|
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
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Çoğalt"
|
msgstr "Çoğalt"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "örn. Hivenet Agent, Presales, puantaj botu"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "Önce düzenle"
|
msgstr "Önce düzenle"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "Parolanızı mı unuttunuz?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2396,10 +2360,6 @@ msgstr ""
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2598,10 +2558,6 @@ msgstr "Aralık birimi"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "Yalıtılmış"
|
msgstr "Yalıtılmış"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
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."
|
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?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "Önceki mesajları yükle"
|
msgstr "Önceki mesajları yükle"
|
||||||
@ -3247,7 +3199,6 @@ msgstr ""
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "Henüz yok"
|
msgstr "Henüz yok"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3309,10 +3260,6 @@ msgstr "OAuth bağlantısı başarısız"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3433,10 +3380,6 @@ msgstr ""
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "Beklet"
|
msgstr "Beklet"
|
||||||
@ -3517,10 +3460,6 @@ msgstr ""
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4030,10 +3969,6 @@ msgstr ""
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr ""
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "Adımlar"
|
msgstr "Adımlar"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr ""
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr ""
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4836,14 +4759,6 @@ msgstr ""
|
|||||||
msgid "Voice set to {0}."
|
msgid "Voice set to {0}."
|
||||||
msgstr ""
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -46,11 +46,6 @@ msgstr "{0}(超过 10 MiB)"
|
|||||||
msgid "{0} {unitLabel}"
|
msgid "{0} {unitLabel}"
|
||||||
msgstr "{0} {unitLabel}"
|
msgstr "{0} {unitLabel}"
|
||||||
|
|
||||||
#. placeholder {0}: subtitle.count
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "{0} adapters"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#. placeholder {0}: block.name
|
#. placeholder {0}: block.name
|
||||||
#: src/pages/shell/message-cards.tsx
|
#: src/pages/shell/message-cards.tsx
|
||||||
msgid "{0} connection"
|
msgid "{0} connection"
|
||||||
@ -128,6 +123,11 @@ msgstr ""
|
|||||||
msgid "{0}, group conversation"
|
msgid "{0}, group conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. placeholder {0}: item.chat.name
|
||||||
|
#: src/pages/Shell.tsx
|
||||||
|
msgid "{0}, main conversation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: active.name
|
#. placeholder {0}: active.name
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "{0}'s screen"
|
msgid "{0}'s screen"
|
||||||
@ -273,22 +273,6 @@ msgstr "{workingBotName} 正在工作"
|
|||||||
msgid "/Users/you/projects/acme"
|
msgid "/Users/you/projects/acme"
|
||||||
msgstr ""
|
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
|
#: src/components/stage/OoStage.tsx
|
||||||
msgid "1:1 conversation"
|
msgid "1:1 conversation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -545,10 +529,6 @@ msgstr ""
|
|||||||
msgid "Allowed once"
|
msgid "Allowed once"
|
||||||
msgstr "已允许一次"
|
msgstr "已允许一次"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Alpha-Signale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Auth.tsx
|
#: src/pages/Auth.tsx
|
||||||
msgid "Already have an account?"
|
msgid "Already have an account?"
|
||||||
msgstr "已有账户?"
|
msgstr "已有账户?"
|
||||||
@ -620,10 +600,6 @@ msgstr "批准"
|
|||||||
msgid "Approve this server to let your agent use its tools."
|
msgid "Approve this server to let your agent use its tools."
|
||||||
msgstr "批准此服务器后,Agent 才能使用它的工具。"
|
msgstr "批准此服务器后,Agent 才能使用它的工具。"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Architektur-Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Archive"
|
msgid "Archive"
|
||||||
msgstr "归档"
|
msgstr "归档"
|
||||||
@ -2078,10 +2054,6 @@ msgstr ""
|
|||||||
msgid "Drag, drop, or paste an image"
|
msgid "Drag, drop, or paste an image"
|
||||||
msgstr "拖放或粘贴图片"
|
msgstr "拖放或粘贴图片"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Dry run"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/BotContextMenu.tsx
|
#: src/pages/BotContextMenu.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "复制"
|
msgstr "复制"
|
||||||
@ -2094,10 +2066,6 @@ msgstr "例如 Hivenet Agent、Presales、工时表机器人"
|
|||||||
msgid "Earlier message"
|
msgid "Earlier message"
|
||||||
msgstr "更早的消息"
|
msgstr "更早的消息"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Echtzeit-Risiko & Drawdown-Analyse"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/AskCard.tsx
|
#: src/components/AskCard.tsx
|
||||||
msgid "Edit first"
|
msgid "Edit first"
|
||||||
msgstr "先编辑"
|
msgstr "先编辑"
|
||||||
@ -2316,10 +2284,6 @@ msgstr "忘记密码?"
|
|||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Frag {activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
msgid "Free"
|
msgid "Free"
|
||||||
msgstr "免费"
|
msgstr "免费"
|
||||||
@ -2396,10 +2360,6 @@ msgstr ""
|
|||||||
msgid "Groups"
|
msgid "Groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Guten Tag."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/UserProfileSettings.tsx
|
#: src/components/UserProfileSettings.tsx
|
||||||
msgid "Habits"
|
msgid "Habits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -2598,10 +2558,6 @@ msgstr "间隔单位"
|
|||||||
msgid "Isolated"
|
msgid "Isolated"
|
||||||
msgstr "隔离"
|
msgstr "隔离"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "ist bereit."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/shell/dialogs.tsx
|
#: src/pages/shell/dialogs.tsx
|
||||||
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
msgid "Its conversation, files, and routines will be permanently deleted. Bots it created stay in your list."
|
||||||
msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。"
|
msgstr "它的对话、文件和例行任务将被永久删除。它创建的 Bot 仍留在列表中。"
|
||||||
@ -2697,10 +2653,6 @@ msgstr ""
|
|||||||
msgid "Listening — what's the story?"
|
msgid "Listening — what's the story?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Live Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Load earlier messages"
|
msgid "Load earlier messages"
|
||||||
msgstr "加载更早的消息"
|
msgstr "加载更早的消息"
|
||||||
@ -3247,7 +3199,6 @@ msgstr ""
|
|||||||
msgid "None yet"
|
msgid "None yet"
|
||||||
msgstr "暂无"
|
msgstr "暂无"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
#: src/pages/PrivacySettingsOverlay.tsx
|
#: src/pages/PrivacySettingsOverlay.tsx
|
||||||
msgid "Not configured"
|
msgid "Not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3309,10 +3260,6 @@ msgstr "OAuth 连接失败"
|
|||||||
msgid "OAuth expired"
|
msgid "OAuth expired"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Offline"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/VoiceSettingsOverlay.tsx
|
#: src/pages/VoiceSettingsOverlay.tsx
|
||||||
msgid "On"
|
msgid "On"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3433,10 +3380,6 @@ msgstr ""
|
|||||||
msgid "PagerDuty incident"
|
msgid "PagerDuty incident"
|
||||||
msgstr "PagerDuty 事件"
|
msgstr "PagerDuty 事件"
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Paper"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
msgid "Park"
|
msgid "Park"
|
||||||
msgstr "搁置"
|
msgstr "搁置"
|
||||||
@ -3517,10 +3460,6 @@ msgstr ""
|
|||||||
msgid "Polling devices…"
|
msgid "Polling devices…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Portfolio-Check"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "POST to"
|
msgid "POST to"
|
||||||
msgstr "POST 到"
|
msgstr "POST 到"
|
||||||
@ -4030,10 +3969,6 @@ msgstr ""
|
|||||||
msgid "Scene"
|
msgid "Scene"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Schlichtheit, Eleganz & Detailtiefe"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/ModelSettingsOverlay.tsx
|
#: src/pages/ModelSettingsOverlay.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
@ -4392,10 +4327,6 @@ msgstr ""
|
|||||||
msgid "Steps"
|
msgid "Steps"
|
||||||
msgstr "步骤"
|
msgstr "步骤"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
|
||||||
msgid "Steve Jobs Audit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/browser/InAppBrowser.tsx
|
#: src/components/browser/InAppBrowser.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Stop"
|
msgid "Stop"
|
||||||
@ -4651,10 +4582,6 @@ msgstr ""
|
|||||||
msgid "Tracing the path…"
|
msgid "Tracing the path…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Trading"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/components/voice/VoiceStage.tsx
|
#: src/components/voice/VoiceStage.tsx
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
msgid "Transcript"
|
msgid "Transcript"
|
||||||
@ -4692,10 +4619,6 @@ msgstr "不可用"
|
|||||||
msgid "Uninstall"
|
msgid "Uninstall"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/TradingModeIndicator.tsx
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: src/pages/MessagingSettingsOverlay.tsx
|
#: src/pages/MessagingSettingsOverlay.tsx
|
||||||
msgid "Unlink"
|
msgid "Unlink"
|
||||||
msgstr "取消关联"
|
msgstr "取消关联"
|
||||||
@ -4836,14 +4759,6 @@ msgstr ""
|
|||||||
msgid "Voice set to {0}."
|
msgid "Voice set to {0}."
|
||||||
msgstr ""
|
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
|
#: src/pages/ProjectSessionTimeline.tsx
|
||||||
msgid "Waiting"
|
msgid "Waiting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@ -16,10 +16,8 @@ import { SuccessPop } from "../components/ai/primitives";
|
|||||||
import { ComputersUnavailableHint } from "../components/ComputersUnavailableHint";
|
import { ComputersUnavailableHint } from "../components/ComputersUnavailableHint";
|
||||||
import { DesktopUpdateSection } from "../components/DesktopUpdates";
|
import { DesktopUpdateSection } from "../components/DesktopUpdates";
|
||||||
import { SoftwareUpdateSection } from "../components/SoftwareUpdateSection";
|
import { SoftwareUpdateSection } from "../components/SoftwareUpdateSection";
|
||||||
import { TradingModeIndicator } from "../components/TradingModeIndicator";
|
|
||||||
import { authClient } from "../lib/auth";
|
import { authClient } from "../lib/auth";
|
||||||
import { getActiveUiLocale, setUiLocale } from "../lib/i18n";
|
import { getActiveUiLocale, setUiLocale } from "../lib/i18n";
|
||||||
import type { TradingBotUiState } from "../lib/trading-bot-status";
|
|
||||||
import {
|
import {
|
||||||
type AppearancePreference,
|
type AppearancePreference,
|
||||||
getUiAppearancePreference,
|
getUiAppearancePreference,
|
||||||
@ -35,25 +33,8 @@ export type SettingsGeneralProps = {
|
|||||||
messagingEnabled?: boolean;
|
messagingEnabled?: boolean;
|
||||||
onOpenMessaging?: () => void;
|
onOpenMessaging?: () => void;
|
||||||
isDeploymentOwner?: boolean;
|
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({
|
export function GeneralSettingsPanels({
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
@ -62,7 +43,6 @@ export function GeneralSettingsPanels({
|
|||||||
messagingEnabled = false,
|
messagingEnabled = false,
|
||||||
onOpenMessaging,
|
onOpenMessaging,
|
||||||
isDeploymentOwner = false,
|
isDeploymentOwner = false,
|
||||||
tradingStatus,
|
|
||||||
}: SettingsGeneralProps) {
|
}: SettingsGeneralProps) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale());
|
const [locale, setLocale] = useState<UiLocale>(() => getActiveUiLocale());
|
||||||
@ -98,7 +78,6 @@ export function GeneralSettingsPanels({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
{tradingStatus ? <LiveTradingBanner snapshot={tradingStatus} /> : null}
|
|
||||||
<section className="rounded-xl border border-border px-4 py-4">
|
<section className="rounded-xl border border-border px-4 py-4">
|
||||||
<h3 className="text-ui-body font-medium text-foreground">
|
<h3 className="text-ui-body font-medium text-foreground">
|
||||||
<Trans>Account</Trans>
|
<Trans>Account</Trans>
|
||||||
|
|||||||
@ -1,20 +1,31 @@
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { Button } from "@rakazo/ui-web";
|
import { Button } from "@rakazo/ui-web";
|
||||||
import { useEffect, useState } from "react";
|
import { Suspense, useEffect, useState } from "react";
|
||||||
import { IntegrationSetup } from "../components/integrations/IntegrationSetup";
|
import { desktopBridge } from "../lib/desktop";
|
||||||
|
import { lazyWithRecovery } from "../lib/lazy-recovery";
|
||||||
import { rpc } from "../lib/rpc";
|
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() {
|
export function LocalSettingsPage() {
|
||||||
const [section, setSection] = useState<"models" | "integrations" | null>(null);
|
const [section, setSection] = useState<"models" | "integrations" | null>(null);
|
||||||
|
const hasLocalBridge = Boolean(desktopBridge()?.localSettings);
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(!hasLocalBridge);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!hasLocalBridge) return;
|
||||||
void rpc.integrationSetup
|
void rpc.integrationSetup
|
||||||
.get()
|
.get()
|
||||||
.then(() => setReady(true))
|
.then(() => setReady(true))
|
||||||
.catch(() => setError(true));
|
.catch(() => setError(true));
|
||||||
}, []);
|
}, [hasLocalBridge]);
|
||||||
return (
|
return (
|
||||||
<main className="h-full overflow-auto bg-background px-6 py-12">
|
<main className="h-full overflow-auto bg-background px-6 py-12">
|
||||||
<div className="mx-auto max-w-xl space-y-6">
|
<div className="mx-auto max-w-xl space-y-6">
|
||||||
@ -43,9 +54,15 @@ export function LocalSettingsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</nav>
|
</nav>
|
||||||
{section === "models" ? (
|
{section === "models" ? (
|
||||||
|
<Suspense fallback={null}>
|
||||||
<ModelSettingsOverlay onClose={() => setSection(null)} localOwner />
|
<ModelSettingsOverlay onClose={() => setSection(null)} localOwner />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
|
{section === "integrations" ? (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<IntegrationSetup serverSetup managedOnly />
|
||||||
|
</Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
{section === "integrations" ? <IntegrationSetup serverSetup managedOnly /> : null}
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import {
|
|||||||
import { type ComponentType, useEffect, useRef, useState } from "react";
|
import { type ComponentType, useEffect, useRef, useState } from "react";
|
||||||
import { computersAreUnavailable } from "../components/ComputersUnavailableHint";
|
import { computersAreUnavailable } from "../components/ComputersUnavailableHint";
|
||||||
import { UserProfileSettings } from "../components/UserProfileSettings";
|
import { UserProfileSettings } from "../components/UserProfileSettings";
|
||||||
import type { TradingBotUiState } from "../lib/trading-bot-status";
|
|
||||||
import {
|
import {
|
||||||
ComputerSettingsPanel,
|
ComputerSettingsPanel,
|
||||||
GeneralSettingsPanels,
|
GeneralSettingsPanels,
|
||||||
@ -65,7 +64,6 @@ export function SettingsOverlay({
|
|||||||
onVoiceStatusMaybeChanged,
|
onVoiceStatusMaybeChanged,
|
||||||
activeBotId,
|
activeBotId,
|
||||||
onOpenMcp,
|
onOpenMcp,
|
||||||
tradingStatus,
|
|
||||||
}: {
|
}: {
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
@ -83,7 +81,6 @@ export function SettingsOverlay({
|
|||||||
onVoiceStatusMaybeChanged?: () => void | Promise<void>;
|
onVoiceStatusMaybeChanged?: () => void | Promise<void>;
|
||||||
activeBotId?: string;
|
activeBotId?: string;
|
||||||
onOpenMcp?: () => void;
|
onOpenMcp?: () => void;
|
||||||
tradingStatus?: TradingBotUiState;
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
@ -238,7 +235,6 @@ export function SettingsOverlay({
|
|||||||
onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined
|
onOpenMessaging ? () => leaveSettings(onOpenMessaging) : undefined
|
||||||
}
|
}
|
||||||
isDeploymentOwner={isDeploymentOwner}
|
isDeploymentOwner={isDeploymentOwner}
|
||||||
tradingStatus={tradingStatus}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{section === "integrations" ? (
|
{section === "integrations" ? (
|
||||||
|
|||||||
@ -139,7 +139,6 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
Smile,
|
Smile,
|
||||||
Sparkles,
|
|
||||||
Square,
|
Square,
|
||||||
TextQuote,
|
TextQuote,
|
||||||
Trash2,
|
Trash2,
|
||||||
@ -178,11 +177,9 @@ import {
|
|||||||
} from "../components/ComputersUnavailableHint";
|
} from "../components/ComputersUnavailableHint";
|
||||||
import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress";
|
import { ComputerUpdateProgress } from "../components/ComputerUpdateProgress";
|
||||||
import { AgentCursor } from "../components/computer/AgentCursor";
|
import { AgentCursor } from "../components/computer/AgentCursor";
|
||||||
import { DataTrustPanel } from "../components/DataTrustPanel";
|
|
||||||
import { MainInbox } from "../components/MainInbox";
|
import { MainInbox } from "../components/MainInbox";
|
||||||
import { MessageHoverMetadata } from "../components/MessageHoverMetadata";
|
import { MessageHoverMetadata } from "../components/MessageHoverMetadata";
|
||||||
import { OoGroupStage, OoStage } from "../components/stage/OoStage";
|
import { OoGroupStage, OoStage } from "../components/stage/OoStage";
|
||||||
import { TradingModeIndicator } from "../components/TradingModeIndicator";
|
|
||||||
import { SkillDraftCard } from "../components/teach/SkillDraftCard";
|
import { SkillDraftCard } from "../components/teach/SkillDraftCard";
|
||||||
import { TeachCaptureOverlay } from "../components/teach/TeachCaptureOverlay";
|
import { TeachCaptureOverlay } from "../components/teach/TeachCaptureOverlay";
|
||||||
import { TeachComputerOverlayControl } from "../components/teach/TeachComputerOverlay";
|
import { TeachComputerOverlayControl } from "../components/teach/TeachComputerOverlay";
|
||||||
@ -252,7 +249,6 @@ import {
|
|||||||
threadRunError,
|
threadRunError,
|
||||||
userHoldsComputerControl,
|
userHoldsComputerControl,
|
||||||
} from "../lib/thread-events";
|
} from "../lib/thread-events";
|
||||||
import { useTradingBotStatus } from "../lib/trading-bot-status";
|
|
||||||
import {
|
import {
|
||||||
transcriptCanSnapAfterFrame,
|
transcriptCanSnapAfterFrame,
|
||||||
transcriptIsNearEnd,
|
transcriptIsNearEnd,
|
||||||
@ -425,7 +421,6 @@ export function ShellPage() {
|
|||||||
if (botId) return "team";
|
if (botId) return "team";
|
||||||
return "heute";
|
return "heute";
|
||||||
});
|
});
|
||||||
const tradingStatus = useTradingBotStatus();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (botId) setActiveNavPillar("team");
|
if (botId) setActiveNavPillar("team");
|
||||||
@ -1034,14 +1029,15 @@ export function ShellPage() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const inProject = Boolean(refreshSpaceId && refreshSpaceId !== refreshHomeSpaceId);
|
||||||
if (currentGroupId) {
|
if (currentGroupId) {
|
||||||
if (!groupList.some((group) => group.id === currentGroupId)) {
|
if (!groupList.some((group) => group.id === currentGroupId)) {
|
||||||
navigate(firstThreadRoute(list, groupList), { replace: true });
|
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
if (!currentBotId || !list.some((bot) => bot.id === currentBotId)) {
|
||||||
navigate(firstThreadRoute(list, groupList), { replace: true });
|
navigate(firstThreadRoute(list, groupList, inProject), { replace: true });
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
botsRefreshInFlight.current -= 1;
|
botsRefreshInFlight.current -= 1;
|
||||||
@ -1278,12 +1274,17 @@ export function ShellPage() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const inProject = bootstrapSpaceId !== bootstrapHomeSpaceId;
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
if (!groupList.some((group) => group.id === groupId)) {
|
if (!groupList.some((group) => group.id === groupId)) {
|
||||||
navigate(firstThreadRoute(bootstrap.bots, groupList), { replace: true });
|
navigate(firstThreadRoute(bootstrap.bots, groupList, inProject), { replace: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (inProject && !botId) {
|
||||||
|
navigate(firstThreadRoute(bootstrap.bots, groupList, true), { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
|
const selectedBotId = bootstrap.thread?.botId ?? bootstrap.bots[0]?.id;
|
||||||
if (selectedBotId && selectedBotId !== botId) {
|
if (selectedBotId && selectedBotId !== botId) {
|
||||||
navigate(`/app/${selectedBotId}`, { replace: true });
|
navigate(`/app/${selectedBotId}`, { replace: true });
|
||||||
@ -1660,6 +1661,8 @@ export function ShellPage() {
|
|||||||
: group.title,
|
: group.title,
|
||||||
showLock: showSpaceNames,
|
showLock: showSpaceNames,
|
||||||
emptySpaceId: undefined as string | undefined,
|
emptySpaceId: undefined as string | undefined,
|
||||||
|
/** A project without its main conversation yet offers to start it. */
|
||||||
|
startSpaceId: undefined as string | undefined,
|
||||||
spaceId: space.id,
|
spaceId: space.id,
|
||||||
spaceName: space.name,
|
spaceName: space.name,
|
||||||
canDeleteSpace: index === 0 && space.canDelete === true,
|
canDeleteSpace: index === 0 && space.canDelete === true,
|
||||||
@ -1676,6 +1679,7 @@ export function ShellPage() {
|
|||||||
sectionId: null,
|
sectionId: null,
|
||||||
showLock: true,
|
showLock: true,
|
||||||
emptySpaceId: space.id,
|
emptySpaceId: space.id,
|
||||||
|
startSpaceId: undefined,
|
||||||
spaceId: space.id,
|
spaceId: space.id,
|
||||||
spaceName: space.name,
|
spaceName: space.name,
|
||||||
canDeleteSpace: space.canDelete === true,
|
canDeleteSpace: space.canDelete === true,
|
||||||
@ -1739,11 +1743,13 @@ export function ShellPage() {
|
|||||||
if (space.id === homeSpaceId || seenProjectSpaceIds.has(space.id)) continue;
|
if (space.id === homeSpaceId || seenProjectSpaceIds.has(space.id)) continue;
|
||||||
seenProjectSpaceIds.add(space.id);
|
seenProjectSpaceIds.add(space.id);
|
||||||
const groupsInSpace = sidebarGroups.flatMap((group) =>
|
const groupsInSpace = sidebarGroups.flatMap((group) =>
|
||||||
group.spaceId === space.id
|
group.spaceId === space.id ? group.bots.filter((item) => item.kind === "group") : [],
|
||||||
? group.bots.filter((item) => item.kind === "group" && item.chat.kind !== "main")
|
|
||||||
: [],
|
|
||||||
);
|
);
|
||||||
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({
|
entries.push({
|
||||||
key:
|
key:
|
||||||
projectBots.length > 0 ? `project-space:${space.id}` : `project-space:${space.id}:empty`,
|
projectBots.length > 0 ? `project-space:${space.id}` : `project-space:${space.id}:empty`,
|
||||||
@ -1752,6 +1758,7 @@ export function ShellPage() {
|
|||||||
sectionId: null,
|
sectionId: null,
|
||||||
showLock: true,
|
showLock: true,
|
||||||
emptySpaceId: projectBots.length > 0 ? undefined : space.id,
|
emptySpaceId: projectBots.length > 0 ? undefined : space.id,
|
||||||
|
startSpaceId: hasMain ? undefined : space.id,
|
||||||
spaceId: space.id,
|
spaceId: space.id,
|
||||||
spaceName: space.name,
|
spaceName: space.name,
|
||||||
canDeleteSpace: space.canDelete === true,
|
canDeleteSpace: space.canDelete === true,
|
||||||
@ -1836,12 +1843,15 @@ export function ShellPage() {
|
|||||||
[bootstrapMe?.spaceId, navigate],
|
[bootstrapMe?.spaceId, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openProjectStart = useCallback(
|
// A project is a conversation with its own main agent, created when first opened.
|
||||||
(spaceId: string, flow: "create" | "group") => {
|
const startProject = useCallback(
|
||||||
|
async (spaceId: string) => {
|
||||||
setActiveNavPillar("projekte");
|
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(() => {
|
const openMainConversation = useCallback(() => {
|
||||||
@ -2085,36 +2095,6 @@ export function ShellPage() {
|
|||||||
}
|
}
|
||||||
}, [botId, emptyProjectSpace, groupId, initialBotsLoaded]);
|
}, [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
|
const activeSnapshot = inGroup
|
||||||
? snapshot?.groupId === groupId
|
? snapshot?.groupId === groupId
|
||||||
? snapshot
|
? 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"
|
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={() => {
|
onClick={() => {
|
||||||
if (group.emptySpaceId) {
|
if (group.emptySpaceId) {
|
||||||
openSpaceChat(group.emptySpaceId, "/app");
|
void startProject(group.emptySpaceId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toggleSidebarSection(group.key);
|
toggleSidebarSection(group.key);
|
||||||
@ -4090,14 +4070,14 @@ export function ShellPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!collapsed && group.emptySpaceId ? (
|
{!collapsed && group.startSpaceId ? (
|
||||||
<div className="px-3 pb-2">
|
<div className="px-3 pb-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full text-ui-label"
|
className="w-full text-ui-label"
|
||||||
data-testid={`project-start-${group.spaceId}`}
|
data-testid={`project-start-${group.spaceId}`}
|
||||||
onClick={() => openProjectStart(group.spaceId, "group")}
|
onClick={() => void startProject(group.spaceId)}
|
||||||
>
|
>
|
||||||
<Plus className="mr-1 size-icon-sm" />
|
<Plus className="mr-1 size-icon-sm" />
|
||||||
{t`Start`}
|
{t`Start`}
|
||||||
@ -4112,6 +4092,8 @@ export function ShellPage() {
|
|||||||
aria-label={
|
aria-label={
|
||||||
item.kind === "bot"
|
item.kind === "bot"
|
||||||
? t`${item.chat.name}, 1:1 conversation`
|
? t`${item.chat.name}, 1:1 conversation`
|
||||||
|
: item.chat.kind === "main"
|
||||||
|
? t`${item.chat.name}, main conversation`
|
||||||
: t`${item.chat.name}, group conversation`
|
: t`${item.chat.name}, group conversation`
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -4124,7 +4106,12 @@ export function ShellPage() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onContextMenu={(event) => {
|
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();
|
event.preventDefault();
|
||||||
botMenuAnchor.current = event.currentTarget;
|
botMenuAnchor.current = event.currentTarget;
|
||||||
setBotMenu({
|
setBotMenu({
|
||||||
@ -4279,8 +4266,6 @@ export function ShellPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<TradingModeIndicator snapshot={tradingStatus} className="mx-3 mb-1 lg:mx-5 2xl:mx-7" />
|
|
||||||
<DataTrustPanel />
|
|
||||||
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
data-testid="user-menu-trigger"
|
data-testid="user-menu-trigger"
|
||||||
@ -4572,7 +4557,7 @@ export function ShellPage() {
|
|||||||
<div className="grid flex-1 place-items-center">
|
<div className="grid flex-1 place-items-center">
|
||||||
<Button
|
<Button
|
||||||
data-testid="project-start-main"
|
data-testid="project-start-main"
|
||||||
onClick={() => currentSpaceId && openProjectStart(currentSpaceId, "create")}
|
onClick={() => currentSpaceId && void startProject(currentSpaceId)}
|
||||||
>
|
>
|
||||||
<Plus aria-hidden="true" className="size-icon-md" />
|
<Plus aria-hidden="true" className="size-icon-md" />
|
||||||
{t`Start`}
|
{t`Start`}
|
||||||
@ -4639,9 +4624,6 @@ export function ShellPage() {
|
|||||||
activeName={
|
activeName={
|
||||||
inGroup ? (activeGroup?.name ?? activeSnapshot?.groupName) : active?.name
|
inGroup ? (activeGroup?.name ?? activeSnapshot?.groupName) : active?.name
|
||||||
}
|
}
|
||||||
onPromptSelect={(prompt) => {
|
|
||||||
void sendMessage(prompt);
|
|
||||||
}}
|
|
||||||
scrollRef={messageScroll}
|
scrollRef={messageScroll}
|
||||||
artifactTarget={transcriptArtifactTarget}
|
artifactTarget={transcriptArtifactTarget}
|
||||||
onOpenArtifact={wideLayout ? openArtifactBeside : undefined}
|
onOpenArtifact={wideLayout ? openArtifactBeside : undefined}
|
||||||
@ -4690,9 +4672,6 @@ export function ShellPage() {
|
|||||||
{(active || activeGroup) && !showProjectTimeline ? (
|
{(active || activeGroup) && !showProjectTimeline ? (
|
||||||
<Composer
|
<Composer
|
||||||
key={inGroup ? `group:${groupId}` : `bot:${active?.id}`}
|
key={inGroup ? `group:${groupId}` : `bot:${active?.id}`}
|
||||||
activeName={
|
|
||||||
inGroup ? (activeGroup?.name ?? activeSnapshot?.groupName) : active?.name
|
|
||||||
}
|
|
||||||
running={composerRunning}
|
running={composerRunning}
|
||||||
disabled={Boolean(recordingSkill)}
|
disabled={Boolean(recordingSkill)}
|
||||||
pendingAttachments={activePendingAttachments}
|
pendingAttachments={activePendingAttachments}
|
||||||
@ -4956,7 +4935,10 @@ export function ShellPage() {
|
|||||||
const remainingGroups = groups.filter((group) => group.id !== activeGroup.id);
|
const remainingGroups = groups.filter((group) => group.id !== activeGroup.id);
|
||||||
setGroups(remainingGroups);
|
setGroups(remainingGroups);
|
||||||
setPanel(null);
|
setPanel(null);
|
||||||
navigate(firstThreadRoute(bots, remainingGroups), { replace: true });
|
navigate(
|
||||||
|
firstThreadRoute(bots, remainingGroups, currentSpaceId !== homeSpaceId),
|
||||||
|
{ replace: true },
|
||||||
|
);
|
||||||
await refreshBots().catch(() => undefined);
|
await refreshBots().catch(() => undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -5567,7 +5549,6 @@ export function ShellPage() {
|
|||||||
setSettingsOpen(false);
|
setSettingsOpen(false);
|
||||||
setMessagingSettingsOpen(true);
|
setMessagingSettingsOpen(true);
|
||||||
}}
|
}}
|
||||||
tradingStatus={tradingStatus}
|
|
||||||
onAvatarStyleChange={async (avatarStyle) => {
|
onAvatarStyleChange={async (avatarStyle) => {
|
||||||
const nextMe = await rpc.preferences.update({ avatarStyle });
|
const nextMe = await rpc.preferences.update({ avatarStyle });
|
||||||
setBootstrapMe(nextMe);
|
setBootstrapMe(nextMe);
|
||||||
@ -5816,7 +5797,6 @@ const Transcript = memo(function Transcript({
|
|||||||
speakingMessageId,
|
speakingMessageId,
|
||||||
onSpeak,
|
onSpeak,
|
||||||
activeName,
|
activeName,
|
||||||
onPromptSelect,
|
|
||||||
encounterMode = false,
|
encounterMode = false,
|
||||||
voiceEncounterActive = false,
|
voiceEncounterActive = false,
|
||||||
}: {
|
}: {
|
||||||
@ -5847,7 +5827,6 @@ const Transcript = memo(function Transcript({
|
|||||||
speakingMessageId: string | null;
|
speakingMessageId: string | null;
|
||||||
onSpeak: (message: ThreadMessage) => void;
|
onSpeak: (message: ThreadMessage) => void;
|
||||||
activeName?: string;
|
activeName?: string;
|
||||||
onPromptSelect?: (prompt: string) => void;
|
|
||||||
encounterMode?: boolean;
|
encounterMode?: boolean;
|
||||||
voiceEncounterActive?: boolean;
|
voiceEncounterActive?: boolean;
|
||||||
}) {
|
}) {
|
||||||
@ -6096,87 +6075,11 @@ const Transcript = memo(function Transcript({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{reactionView.visibleMessages.length === 0 && encounterMode ? (
|
{reactionView.visibleMessages.length === 0 && encounterMode ? (
|
||||||
<div className="flex flex-col gap-2 py-2">
|
<p className="py-2 text-ui-body-sm text-muted-foreground">
|
||||||
<p className="text-ui-body-sm text-muted-foreground">
|
|
||||||
{activeName
|
{activeName
|
||||||
? t`Start the conversation — ${activeName} is on stage above.`
|
? t`Start the conversation — ${activeName} is on stage above.`
|
||||||
: t`Start the conversation.`}
|
: t`Start the conversation.`}
|
||||||
</p>
|
</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}
|
) : null}
|
||||||
{reactionView.visibleMessages.map((message) => {
|
{reactionView.visibleMessages.map((message) => {
|
||||||
if (!message.blocks.some((block) => !isToolActivityBlock(block))) return null;
|
if (!message.blocks.some((block) => !isToolActivityBlock(block))) return null;
|
||||||
@ -6421,7 +6324,6 @@ const QuoteSelectionButton = memo(function QuoteSelectionButton({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Composer = memo(function Composer({
|
const Composer = memo(function Composer({
|
||||||
activeName,
|
|
||||||
running,
|
running,
|
||||||
disabled,
|
disabled,
|
||||||
pendingAttachments,
|
pendingAttachments,
|
||||||
@ -6449,7 +6351,6 @@ const Composer = memo(function Composer({
|
|||||||
onSlashOpen,
|
onSlashOpen,
|
||||||
onSlashAction,
|
onSlashAction,
|
||||||
}: {
|
}: {
|
||||||
activeName?: string;
|
|
||||||
running: boolean;
|
running: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
pendingAttachments: PendingAttachment[];
|
pendingAttachments: PendingAttachment[];
|
||||||
@ -6758,28 +6659,6 @@ const Composer = memo(function Composer({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="mx-auto w-full max-w-4xl">
|
<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 ? (
|
{sendError || runError ? (
|
||||||
<div
|
<div
|
||||||
ref={runErrorRef}
|
ref={runErrorRef}
|
||||||
@ -7274,48 +7153,6 @@ function cleanRosterPreview(raw?: string | null): string {
|
|||||||
return text;
|
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({
|
function MessageHoverActions({
|
||||||
message,
|
message,
|
||||||
side,
|
side,
|
||||||
@ -7418,8 +7255,12 @@ function MessageHoverActions({
|
|||||||
|
|
||||||
function firstThreadRoute(
|
function firstThreadRoute(
|
||||||
bots: readonly Pick<Bot, "id">[],
|
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 {
|
): 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 (bots[0]) return `/app/${bots[0].id}`;
|
||||||
if (groups[0]) return `/app/g/${groups[0].id}`;
|
if (groups[0]) return `/app/g/${groups[0].id}`;
|
||||||
return "/app";
|
return "/app";
|
||||||
|
|||||||
2
apps/web/src/vite-env.d.ts
vendored
2
apps/web/src/vite-env.d.ts
vendored
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_DEFAULT_UI_LOCALE?: string;
|
readonly VITE_DEFAULT_UI_LOCALE?: string;
|
||||||
/** Optional Tradingbot origin. Empty uses the same-origin API proxy. */
|
|
||||||
readonly VITE_TRADING_BOT_URL?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
|||||||
@ -22,8 +22,7 @@
|
|||||||
"!**/data",
|
"!**/data",
|
||||||
"!**/*.astro",
|
"!**/*.astro",
|
||||||
"!**/apps/www",
|
"!**/apps/www",
|
||||||
"!**/.claude-handoff*",
|
"!**/.claude-handoff*"
|
||||||
"!**/screenshot*.cjs"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"formatter": {
|
"formatter": {
|
||||||
|
|||||||
138
deploy.sh
138
deploy.sh
@ -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}"
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
# evOo-local — Agenten-Interface only
|
# evOo-local — Agenten-Interface only
|
||||||
|
|
||||||
Primary: multi-agent / Oo UI at `/app/`.
|
Primary: multi-agent / Oo UI at `/app/`.
|
||||||
Not primary: Mission Control, Cockpit, trading dashboards, old HTML evoo-platform.
|
|
||||||
Spelling: **evOo** (letter O).
|
Spelling: **evOo** (letter O).
|
||||||
|
|||||||
@ -102,10 +102,7 @@ Then connect **Kokoro** in Voice settings. See [kokoro-voice.md](./kokoro-voice.
|
|||||||
|
|
||||||
| Tool | Path |
|
| Tool | Path |
|
||||||
|------|------|
|
|------|------|
|
||||||
| self_improve | `scripts/self_improve.py` |
|
|
||||||
| autonomous-sentinel | `scripts/autonomous-sentinel.ts` |
|
|
||||||
| self-update | `packages/core/src/self-update.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` |
|
| local-stack | `apps/desktop/src/local-stack.ts` |
|
||||||
| pi-local-provider | `packages/adapters/src/pi-local-provider.ts` |
|
| pi-local-provider | `packages/adapters/src/pi-local-provider.ts` |
|
||||||
| seed-ollama | `scripts/seed-ollama.ts` |
|
| seed-ollama | `scripts/seed-ollama.ts` |
|
||||||
@ -117,12 +114,3 @@ Display brand is **evOo**. Workspace package names remain `@rakazo/api`, `@rakaz
|
|||||||
## Source
|
## 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.
|
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.
|
|
||||||
|
|||||||
@ -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.
|
|
||||||
@ -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" }, "*");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -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 & 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 & 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 & QA Auditor</span>
|
|
||||||
<span class="agent-role">Integritätsprüfung & 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 & Service Liveness</span>
|
|
||||||
<span class="agent-node">Node: Host Mac (Daemons & 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 & 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 & 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 & 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 & 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 & 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 & 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 • Physical Reality Guaranteed • Executive Command Center</span>
|
|
||||||
</div>
|
|
||||||
<div class="footer-right">
|
|
||||||
<span id="timestamp-clock">--:--:-- UTC</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -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}`;
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
47
infra/updater/src/ci-workflow.test.ts
Normal file
47
infra/updater/src/ci-workflow.test.ts
Normal 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'");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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()
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
* Hands work from a 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
|
* conversation of the main bot's space; a specialist of that space joins it and works there, one
|
||||||
* works in its own chat. Replays of the same tool call return the task created first.
|
* of another space works in its own chat. Replays of the same tool call return the task created
|
||||||
|
* first.
|
||||||
*/
|
*/
|
||||||
export async function delegateTask(
|
export async function delegateTask(
|
||||||
deps: DelegationDeps,
|
deps: DelegationDeps,
|
||||||
@ -63,6 +64,7 @@ export async function delegateTask(
|
|||||||
id: string;
|
id: string;
|
||||||
botId: string;
|
botId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
spaceId: string;
|
||||||
sourceMessageId?: string | null;
|
sourceMessageId?: string | null;
|
||||||
},
|
},
|
||||||
sender: { id: string; name: string },
|
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" };
|
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.
|
// Resolving the conversation also settles who the space's main bot is; only its lead delegates.
|
||||||
const conversation = await ensureMainConversation(deps.prisma, run.userId);
|
const conversation = await ensureMainConversation(deps.prisma, run.userId, run.spaceId);
|
||||||
if (conversation.leadBotId !== sender.id) {
|
if (conversation.leadBotId !== sender.id) {
|
||||||
return { error: "only the main agent can delegate tasks" };
|
return { error: "only the main agent can delegate tasks" };
|
||||||
}
|
}
|
||||||
@ -115,8 +117,9 @@ export async function delegateTask(
|
|||||||
const candidates = await deps.prisma.bot.findMany({
|
const candidates = await deps.prisma.bot.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: run.userId,
|
userId: run.userId,
|
||||||
|
// Main bots lead their own space and are never delegated to.
|
||||||
|
kind: "specialist",
|
||||||
archivedAt: null,
|
archivedAt: null,
|
||||||
id: { not: sender.id },
|
|
||||||
thread: { isNot: null },
|
thread: { isNot: null },
|
||||||
space: { memberships: { some: { userId: run.userId } } },
|
space: { memberships: { some: { userId: run.userId } } },
|
||||||
},
|
},
|
||||||
@ -139,7 +142,11 @@ export async function delegateTask(
|
|||||||
const targetThreadId = target.thread.id;
|
const targetThreadId = target.thread.id;
|
||||||
const inConversation = target.spaceId === conversation.spaceId;
|
const inConversation = target.spaceId === conversation.spaceId;
|
||||||
if (inConversation) {
|
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}`;
|
const deliveryKey = `delegated-task:${input.deliveryKey}`;
|
||||||
|
|||||||
@ -88,6 +88,7 @@ async function runBot(policy: BotPolicy, calls: Call[] = [], models: Record<stri
|
|||||||
updateMany: vi.fn(async () => ({ count: 1 })),
|
updateMany: vi.fn(async () => ({ count: 1 })),
|
||||||
},
|
},
|
||||||
thread: { findUniqueOrThrow: vi.fn(async () => ({ id: run.threadId, groupId: null })) },
|
thread: { findUniqueOrThrow: vi.fn(async () => ({ id: run.threadId, groupId: null })) },
|
||||||
|
space: { findUnique: vi.fn(async () => ({ name: "Home", isDefault: true })) },
|
||||||
message: { findMany: vi.fn(async () => []) },
|
message: { findMany: vi.fn(async () => []) },
|
||||||
task: { findUniqueOrThrow: vi.fn(async () => ({ id: run.taskId, prompt: "Work" })) },
|
task: { findUniqueOrThrow: vi.fn(async () => ({ id: run.taskId, prompt: "Work" })) },
|
||||||
connection: { findMany: vi.fn(async () => []) },
|
connection: { findMany: vi.fn(async () => []) },
|
||||||
|
|||||||
@ -28,6 +28,8 @@ interface FixtureOptions {
|
|||||||
spaceId?: string;
|
spaceId?: string;
|
||||||
space?: { name: 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. */
|
/** 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) },
|
chatGroup: { findUnique: vi.fn(async () => options.group ?? null) },
|
||||||
|
space: {
|
||||||
|
findUnique: vi.fn(async () => options.space ?? { name: "Home", isDefault: true }),
|
||||||
|
},
|
||||||
message: {
|
message: {
|
||||||
findMany: vi.fn(async () => []),
|
findMany: vi.fn(async () => []),
|
||||||
findUnique: vi.fn(async () => ({ blocks: options.sourceBlocks ?? [] })),
|
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");
|
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 () => {
|
it("shows the main bot every specialist it can delegate to, its own space first", async () => {
|
||||||
const { request, peersFindMany } = await runOnce({
|
const { request, peersFindMany } = await runOnce({
|
||||||
bot: { kind: "main", name: "Chief", title: "Chief of Staff", instructions: "" },
|
bot: { kind: "main", name: "Chief", title: "Chief of Staff", instructions: "" },
|
||||||
@ -366,6 +384,7 @@ describe("run instructions", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
where: expect.objectContaining({
|
where: expect.objectContaining({
|
||||||
userId: "user-1",
|
userId: "user-1",
|
||||||
|
kind: "specialist",
|
||||||
space: { memberships: { some: { userId: "user-1" } } },
|
space: { memberships: { some: { userId: "user-1" } } },
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -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
|
* 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.
|
* 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 [
|
return [
|
||||||
"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 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.",
|
? `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")
|
toolNames.has("delegate_task")
|
||||||
? "Give a specialist tracked work with delegate_task; its result comes back to you."
|
? "Give a specialist tracked work with delegate_task; its result comes back to you."
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@ -91,6 +91,7 @@ import {
|
|||||||
createSpaceForMember,
|
createSpaceForMember,
|
||||||
createThreadMessageInTransaction,
|
createThreadMessageInTransaction,
|
||||||
effectiveMemoryScope,
|
effectiveMemoryScope,
|
||||||
|
ensureMainConversation,
|
||||||
findDefaultModelCredential,
|
findDefaultModelCredential,
|
||||||
findModelCredential,
|
findModelCredential,
|
||||||
findSpaceModelTier,
|
findSpaceModelTier,
|
||||||
@ -3218,6 +3219,8 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
userId: run.userId,
|
userId: run.userId,
|
||||||
name: String(args.name ?? ""),
|
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 });
|
return finish({ ok: true, spaceId: space.id, name: space.name });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof SpaceLimitError || error instanceof InvalidSpaceNameError) {
|
if (error instanceof SpaceLimitError || error instanceof InvalidSpaceNameError) {
|
||||||
@ -3667,8 +3670,9 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
await deps.prisma.bot.findMany({
|
await deps.prisma.bot.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: run.userId,
|
userId: run.userId,
|
||||||
|
// Main bots lead their own space and are never delegated to.
|
||||||
|
kind: "specialist",
|
||||||
archivedAt: null,
|
archivedAt: null,
|
||||||
id: { not: bot.id },
|
|
||||||
thread: { isNot: null },
|
thread: { isNot: null },
|
||||||
space: { memberships: { some: { userId: run.userId } } },
|
space: { memberships: { some: { userId: run.userId } } },
|
||||||
},
|
},
|
||||||
@ -3726,6 +3730,16 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
const timeInstruction = formatCurrentTimeInstruction(userContext.timeZone);
|
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.
|
// Replies to another owner's agent, like a shared channel, only get the basics.
|
||||||
const userInstruction = formatUserContext(userContext, {
|
const userInstruction = formatUserContext(userContext, {
|
||||||
basicsOnly: messagingChannelRun || foreignAgentRun,
|
basicsOnly: messagingChannelRun || foreignAgentRun,
|
||||||
@ -3758,7 +3772,9 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
instructions: [
|
instructions: [
|
||||||
formatBotIdentity(bot),
|
formatBotIdentity(bot),
|
||||||
bot.instructions,
|
bot.instructions,
|
||||||
bot.kind === "main" ? formatMainAgentRole(exposedToolNames) : undefined,
|
bot.kind === "main"
|
||||||
|
? formatMainAgentRole(exposedToolNames, mainAgentProject)
|
||||||
|
: undefined,
|
||||||
timeInstruction,
|
timeInstruction,
|
||||||
userBlock,
|
userBlock,
|
||||||
groupContext?.instructions,
|
groupContext?.instructions,
|
||||||
|
|||||||
@ -313,8 +313,10 @@ export async function consolidateProfile(
|
|||||||
// The scripted runtime answers with canned text keyed off the prompt; facts "learned" from it
|
// The scripted runtime answers with canned text keyed off the prompt; facts "learned" from it
|
||||||
// would be nonsense. Skip without touching the cursor.
|
// would be nonsense. Skip without touching the cursor.
|
||||||
if (!deps.runtime.describe().capabilities.compaction) return;
|
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({
|
const main = await deps.prisma.bot.findFirst({
|
||||||
where: { userId, kind: "main", archivedAt: null },
|
where: { userId, kind: "main", archivedAt: null },
|
||||||
|
orderBy: [{ space: { isDefault: "desc" } }, { createdAt: "asc" }],
|
||||||
select: { id: true, spaceId: true, thread: { select: { id: true } } },
|
select: { id: true, spaceId: true, thread: { select: { id: true } } },
|
||||||
});
|
});
|
||||||
if (!main?.thread) return;
|
if (!main?.thread) return;
|
||||||
|
|||||||
@ -51,14 +51,14 @@ describe("remote MCP URL policy", () => {
|
|||||||
assertSafeRemoteUrl(magicDns, async () => [{ address: "100.64.1.2", family: 4 as const }]),
|
assertSafeRemoteUrl(magicDns, async () => [{ address: "100.64.1.2", family: 4 as const }]),
|
||||||
).resolves.toEqual(new URL(magicDns));
|
).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) => {
|
const result = await new Promise<{ address: string; family?: number }>((resolve, reject) => {
|
||||||
safeLookup("box.tail12345.ts.net", { family: 0, all: false }, (error, address, family) => {
|
safeLookup("box.tail12345.ts.net", { family: 0, all: false }, (error, address, family) => {
|
||||||
if (error) reject(error);
|
if (error) reject(error);
|
||||||
else resolve({ address: String(address), family });
|
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 () => {
|
it("allows the IPv6 half of a MagicDNS answer", async () => {
|
||||||
|
|||||||
@ -39,7 +39,7 @@ export const AgentSecretInputSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type AgentSecretInput = z.infer<typeof AgentSecretInputSchema>;
|
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 BOT_KINDS = ["main", "specialist"] as const;
|
||||||
export const BotKindSchema = z.enum(BOT_KINDS);
|
export const BotKindSchema = z.enum(BOT_KINDS);
|
||||||
export type BotKind = z.infer<typeof BotKindSchema>;
|
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_MIN = 2;
|
||||||
export const GROUP_MEMBER_MAX = 6;
|
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 GROUP_KINDS = ["group", "main"] as const;
|
||||||
export const GroupKindSchema = z.enum(GROUP_KINDS);
|
export const GroupKindSchema = z.enum(GROUP_KINDS);
|
||||||
export type GroupKind = z.infer<typeof GroupKindSchema>;
|
export type GroupKind = z.infer<typeof GroupKindSchema>;
|
||||||
@ -303,7 +303,7 @@ export const SpaceSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type Space = z.infer<typeof SpaceSchema>;
|
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({
|
export const MainConversationRefSchema = z.object({
|
||||||
botId: Id,
|
botId: Id,
|
||||||
groupId: Id,
|
groupId: Id,
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
GroupDetailSchema,
|
GroupDetailSchema,
|
||||||
GroupSchema,
|
GroupSchema,
|
||||||
IntegrationCatalogResultSchema,
|
IntegrationCatalogResultSchema,
|
||||||
|
MainConversationRefSchema,
|
||||||
McpServerConfigInput,
|
McpServerConfigInput,
|
||||||
McpServerSchema,
|
McpServerSchema,
|
||||||
MemoryDocumentSchema,
|
MemoryDocumentSchema,
|
||||||
@ -195,6 +196,8 @@ export const appContract = {
|
|||||||
remove: oc
|
remove: oc
|
||||||
.input(z.object({ spaceId: Id }))
|
.input(z.object({ spaceId: Id }))
|
||||||
.output(z.object({ ok: z.literal(true), activeSpaceId: 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),
|
bootstrap: oc.input(z.object({ botId: Id.optional() })).output(AppBootstrapSchema),
|
||||||
deployment: {
|
deployment: {
|
||||||
|
|||||||
@ -45,7 +45,6 @@ export * from "./text-direction.js";
|
|||||||
export * from "./thread-message-updates.js";
|
export * from "./thread-message-updates.js";
|
||||||
export * from "./thread-subscription.js";
|
export * from "./thread-subscription.js";
|
||||||
export * from "./tool-activity.js";
|
export * from "./tool-activity.js";
|
||||||
export * from "./trading-bot-status.js";
|
|
||||||
export * from "./voice-chat-command.js";
|
export * from "./voice-chat-command.js";
|
||||||
export * from "./voice-labels.js";
|
export * from "./voice-labels.js";
|
||||||
export * from "./workspace-focus.js";
|
export * from "./workspace-focus.js";
|
||||||
|
|||||||
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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>;
|
|
||||||
}
|
|
||||||
@ -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();
|
|
||||||
@ -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';
|
||||||
@ -402,7 +402,7 @@ model Bot {
|
|||||||
webhookSecretId String?
|
webhookSecretId String?
|
||||||
teamChatAmbientEnabled Boolean @default(false)
|
teamChatAmbientEnabled Boolean @default(false)
|
||||||
teamChatRules String @default("")
|
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")
|
kind String @default("specialist")
|
||||||
tags String[] @default([])
|
tags String[] @default([])
|
||||||
restrictBuiltinTools Boolean @default(false)
|
restrictBuiltinTools Boolean @default(false)
|
||||||
@ -427,7 +427,7 @@ model Bot {
|
|||||||
externalConversations ExternalConversation[]
|
externalConversations ExternalConversation[]
|
||||||
|
|
||||||
@@unique([spaceId, spawnKey])
|
@@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([spaceId, userId, archivedAt, pinned, updatedAt])
|
||||||
@@index([sectionId])
|
@@index([sectionId])
|
||||||
@@index([computerId])
|
@@index([computerId])
|
||||||
@ -473,7 +473,7 @@ model ChatGroup {
|
|||||||
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
|
space Space @relation(fields: [spaceId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
name 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")
|
kind String @default("group")
|
||||||
leadBotId String?
|
leadBotId String?
|
||||||
leadBot Bot? @relation("ChatGroupLead", fields: [leadBotId], references: [id], onDelete: SetNull)
|
leadBot Bot? @relation("ChatGroupLead", fields: [leadBotId], references: [id], onDelete: SetNull)
|
||||||
@ -491,7 +491,7 @@ model ChatGroup {
|
|||||||
@@index([spaceId, userId, archivedAt, pinned, updatedAt])
|
@@index([spaceId, userId, archivedAt, pinned, updatedAt])
|
||||||
@@index([sectionId])
|
@@index([sectionId])
|
||||||
@@index([leadBotId])
|
@@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")
|
@@map("chat_groups")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
Loading…
x
Reference in New Issue
Block a user