diff --git a/.github/workflows/trade-dashboard.yml b/.github/workflows/trade-dashboard.yml
deleted file mode 100644
index a7f8371..0000000
--- a/.github/workflows/trade-dashboard.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-name: Build and Push Trade Dashboard
-
-on:
- push:
- branches: [master]
- paths:
- - 'trade-dashboard/**'
- workflow_dispatch:
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository_owner }}/trade-dashboard
-
-permissions:
- contents: read
- packages: write
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Login to GHCR
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Extract metadata
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- tags: |
- type=sha,prefix=
- type=raw,value=latest,enable={{is_default_branch}}
-
- - name: Build and push
- uses: docker/build-push-action@v5
- with:
- context: trade-dashboard/
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
diff --git a/analyses/telegram-webhook-container-analysis.md b/analyses/telegram-webhook-container-analysis.md
new file mode 100644
index 0000000..d2ef6c7
--- /dev/null
+++ b/analyses/telegram-webhook-container-analysis.md
@@ -0,0 +1,303 @@
+# Telegram Webhook Failure Analysis - Deep Dive (Container-Level)
+
+**Date:** 2026-05-10
+**Repo:** https://github.com/sirius0xdev/gcloud-lab
+**Cluster:** devops-lab (GKE), Namespace: customer1
+**Webhook URL:** https://ws.siriusdevops.com/telegram/webhook/default
+
+---
+
+## Status Update
+
+**TLS certificates ARE provisioned** in GCP admin console. The Gateway certmap annotation is working.
+
+The real issues are in the **container configuration** and **missing probes**.
+
+---
+
+## Root Cause #1: NO Liveness/Readiness Probes (CONFIRMED CRITICAL)
+
+**The hermes-agent deployment has ZERO probes defined.**
+
+This means:
+- Kubernetes marks pods "Ready" immediately after container start
+- The Gateway routes webhook traffic before the process has bound to port 9118
+- During restarts, there is no graceful drain
+- **A crashed or hung pod stays in the endpoint list forever**
+
+### The /health endpoint problem
+
+The Telegram webhook server runs on **port 9118** via python-telegram-bot's `start_webhook()`. This internally starts an aiohttp server that **ONLY registers the webhook path** (`/telegram/webhook/default`). It does NOT expose a `/health` endpoint.
+
+So a probe like this would FAIL:
+```yaml
+# THIS WON'T WORK - port 9118 has no /health
+readinessProbe:
+ httpGet:
+ path: /health
+ port: 9118
+```
+
+### What DOES have a health endpoint?
+
+The **generic webhook adapter** on **port 8644** IS a Hermes-managed server. From the source code, it exposes `/health`. This IS enabled via `WEBHOOK_ENABLED: "true"`.
+
+### The Fix
+
+Add probes targeting port 8644 (the generic webhook server that IS healthy when the gateway is running):
+
+```yaml
+readinessProbe:
+ httpGet:
+ path: /health
+ port: 8644
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+livenessProbe:
+ httpGet:
+ path: /health
+ port: 8644
+ initialDelaySeconds: 30
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+```
+
+Alternatively, enable the API server on port 8642 and probe there (it also has `/health`).
+
+---
+
+## Root Cause #2: NO Resource Limits (HIGH)
+
+The **new deployment dropped all resource limits** that existed in the old deployment.
+
+**Old deployment (`deployment.yaml`):**
+```yaml
+resources:
+ requests:
+ memory: 2Gi
+ cpu: "1"
+ limits:
+ memory: 3Gi
+ cpu: "2"
+```
+
+**New deployment (`new-deployment.yaml`):**
+```yaml
+# NO resources block for the hermes-agent container
+```
+
+Only the `hermes-webui` sidecar has limits (500Mi-1Gi memory, 100m-500m CPU).
+
+### Impact
+- The hermes-agent container can consume unbounded memory
+- With `agent.max_turns: 90` and `gateway_timeout: 1800` (30 min), a single agent run can eat massive memory
+- Pod may be OOMKilled by the node, but K8s won't restart it gracefully without probes
+- **This is a likely cause of intermittent failures**
+
+### Fix
+Add resource limits back:
+```yaml
+resources:
+ requests:
+ memory: 2Gi
+ cpu: "1"
+ limits:
+ memory: 4Gi
+ cpu: "2"
+```
+
+---
+
+## Root Cause #3: Path Handling Concern (MEDIUM)
+
+**Source code** (`/opt/hermes/gateway/platforms/telegram.py:1213`):
+```python
+webhook_path = urlparse(webhook_url).path or "/telegram"
+```
+
+For `TELEGRAM_WEBHOOK_URL=https://ws.siriusdevops.com/telegram/webhook/default`:
+- `webhook_path` = `/telegram/webhook/default`
+- PTB's aiohttp server registers a handler at exactly `/telegram/webhook/default` on port 9118
+
+**HTTPRoute** (`hermes-webhook.yaml`):
+```yaml
+rules:
+- matches:
+ - path:
+ type: PathPrefix
+ value: /telegram/webhook
+ backendRefs:
+ - name: http-tele-webhook
+ port: 9118
+```
+
+**The question:** Does the GKE Gateway strip the `/telegram/webhook` prefix before forwarding?
+
+- If it does NOT strip: Pod receives `/telegram/webhook/default` -> **OK**
+- If it DOES strip to `/default`: Pod receives `/default` -> **404**
+- If it strips to `/`: Pod receives `/` -> **404**
+
+Most GatewayAPI implementations pass the full original path by default, but some ingress controllers strip the matched prefix. **Verify this on the running cluster.**
+
+---
+
+## Root Cause #4: API Server Not Enabled (MEDIUM)
+
+The old deployment had:
+```yaml
+- name: API_SERVER_ENABLED
+ value: "true"
+- name: API_SERVER_HOST
+ value: "0.0.0.0"
+- name: API_SERVER_PORT
+ value: "8642"
+- name: API_SERVER_KEY
+ valueFrom: {secretKeyRef: ...}
+- name: API_SERVER_MODEL_NAME
+ value: "hermes-agent"
+```
+
+**These are ALL absent from `new-deployment.yaml`.** Port 8642 is declared as a containerPort but nothing listens on it because `API_SERVER_ENABLED` is not set.
+
+The API server exposes `/health` and `/health/detailed` endpoints. Without it, you lose a convenient health check and the API server interface.
+
+---
+
+## Other Findings
+
+### 5. TELEGRAM_WEBHOOK_ENABLED is Ignored (INFO)
+
+The code only checks if `TELEGRAM_WEBHOOK_URL` is set (non-empty). It does NOT read `TELEGRAM_WEBHOOK_ENABLED`. The env var in the deployment is a dead config — set but ignored.
+
+### 6. vLLM Service Name Mismatch (INFO)
+
+```yaml
+OPENAI_BASE_URL: "http://openclaw-brain-service.customer1.svc.cluster.local:8000/v1"
+```
+
+But the active vLLM deployment (`rtx6000-vllm.yaml`) creates a service named `rtx6000-brain-service`. If Hermes ever switches from xAI to the openai provider, local vLLM is unreachable.
+
+### 7. SOPS Secrets (VERIFY)
+
+Both `hermes-secret.yaml` and `tele-webhook.yaml` are SOPS-encrypted. Verify they are decrypted in the cluster:
+```bash
+kubectl get secret hermes-secrets -n customer1 -o jsonpath='{.data.TELEGRAM_BOT_TOKEN}' | base64 -d
+kubectl get secret telegram-webhook -n customer1 -o jsonpath='{.data.TELEGRAM_WEBHOOK_SECRET}' | base64 -d
+```
+
+### 8. Container Env Vars Summary
+
+| Variable | Value | Notes |
+|----------|-------|-------|
+| TELEGRAM_WEBHOOK_URL | https://ws.siriusdevops.com/telegram/webhook/default | OK |
+| TELEGRAM_WEBHOOK_PORT | 9118 | OK |
+| TELEGRAM_WEBHOOK_SECRET | From SOPS secret | Verify decrypted |
+| TELEGRAM_WEBHOOK_ENABLED | "true" | **Ignored by code** |
+| TELEGRAM_BOT_TOKEN | From SOPS secret | Verify decrypted |
+| TELEGRAM_ALLOWED_USERS | 7528130947 | OK |
+| WEBHOOK_ENABLED | "true" | OK (enables port 8644 /health) |
+| WEBHOOK_PORT | 8644 | OK |
+| API_SERVER_ENABLED | **NOT SET** | Port 8642 has no listener |
+| HERMES_MODEL_PROVIDER | xai | OK (uses Grok) |
+| HERMES_MODEL | grok-4.20-0309-reasoning | OK |
+
+---
+
+## Recommended Fix (Priority Order)
+
+### P0 - Add Probes (will detect and restart hung/crashed pods)
+
+Add to `new-deployment.yaml` under the hermes-agent container spec:
+
+```yaml
+readinessProbe:
+ httpGet:
+ path: /health
+ port: 8644
+ initialDelaySeconds: 10
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+livenessProbe:
+ httpGet:
+ path: /health
+ port: 8644
+ initialDelaySeconds: 30
+ periodSeconds: 30
+ timeoutSeconds: 5
+ failureThreshold: 3
+```
+
+### P1 - Add Resource Limits (prevents OOM kills)
+
+```yaml
+resources:
+ requests:
+ memory: 2Gi
+ cpu: "1"
+ limits:
+ memory: 4Gi
+ cpu: "2"
+```
+
+### P2 - Verify Path Handling
+
+Test if the Gateway passes the full path:
+```bash
+# From inside the pod, check what the webhook server receives
+kubectl exec -n customer1 deploy/hermes-agent -- curl -s http://localhost:9118/telegram/webhook/default -X POST -H "Content-Type: application/json" -d '{}'
+```
+
+### P3 - Enable API Server (optional, gives /health on 8642)
+
+Add back the API_SERVER_ENABLED env var if you want the API server health endpoint.
+
+---
+
+## Diagnostic Commands
+
+Run these on the cluster RIGHT NOW to confirm the current state:
+
+```bash
+# 1. Check pod status and restart count
+kubectl get pods -n customer1 -l app=hermes-agent -o wide
+
+# 2. Check events for OOMKilled or probe failures
+kubectl describe pod -n customer1 -l app=hermes-agent | grep -A5 -i 'oom\|probe\|restart'
+
+# 3. Check if the webhook port is actually listening
+kubectl exec -n customer1 deploy/hermes-agent -- ss -tlnp | grep 9118
+
+# 4. Check if port 8644 health endpoint works
+kubectl exec -n customer1 deploy/hermes-agent -- curl -s http://localhost:8644/health
+
+# 5. Check logs for webhook startup messages
+kubectl logs -n customer1 deploy/hermes-agent --tail=50 | grep -i 'webhook\|listening\|9118'
+
+# 6. Verify SOPS secrets are decrypted
+kubectl get secret hermes-secrets -n customer1 -o jsonpath='{.data.TELEGRAM_BOT_TOKEN}' | base64 -d && echo
+kubectl get secret telegram-webhook -n customer1 -o jsonpath='{.data.TELEGRAM_WEBHOOK_SECRET}' | base64 -d && echo
+
+# 7. Check if the webhook is registered with Telegram (using the pod's bot token)
+BOT_TOKEN=$(kubectl get secret hermes-secrets -n customer1 -o jsonpath='{.data.TELEGRAM_BOT_TOKEN}' | base64 -d)
+curl -s "https://api.telegram.org/bot${BOT_TOKEN}/getWebhookInfo" | python3 -m json.tool
+
+# 8. Test the full path through the Gateway
+curl -v https://ws.siriusdevops.com/telegram/webhook/default -X POST -H "Content-Type: application/json" -d '{}' 2>&1
+
+# 9. Check container memory usage
+kubectl top pod -n customer1 -l app=hermes-agent 2>/dev/null || echo "metrics-server not available"
+
+# 10. Check resource limits on the container
+kubectl get pod -n customer1 -l app=hermes-agent -o jsonpath='{.items[0].spec.containers[0].resources}'
+```
+
+---
+
+## Files to Modify
+
+1. `apps/base/customer1/hermes-agent/new-deployment.yaml` - Add probes + resource limits
+2. Optionally: Re-enable API server env vars in `new-deployment.yaml`
diff --git a/analyses/telegram-webhook-failure-analysis.md b/analyses/telegram-webhook-failure-analysis.md
new file mode 100644
index 0000000..cdcac55
--- /dev/null
+++ b/analyses/telegram-webhook-failure-analysis.md
@@ -0,0 +1,345 @@
+# Telegram Webhook Failure Analysis - Hermes Agent on GKE
+
+**Date:** 2026-05-10
+**Repo:** https://github.com/sirius0xdev/gcloud-lab
+**Cluster:** devops-lab (GKE)
+**Namespace:** customer1
+**Webhook URL:** https://ws.siriusdevops.com/telegram/webhook/default
+
+---
+
+## Executive Summary
+
+The Telegram webhook is failing because **there is no valid TLS certificate for `ws.siriusdevops.com`** at the Gateway layer. Telegram strictly requires HTTPS with a publicly-trusted certificate for webhook delivery. The Gateway listener declares `protocol: HTTPS` but has no `tls.certificateRefs` and relies on a GKE `CertMap` annotation referencing `gateway-cert-map` — a resource that **does not exist** in the repository. Cert-manager is configured but disconnected from the GatewayAPI setup (HTTP-01 solver points to a non-existent Traefik ingress class).
+
+**TL;DR:** Telegram tries to POST to `https://ws.siriusdevops.com/...`, but the Gateway has no certificate to present during the TLS handshake. Connection fails before it ever reaches the hermes-agent pod.
+
+---
+
+## Root Cause #1: Missing TLS Certificate (CRITICAL)
+
+### What's happening
+
+**File:** `infrastructure/gatewayapi/apigateway.yaml`
+
+```yaml
+listeners:
+- name: https
+ protocol: HTTPS
+ port: 443
+ allowedRoutes:
+ namespaces:
+ from: All
+```
+
+The listener says HTTPS but has **no `tls:` block**. It relies entirely on this annotation:
+
+```yaml
+annotations:
+ networking.gke.io/certmap: gateway-cert-map
+```
+
+### The problem
+
+- **No `CertMap` or `CertMapEntry` resource** exists anywhere in the repository for `gateway-cert-map`
+- Without it, GKE has no managed certificate to attach to the Gateway
+- Telegram's webhook delivery gets a TLS handshake failure or no certificate
+
+### Cert-manager is also broken
+
+**File:** `infrastructure/controllers/base/certmanager/clusterissuer.yaml`
+
+```yaml
+solvers:
+ - http01:
+ ingress:
+ class: traefik
+```
+
+- HTTP-01 solver references `class: traefik`, but **no Traefik ingress controller exists** in the cluster
+- **No `Certificate` CRs** exist for `ws.siriusdevops.com` or any other domain
+- Cert-manager is completely disconnected from the GatewayAPI setup
+
+### How to fix (choose ONE approach)
+
+**Option A: GKE Managed Certificates (recommended for GatewayAPI)**
+
+Create a `ManagedCertificate` + `BackendConfig` or `CertMap`/`CertMapEntry`:
+
+```yaml
+apiVersion: networking.gke.io/v1
+kind: ManagedCertificate
+metadata:
+ name: hermes-webhook-cert
+ namespace: customer1
+spec:
+ domains:
+ - ws.siriusdevops.com
+ - brain.siriusdevops.com
+ - paaas.siriusdevops.com
+```
+
+Then add `tls.certificateRefs` to the Gateway listener:
+
+```yaml
+listeners:
+- name: https
+ protocol: HTTPS
+ port: 443
+ tls:
+ certificateRefs:
+ - name: hermes-webhook-cert
+ group: networking.gke.io
+```
+
+**Option B: Fix Cert-manager with DNS-01**
+
+Switch ClusterIssuer from HTTP-01/Traefik to DNS-01 (e.g., Cloudflare, GCP DNS, or Route53), then create `Certificate` resources for each domain.
+
+---
+
+## Root Cause #2: No Readiness/Liveness Probes (HIGH)
+
+**File:** `apps/base/customer1/hermes-agent/new-deployment.yaml`
+
+No readiness or liveness probes are defined on any hermes-agent deployment.
+
+### Impact
+- Pods are marked "Ready" immediately after container start
+- Gateway routes traffic to the webhook port (9118) before the process has bound to it
+- During restarts, traffic hits pods that haven't initialized
+
+### Fix
+Add probes to the deployment:
+
+```yaml
+readinessProbe:
+ httpGet:
+ path: /health
+ port: 9118
+ initialDelaySeconds: 5
+ periodSeconds: 10
+livenessProbe:
+ httpGet:
+ path: /health
+ port: 9118
+ initialDelaySeconds: 15
+ periodSeconds: 30
+```
+
+---
+
+## Root Cause #3: SOPS Encrypted Secrets (HIGH)
+
+**Files:**
+- `apps/base/customer1/hermes-agent/hermes-secret.yaml` (SOPS encrypted)
+- `apps/base/customer1/hermes-agent/tele-webhook.yaml` (SOPS encrypted)
+
+These contain `TELEGRAM_BOT_TOKEN` and `TELEGRAM_WEBHOOK_SECRET`.
+
+### Risk
+- If the deployment pipeline (Flux/Kustomize controller) is **not decrypting SOPS secrets**, pods receive literal `ENC[...]` strings
+- The bot token would be invalid, so even if TLS worked, Telegram auth would fail
+- The webhook secret would not match, causing Telegram to reject payloads
+
+### Verify
+Run `kubectl get secret hermes-secrets -n customer1 -o yaml` and check if values are base64-encoded real tokens or `ENC[...]` strings.
+
+---
+
+## Secondary Issues
+
+### 3a. Service Name Mismatch (vLLM Integration)
+
+**File:** `apps/base/customer1/hermes-agent/new-deployment.yaml`
+
+```yaml
+OPENAI_BASE_URL: "http://openclaw-brain-service.customer1.svc.cluster.local:8000/v1"
+```
+
+But the active vLLM deployment (`rtx6000-vllm.yaml`) creates a service named **`rtx6000-brain-service`**.
+
+- `OPENAI_BASE_URL` points to `openclaw-brain-service` which may not exist
+- If Hermes ever switches to the `openai` provider (instead of `xai`), local vLLM is unreachable
+- The model provider defaults to `HERMES_MODEL_PROVIDER: xai` (Grok external API)
+
+### 3b. Duplicate HTTPRoute Deployment
+
+The webhook HTTPRoute (`hermes-webhook.yaml`) is included in **two** kustomization trees:
+
+1. `infrastructure/gatewayapi/gateway-routes/kustomization.yaml` -> deployed via Flux
+2. `apps/base/customer1/hermes-agent/kustomization.yaml` -> deployed via Flux
+
+Same resource (`http-telegram-webhook` in `customer1`) from two sources. This may cause Flux reconciliation conflicts.
+
+### 3c. Empty HF_TOKEN in vLLM Deployments
+
+All vLLM deployments have:
+
+```yaml
+- name: HF_TOKEN
+ value: ""
+```
+
+If the model `edp1096/Huihui-Qwen3.6-27B-abliterated-FP8` is a gated model on HuggingFace, it will fail to download.
+
+### 3d. KEDA Scale-to-Zero
+
+**File:** `infrastructure/gpus/base/keda-gpu-scaling/keda-vllm.yaml`
+
+```yaml
+minReplicaCount: 0
+maxReplicaCount: 1
+```
+
+- vLLM scales to **zero** when idle
+- First request after cold start incurs full model load time (30-60 seconds)
+- For real-time Telegram responses, this causes visible latency
+
+### 3e. PVC Name Collision
+
+Both `rtx6000-vllm.yaml` and `a100-vllm.yaml` define a PVC named `vllm-model-qwen3.6-27b-uncensored` in namespace `customer1`. If both are ever active simultaneously, they conflict.
+
+---
+
+## Architecture Overview
+
+```
+ Internet
+ |
+ v
+ [GKE External LB]
+ |
+ Gateway: external-http-gateway
+ (port 443/HTTPS, NO TLS cert!)
+ |
+ +-----------+-----------+
+ | | |
+ ws.siriusdevops.com brain.siriusdevops.com paaas.siriusdevops.com
+ | | |
+ v v v
+ /telegram/webhook / /
+ | | |
+ v v v
+ http-tele-webhook rtx6000- paaas-landing
+ :9118 brain- :8080
+ service:8000
+ |
+ v
+ hermes-agent pod
+ (ports: 8642, 8644, 9118)
+ |
+ v
+ Model Provider: xai (Grok via external API)
+ Fallback: OPENAI_BASE_URL -> openclaw-brain-service (MISMATCHED)
+```
+
+---
+
+## vLLM Server Status
+
+| Server | GPU | Model | Quantization | Status |
+|--------|-----|-------|-------------|--------|
+| RTX 6000 | 1x Pro 6000 (96GB) | edp1096/Huihui-Qwen3.6-27B-abliterated-FP8 | FP8 | **ACTIVE** |
+| A100 | 1x A100 (80GB) | Youssofal/Qwen3.6-27B-Abliterated-Heretic-Uncensored-BF16 | BF16 | Commented out |
+| L4 | 1x L4 (24GB) | p-e-w/Qwen3-8B-heretic | auto | Commented out |
+| Gemma | 1x A100 (80GB) | coder3101/Qwen3.5-27B-heretic | BF16 | Not in kustomization |
+
+**Note:** The architecture plan (`plans/AI_ARCHITECTURE.md`) describes a dual-tier L4 dispatcher + A100 deep thinker setup, but the active deployment only has RTX 6000.
+
+---
+
+## Action Plan (Priority Order)
+
+### P0 - Fix TLS (will unblock Telegram webhooks)
+
+1. **Create a `CertMap`/`CertMapEntry`** or **`ManagedCertificate`** resource for `ws.siriusdevops.com`
+2. **Add `tls.certificateRefs`** to the Gateway listener in `apigateway.yaml`
+3. Verify with: `curl -vI https://ws.siriusdevops.com/telegram/webhook/default`
+4. If cert is valid, Telegram should start delivering webhooks
+
+### P1 - Verify Secrets
+
+5. Check if SOPS secrets are actually decrypted in the cluster
+6. Run: `kubectl get secret hermes-secrets -n customer1 -o jsonpath='{.data.TELEGRAM_BOT_TOKEN}' | base64 -d`
+
+### P2 - Add Probes
+
+7. Add readiness/liveness probes to hermes-agent deployment
+8. Redeploy
+
+### P3 - Clean Up GatewayAPI
+
+9. Remove duplicate `hermes-webhook.yaml` reference from one kustomization
+10. Fix or remove Traefik-referencing ClusterIssuers
+
+### P4 - Fix vLLM Integration
+
+11. Fix `OPENAI_BASE_URL` service name or update kustomization to create `openclaw-brain-service`
+12. Set real `HF_TOKEN` in vLLM deployments
+13. Consider setting `minReplicaCount: 1` in KEDA for consistent response times
+
+---
+
+## Files Referenced
+
+### GatewayAPI & TLS
+- `infrastructure/gatewayapi/apigateway.yaml` - Gateway definition (missing TLS)
+- `infrastructure/gatewayapi/gateway-routes/hermes-webhook.yaml` - Webhook HTTPRoute
+- `infrastructure/gatewayapi/gateway-routes/route.yaml` - General route
+- `infrastructure/controllers/base/certmanager/clusterissuer.yaml` - Cert-manager (Traefik mismatch)
+
+### Hermes Agent Deployment
+- `apps/base/customer1/hermes-agent/new-deployment.yaml` - Active deployment + webhook service
+- `apps/base/customer1/hermes-agent/deployment.yaml` - Old deployment (no webhook config)
+- `apps/base/customer1/hermes-agent/configmap.yaml` - Hermes config
+- `apps/base/customer1/hermes-agent/hermes-secret.yaml` - SOPS encrypted secrets
+- `apps/base/customer1/hermes-agent/tele-webhook.yaml` - SOPS encrypted webhook secret
+- `apps/base/customer1/hermes-agent/kustomization.yaml` - Kustomize composition
+
+### vLLM Infrastructure
+- `infrastructure/gpus/base/vllm-servers/rtx6000-vllm.yaml` - Active vLLM server
+- `infrastructure/gpus/base/keda-gpu-scaling/keda-vllm.yaml` - KEDA scaling
+- `infrastructure/gpus/base/keda-gpu-scaling/vllm-route.yaml` - vLLM external route
+
+### Architecture Plans
+- `plans/AI_ARCHITECTURE.md` - AI architecture plan
+- `plans/models-to-try.md` - Models being considered
+
+---
+
+## Quick Diagnostic Commands
+
+Run these on the cluster to confirm findings:
+
+```bash
+# 1. Check if TLS cert exists for the webhook domain
+curl -vI https://ws.siriusdevops.com/telegram/webhook/default 2>&1 | grep -E 'SSL|certificate|subject|issuer'
+
+# 2. Check Gateway status
+kubectl get gateway external-http-gateway -n customer1 -o yaml
+
+# 3. Check HTTPRoute status (attached/programmed)
+kubectl get httproute http-telegram-webhook -n customer1 -o yaml
+
+# 4. Verify secrets are decrypted
+kubectl get secret hermes-secrets -n customer1 -o jsonpath='{.data.TELEGRAM_BOT_TOKEN}' | base64 -d && echo
+
+# 5. Check if pods are actually ready
+kubectl get pods -n customer1 -l app=hermes-agent -o wide
+
+# 6. Check CertMap exists
+kubectl get certmap gateway-cert-map -A 2>/dev/null || echo "CertMap NOT FOUND"
+
+# 7. Check managed certificates
+kubectl get managedcertificate -A 2>/dev/null || echo "No ManagedCertificates"
+
+# 8. Check cert-manager certificates
+kubectl get certificate -A 2>/dev/null || echo "No Certificates"
+
+# 9. Check vLLM pod status
+kubectl get pods -n customer1 -l app=rtx6000-brain-vllm -o wide
+
+# 10. Test internal webhook endpoint
+kubectl exec -n customer1 deploy/hermes-agent -- curl -s http://localhost:9118/health || echo "Health check failed"
+```
diff --git a/rays-new-deployment.yaml b/rays-new-deployment.yaml
deleted file mode 100644
index da38535..0000000
--- a/rays-new-deployment.yaml
+++ /dev/null
@@ -1,217 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: rays-hermes-agent
- namespace: customer1
-spec:
- replicas: 1
- selector:
- matchLabels:
- app: rays-hermes-agent
- template:
- metadata:
- labels:
- app: rays-hermes-agent
- spec:
- # 1. Pod-level security context to ensure volumes inherit the right group
- shareProcessNamespace: true
- securityContext:
- fsGroup: 1000
-
- # 2. Define our shared bridge volumes
- volumes:
- - name: hermes-home
- persistentVolumeClaim:
- claimName: rays-hermes-agent-pvc
- - name: hermes-agent-src
- emptyDir: {}
- - name: hermes-workspace
- emptyDir: {}
- - name: hermes-webui-app
- emptyDir: {}
- - name: hermes-configmap
- configMap:
- name: hermes-config
-
- initContainers:
- # 3. K8s workaround: Copy the agent source code into the shared emptyDir
- - name: copy-agent-source
- image: nousresearch/hermes-agent:latest
- command:
- - "sh"
- - "-c"
- - |
- cp -a /opt/hermes/. /shared-src/ && chown -R 1024:1000 /shared-src /shared-home
-
- if [ -f /tmp/hermes/config.yaml ]; then
- cp -f /tmp/hermes/config.yaml /shared-home/config.yaml
- fi
-
- mkdir -p /shared-home/.local/bin
- echo '#!/bin/sh' > /shared-home/.local/bin/gh
- echo 'exit 1' >> /shared-home/.local/bin/gh
- chmod +x /shared-home/.local/bin/gh
-
- securityContext:
- runAsUser: 0 # Run as root briefly to copy and fix permissions
- runAsNonRoot: false
- volumeMounts:
- - name: hermes-agent-src
- mountPath: /shared-src
- - name: hermes-home
- mountPath: /shared-home
- - name: hermes-configmap
- mountPath: /tmp/hermes/config.yaml
- subPath: config.yaml
- containers:
- # ==========================================
- # CONTAINER 1: HERMES AGENT
- # ==========================================
- - name: rays-hermes-agent
- image: nousresearch/hermes-agent:latest
- args: ["gateway", "run"]
- ports:
- - containerPort: 8642
-
- env:
- - name: PATH
- value: "/home/hermes/.hermes/.local/bin:/opt/hermes/.venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
- - name: HOME
- value: "/home/hermes/.hermes"
- - name: HERMES_HOME
- value: "/home/hermes/.hermes"
- - name: HERMES_UID
- value: "1024"
- - name: HERMES_GID
- value: "1000"
-
- - name: TELEGRAM_BOT_TOKEN
- valueFrom:
- secretKeyRef:
- name: hermes-secrets
- key: TELEGRAM_BOT_TOKEN
-
- - name: XAI_API_KEY
- valueFrom:
- secretKeyRef:
- name: xai-apikey
- key: XAI_API_KEY
-
- - name: TELEGRAM_ALLOWED_USERS
- value: "7105451284"
-
- # === Local vLLM (OpenAI-compatible) ===
- - name: OPENAI_BASE_URL
- value: "http://openclaw-brain-service.customer1.svc.cluster.local:8000/v1" # ← adjust if your service name differs
-
- - name: HERMES_MODEL_PROVIDER
- value: xai
-
- - name: HERMES_MODEL
- value: grok-4.20-0309-reasoning
-
- - name: OPENAI_API_KEY
- value: "dummy"
-
-
-
- volumeMounts:
- - name: hermes-home
- mountPath: /home/hermes/.hermes
- - name: hermes-agent-src
- mountPath: /opt/hermes
-
- securityContext:
- runAsUser: 1024
- runAsGroup: 1000
- runAsNonRoot: true
- allowPrivilegeEscalation: true
-
- # ==========================================
- # CONTAINER 2: HERMES WEBUI
- # ==========================================
- - name: hermes-webui
- image: ghcr.io/nesquena/hermes-webui:latest
- ports:
- - containerPort: 8787
-
- env:
- - name: HOME
- value: "/home/hermeswebui/.hermes"
- - name: HERMES_HOME
- value: "/home/hermeswebui/.hermes"
- - name: HERMES_WEBUI_HOST
- value: "0.0.0.0"
- - name: HERMES_WEBUI_PORT
- value: "8787"
- - name: HERMES_WEBUI_STATE_DIR
- value: "/home/hermeswebui/.hermes/webui"
- - name: WANTED_UID
- value: "1024"
- - name: WANTED_GID
- value: "1000"
- - name: HERMES_SKIP_CHMOD
- value: "1"
-
- volumeMounts:
- - name: hermes-home
- mountPath: /home/hermeswebui/.hermes
- # This is where the WebUI looks for the agent source code to run `uv pip install`
- - name: hermes-agent-src
- mountPath: /home/hermeswebui/.hermes/hermes-agent
- - name: hermes-workspace
- mountPath: /workspace
- - name: hermes-webui-app
- mountPath: /app
-
- resources:
- requests:
- memory: 500Mi
- cpu: "100m"
- limits:
- memory: 1Gi
- cpu: "500m"
-
- securityContext:
- runAsUser: 1024
- runAsGroup: 1000
- runAsNonRoot: true
- allowPrivilegeEscalation: true
- readOnlyRootFilesystem: false
- seccompProfile:
- type: RuntimeDefault
----
-
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
- name: rays-hermes-agent-pvc
- namespace: customer1
-spec:
- accessModes:
- - ReadWriteOnce
- resources:
- requests:
- storage: 25Gi
-
----
-
-
-apiVersion: v1
-kind: Service
-metadata:
- name: rays-hermes-webui-service
- namespace: customer1
- annotations:
- tailscale.com/expose: "true"
- tailscale.com/hostname: "rays-hermes-webui"
- tailscale.com/tags: "tag:k8s-operator"
- tailscale.com/ports: "http:8787"
-spec:
- type: ClusterIP
- selector:
- app: hermes-agent
- ports:
- - port: 8787
- targetPort: 8787
- name: http
diff --git a/trade-dashboard/Dockerfile b/trade-dashboard/Dockerfile
deleted file mode 100644
index 9ca7f24..0000000
--- a/trade-dashboard/Dockerfile
+++ /dev/null
@@ -1,18 +0,0 @@
-FROM python:3.13-slim AS base
-
-WORKDIR /app
-
-RUN apt-get update && apt-get install -y --no-install-recommends \
- gcc libpq-dev \
- && rm -rf /var/lib/apt/lists/*
-
-COPY app/requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
-
-COPY app/ ./app/
-COPY alembic.ini ./alembic.ini
-COPY alembic/ ./alembic/
-
-EXPOSE 8000
-ENV PYTHONPATH=/app/app
-CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/trade-dashboard/alembic.ini b/trade-dashboard/alembic.ini
deleted file mode 100644
index 60ba74e..0000000
--- a/trade-dashboard/alembic.ini
+++ /dev/null
@@ -1,36 +0,0 @@
-[alembic]
-script_location = alembic
-sqlalchemy.url = postgresql+asyncpg://trading:CHANGE_ME@hermes-pgdb-rw.customer1.svc.cluster.local:5432/trading_data
-
-[loggers]
-keys = root,sqlalchemy,alembic
-
-[handlers]
-keys = console
-
-[formatters]
-keys = generic
-
-[logger_root]
-level = WARN
-handlers = console
-
-[logger_sqlalchemy]
-level = WARN
-handlers =
-qualname = sqlalchemy.engine
-
-[logger_alembic]
-level = INFO
-handlers =
-qualname = alembic
-
-[handler_console]
-class = StreamHandler
-args = (sys.stderr,)
-level = NOTSET
-formatter = generic
-
-[formatter_generic]
-format = %(levelname)-5.5s [%(name)s] %(message)s
-datefmt = %H:%M:%S
diff --git a/trade-dashboard/alembic/env.py b/trade-dashboard/alembic/env.py
deleted file mode 100644
index 5fcf711..0000000
--- a/trade-dashboard/alembic/env.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Alembic environment configuration."""
-
-import sys
-from logging.config import fileConfig
-from pathlib import Path
-
-from alembic import context
-from sqlalchemy import engine_from_config, pool
-from sqlalchemy.ext.asyncio import AsyncEngine
-
-sys.path.insert(0, str(Path(__file__).parent.parent / "app"))
-
-config = context.config
-
-if config.config_file_name is not None:
- fileConfig(config.config_file_name)
-
-from models import metadata # noqa: E402
-
-target_metadata = metadata
-
-
-def run_migrations_offline() -> None:
- """Run migrations in 'offline' mode."""
- url = config.get_main_option("sqlalchemy.url")
- context.configure(
- url=url,
- target_metadata=target_metadata,
- literal_binds=True,
- dialect_opts={"paramstyle": "named"},
- )
- with context.begin_transaction():
- context.run_migrations()
-
-
-def do_run_migrations(connection):
- context.configure(connection=connection, target_metadata=target_metadata)
- with context.begin_transaction():
- context.run_migrations()
-
-
-async def run_migrations_online() -> None:
- """Run migrations in 'online' mode."""
- connectable = AsyncEngine(
- engine_from_config(
- config.get_section(config.config_ini_section) or {},
- prefix="sqlalchemy.",
- poolclass=pool.NullPool,
- future=True,
- )
- )
- async with connectable.connect() as connection:
- await connection.run_sync(do_run_migrations)
- await connectable.dispose()
-
-
-import asyncio
-
-if context.is_offline_mode():
- run_migrations_offline()
-else:
- asyncio.run(run_migrations_online())
diff --git a/trade-dashboard/alembic/script.py.mako b/trade-dashboard/alembic/script.py.mako
deleted file mode 100644
index d458e51..0000000
--- a/trade-dashboard/alembic/script.py.mako
+++ /dev/null
@@ -1,2 +0,0 @@
-# Alembic migration script - DO NOT EDIT MANUALLY
-# Use: alembic revision --autogenerate -m "description"
diff --git a/trade-dashboard/alembic/versions/001_initial.py b/trade-dashboard/alembic/versions/001_initial.py
deleted file mode 100644
index 702c57d..0000000
--- a/trade-dashboard/alembic/versions/001_initial.py
+++ /dev/null
@@ -1,43 +0,0 @@
-"""initial schema — positions table
-
-Revision ID: 001_initial
-Create Date: 2026-05-02
-"""
-
-from alembic import op
-import sqlalchemy as sa
-from sqlalchemy.dialects import postgresql
-
-revision = "001_initial"
-down_revision = None
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
- op.execute('CREATE TYPE position_direction AS ENUM (\'long\', \'short\')')
-
- op.create_table(
- "positions",
- sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
- sa.Column("symbol", sa.String(32), nullable=False),
- sa.Column("direction", postgresql.ENUM("long", "short", name="position_direction", create_type=False), nullable=False),
- sa.Column("entry_price", sa.Numeric(precision=16, scale=8), nullable=False),
- sa.Column("exit_price", sa.Numeric(precision=16, scale=8)),
- sa.Column("quantity", sa.Numeric(precision=16, scale=8), nullable=False),
- sa.Column("exchange", sa.String(32), nullable=False),
- sa.Column("opened_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
- sa.Column("closed_at", sa.DateTime(timezone=True)),
- sa.Column("pnl", sa.Numeric(precision=16, scale=2)),
- sa.Column("metadata", sa.JSON),
- )
-
- op.create_index(op.f("ix_positions_symbol"), "positions", ["symbol"])
- op.create_index(op.f("ix_positions_exchange"), "positions", ["exchange"])
-
-
-def downgrade() -> None:
- op.drop_index(op.f("ix_positions_exchange"), table_name="positions")
- op.drop_index(op.f("ix_positions_symbol"), table_name="positions")
- op.drop_table("positions")
- op.execute("DROP TYPE IF EXISTS position_direction")
diff --git a/trade-dashboard/app/database.py b/trade-dashboard/app/database.py
deleted file mode 100644
index f6c74d9..0000000
--- a/trade-dashboard/app/database.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import os
-
-from sqlalchemy import MetaData
-from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
-
-DB_USER = os.getenv("DB_USER", "trading")
-DB_PASS = os.getenv("DB_PASSWORD", "")
-DB_HOST = os.getenv("DB_HOST", "hermes-pgdb-rw.customer1.svc.cluster.local")
-DB_PORT = os.getenv("DB_PORT", "5432")
-DB_NAME = os.getenv("DB_NAME", "trading_data")
-
-DATABASE_URL = f"postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
-
-engine = create_async_engine(DATABASE_URL, echo=False, pool_size=5, max_overflow=10)
-async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
-metadata = MetaData()
diff --git a/trade-dashboard/app/main.py b/trade-dashboard/app/main.py
deleted file mode 100644
index c144033..0000000
--- a/trade-dashboard/app/main.py
+++ /dev/null
@@ -1,253 +0,0 @@
-"""Trade Dashboard — FastAPI service for tracking PnL and open positions."""
-
-from __future__ import annotations
-
-from datetime import datetime, timedelta, timezone
-from decimal import Decimal
-from pathlib import Path
-from uuid import UUID
-
-from fastapi import FastAPI, HTTPException, Query
-from fastapi.responses import FileResponse, HTMLResponse
-from sqlalchemy import and_, func, select
-from sqlalchemy.ext.asyncio import AsyncSession
-from database import async_session
-from models import positions
-from schemas import (
- Direction,
- PnLSnapshot,
- PositionCreate,
- PositionOut,
- PositionUpdate,
- WebhookTrade,
-)
-
-app = FastAPI(title="Trade Dashboard", version="0.1.0")
-
-STATIC_DIR = Path(__file__).parent / "static"
-
-
-# ── Helpers ─────────────────────────────────────────────────────────────
-
-def position_to_out(row: dict) -> PositionOut:
- return PositionOut(
- id=row["id"],
- symbol=row["symbol"],
- direction=row["direction"],
- entry_price=row["entry_price"],
- exit_price=row["exit_price"],
- quantity=row["quantity"],
- exchange=row["exchange"],
- opened_at=row["opened_at"],
- closed_at=row["closed_at"],
- pnl=row["pnl"],
- metadata=row["metadata"],
- )
-
-
-# ── Health ─────────────────────────────────────────────────────────────
-
-@app.get("/api/health")
-async def health():
- async with async_session() as session:
- result = await session.execute(select(func.now()))
- db_time = result.scalar()
- return {"status": "ok", "db_time": db_time.isoformat()}
-
-
-# ── Positions ────────────────────────────────────────────────────────────
-
-@app.get("/api/positions", response_model=list[PositionOut])
-async def list_positions(
- open_only: bool = Query(True, description="Only show open positions"),
-):
- async with async_session() as session:
- if open_only:
- stmt = select(positions).where(positions.c.closed_at.is_(None)).order_by(positions.c.opened_at.desc())
- else:
- stmt = select(positions).order_by(positions.c.opened_at.desc())
- rows = (await session.execute(stmt)).mappings().all()
- return [position_to_out(r) for r in rows]
-
-
-@app.post("/api/positions", status_code=201)
-async def create_position(payload: PositionCreate):
- async with async_session() as session:
- values = payload.model_dump()
- result = await session.execute(positions.insert().values(**values))
- session.commit()
- pk = result.inserted_primary_key[0]
- return {"id": str(pk)}
-
-
-@app.patch("/api/positions/{position_id}")
-async def update_position(position_id: UUID, payload: PositionUpdate):
- async with async_session() as session:
- row = await session.execute(
- select(positions).where(positions.c.id == position_id)
- )
- row = row.mappings().one_or_none()
- if not row:
- raise HTTPException(404, "Position not found")
-
- updates = payload.model_dump(exclude_unset=True)
-
- # Auto-compute PnL if closing
- if "exit_price" in updates:
- entry = row["entry_price"]
- qty = row["quantity"]
- exit_p = updates["exit_price"]
- direction = row["direction"]
- if direction == "long":
- updates["pnl"] = float((exit_p - entry) * qty)
- else:
- updates["pnl"] = float((entry - exit_p) * qty)
- updates["closed_at"] = datetime.now(timezone.utc)
-
- await session.execute(
- positions.update().where(positions.c.id == position_id).values(**updates)
- )
- session.commit()
-
- return {"ok": True}
-
-
-@app.delete("/api/positions/{position_id}")
-async def close_position(position_id: UUID, exit_price: Decimal = Query(None)):
- async with async_session() as session:
- row = await session.execute(
- select(positions).where(positions.c.id == position_id)
- )
- row = row.mappings().one_or_none()
- if not row:
- raise HTTPException(404, "Position not found")
-
- if row["closed_at"]:
- raise HTTPException(400, "Position already closed")
-
- exit_p = exit_price or row["entry_price"] # breakeven default
- entry = row["entry_price"]
- qty = row["quantity"]
- direction = row["direction"]
-
- if direction == "long":
- pnl = float((exit_p - entry) * qty)
- else:
- pnl = float((entry - exit_p) * qty)
-
- await session.execute(
- positions.update()
- .where(positions.c.id == position_id)
- .values(exit_price=exit_p, closed_at=datetime.now(timezone.utc), pnl=pnl)
- )
- session.commit()
-
- return {"ok": True, "pnl": pnl, "exit_price": float(exit_p)}
-
-
-# ── PnL ─────────────────────────────────────────────────────────────────
-
-@app.get("/api/pnl", response_model=PnLSnapshot)
-async def get_pnl():
- async with async_session() as session:
- now = datetime.now(timezone.utc)
- today = now.replace(hour=0, minute=0, second=0, microsecond=0)
- week_start = today - timedelta(days=now.weekday())
- month_start = today.replace(day=1)
-
- # Summary for closed trades
- closed = select(
- func.coalesce(func.sum(positions.c.pnl), 0).label("total"),
- func.count(positions.c.id).label("count"),
- ).where(positions.c.closed_at.isnot(None))
-
- result = (await session.execute(closed)).mappings().one()
- all_time_pnl = float(result["total"])
- total_trades = result["count"]
-
- # PnL by period
- def period_query(start):
- return select(
- func.coalesce(func.sum(positions.c.pnl), 0)
- ).where(
- and_(
- positions.c.closed_at.isnot(None),
- positions.c.closed_at >= start,
- )
- )
-
- today_pnl = float((await session.execute(period_query(today))).scalar())
- week_pnl = float((await session.execute(period_query(week_start))).scalar())
- month_pnl = float((await session.execute(period_query(month_start))).scalar())
-
- # Open count
- open_count = (await session.execute(
- select(func.count()).where(positions.c.closed_at.is_(None))
- )).scalar()
-
- return PnLSnapshot(
- today_pnl=Decimal(str(today_pnl)),
- week_pnl=Decimal(str(week_pnl)),
- month_pnl=Decimal(str(month_pnl)),
- all_time_pnl=Decimal(str(all_time_pnl)),
- total_trades=total_trades,
- open_positions=open_count,
- )
-
-
-@app.get("/api/pnl/history", response_model=list[PositionOut])
-async def pnl_history(
- limit: int = Query(50, ge=1, le=500),
-):
- async with async_session() as session:
- stmt = (
- select(positions)
- .where(positions.c.closed_at.isnot(None))
- .order_by(positions.c.closed_at.desc())
- .limit(limit)
- )
- rows = (await session.execute(stmt)).mappings().all()
- return [position_to_out(r) for r in rows]
-
-
-# ── Webhook (for scanner scripts) ───────────────────────────────────────
-
-@app.post("/webhook/trade", status_code=201)
-async def webhook_trade(payload: WebhookTrade):
- meta = {"strategy": payload.strategy} if payload.strategy else {}
- async with async_session() as session:
- result = await session.execute(positions.insert().values(**{
- "symbol": payload.symbol,
- "direction": payload.direction,
- "entry_price": payload.entry_price,
- "quantity": payload.quantity,
- "exchange": payload.exchange,
- "metadata": meta,
- }))
- session.commit()
- pk = result.inserted_primary_key[0]
- return {"id": str(pk)}
-
-
-# ── Frontend ────────────────────────────────────────────────────────────
-
-@app.get("/", response_class=HTMLResponse)
-async def index():
- return FileResponse(str(STATIC_DIR / "index.html"))
-
-
-# ── Startup: run Alembic migrations ──────────────────────────────────────
-
-@app.on_event("startup")
-async def startup():
- from alembic import command
- from alembic.config import Config
- from pathlib import Path
-
- alembic_cfg = Config(
- str(Path(__file__).parent.parent / "alembic.ini")
- )
-
-if __name__ == "__main__":
- import uvicorn
- uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/trade-dashboard/app/models.py b/trade-dashboard/app/models.py
deleted file mode 100644
index 9e0e92d..0000000
--- a/trade-dashboard/app/models.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from sqlalchemy import Column, String, Numeric, Enum, DateTime, JSON, func, Table
-from sqlalchemy.dialects.postgresql import UUID
-import uuid
-
-from database import metadata
-
-positions = Table(
- "positions",
- metadata,
- Column("id", UUID(as_uuid=True), primary_key=True, default=uuid.uuid4),
- Column("symbol", String(32), nullable=False, index=True),
- Column("direction", Enum("long", "short", name="position_direction"), nullable=False),
- Column("entry_price", Numeric(precision=16, scale=8), nullable=False),
- Column("exit_price", Numeric(precision=16, scale=8)),
- Column("quantity", Numeric(precision=16, scale=8), nullable=False),
- Column("exchange", String(32), nullable=False, index=True),
- Column("opened_at", DateTime(timezone=True), server_default=func.now(), nullable=False),
- Column("closed_at", DateTime(timezone=True)),
- Column("pnl", Numeric(precision=16, scale=2)),
- Column("metadata", JSON),
-)
diff --git a/trade-dashboard/app/requirements.txt b/trade-dashboard/app/requirements.txt
deleted file mode 100644
index 51462ea..0000000
--- a/trade-dashboard/app/requirements.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-fastapi==0.115.0
-uvicorn[standard]==0.32.0
-sqlalchemy[asyncio]==2.0.35
-asyncpg==0.30.0
-alembic==1.14.0
-pydantic==2.9.2
-python-dotenv==1.0.1
diff --git a/trade-dashboard/app/schemas.py b/trade-dashboard/app/schemas.py
deleted file mode 100644
index f911657..0000000
--- a/trade-dashboard/app/schemas.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from __future__ import annotations
-
-from datetime import datetime
-from decimal import Decimal
-from enum import Enum
-from typing import Optional
-from uuid import UUID
-
-from pydantic import BaseModel
-
-
-class Direction(str, Enum):
- long = "long"
- short = "short"
-
-
-# ─── Request schemas ──────────────────────────────────────────────
-
-class PositionCreate(BaseModel):
- symbol: str
- direction: Direction
- entry_price: Decimal
- quantity: Decimal
- exchange: str
- metadata: Optional[dict] = None
-
-
-class PositionUpdate(BaseModel):
- entry_price: Optional[Decimal] = None
- exit_price: Optional[Decimal] = None
- quantity: Optional[Decimal] = None
- metadata: Optional[dict] = None
-
-
-class WebhookTrade(BaseModel):
- """Payload from automated scanner scripts."""
- symbol: str
- direction: Direction
- entry_price: Decimal
- quantity: Decimal
- exchange: str
- strategy: Optional[str] = None
-
-
-# ─── Response schemas ─────────────────────────────────────────────
-
-class PositionOut(BaseModel):
- id: UUID
- symbol: str
- direction: Direction
- entry_price: Decimal
- exit_price: Optional[Decimal]
- quantity: Decimal
- exchange: str
- opened_at: datetime
- closed_at: Optional[datetime]
- pnl: Optional[Decimal]
- metadata: Optional[dict]
-
- model_config = {"from_attributes": True}
-
-
-class PnLSnapshot(BaseModel):
- today_pnl: Decimal
- week_pnl: Decimal
- month_pnl: Decimal
- all_time_pnl: Decimal
- total_trades: int
- open_positions: int
-
diff --git a/trade-dashboard/app/static/index.html b/trade-dashboard/app/static/index.html
deleted file mode 100644
index 8795aba..0000000
--- a/trade-dashboard/app/static/index.html
+++ /dev/null
@@ -1,158 +0,0 @@
-
-
-
-
-
-Trade Dashboard
-
-
-
-📊 Trade Dashboard
-
-
-
-
-
-
-
Open Positions 0
-
- | Symbol | Dir | Entry | Qty | Exchange | Opened | Action |
-
-
-
-
-
-
-
Open New Position
-
-
-
-
-
-
Trade History
-
- | Symbol | Dir | Entry | Exit | Qty | PnL | Exchange | Closed |
-
-
-
-
-
-
-