Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
314265b
release: merge develop into main for v0.22.2
DavidsonGomes Apr 14, 2026
ba6328c
release: merge develop into main for v0.22.3
DavidsonGomes Apr 14, 2026
baa4360
fix: add PID file lock to prevent multiple scheduler instances
Apr 15, 2026
2c3c14b
release: merge develop into main for v0.22.4
DavidsonGomes Apr 15, 2026
6f05660
release: merge develop into main for v0.22.5
DavidsonGomes Apr 15, 2026
115d64d
release: merge develop into main for v0.23.0
DavidsonGomes Apr 15, 2026
0b051af
fix(scheduler): atomic PID lock to prevent duplicate instances
Apr 16, 2026
b7bfab5
fix(dashboard): restart-all kills processes directly instead of syste…
Apr 16, 2026
7782615
fix(heartbeat): pass prompt as positional arg instead of -p flag
Apr 16, 2026
25004fb
release: merge develop into main for v0.23.1
DavidsonGomes Apr 16, 2026
e251ed7
release: merge develop into main for v0.23.2
DavidsonGomes Apr 16, 2026
e2ae123
fix(scheduler): remove duplicate scheduler thread from app.py
Apr 17, 2026
64b7107
release: merge develop into main for v0.24.0
DavidsonGomes Apr 17, 2026
d934d9c
fix(swarm): three deploy bugs found during production setup
MarcelocardosoLeal Apr 18, 2026
7da1ca1
fix(swarm): add claude-auth volume and fix docker-compose for dashboard
MarcelocardosoLeal Apr 18, 2026
61bd3a4
fix(swarm): add claude_auth volume to official stack template
MarcelocardosoLeal Apr 18, 2026
1c858e6
fix(dashboard): three more bugs found in production after F1.4 redeploy
MarcelocardosoLeal Apr 18, 2026
bc06c5e
fix(swarm): restore /root/.claude.json from backup on container start
MarcelocardosoLeal Apr 19, 2026
ff1b770
fix(dashboard): copy .claude/ and docs/ into image
MarcelocardosoLeal Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions .claude/skills/fin-daily-pulse/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: fin-daily-pulse
description: "Daily financial pulse — queries Stripe (MRR, charges, churn, failures) and Omie (accounts payable/receivable, invoices) to generate an HTML snapshot of the company's financial health. Trigger when user says 'financial pulse', 'financial snapshot', or 'financial metrics'."
description: "Daily financial pulse — queries Stripe (MRR, charges, churn, failures), Omie (accounts payable/receivable, invoices) and Evo Academy (courses, subscriptions, Summit tickets) to generate an HTML snapshot of the company's financial health. Trigger when user says 'financial pulse', 'financial snapshot', or 'financial metrics'."
---

# Financial Pulse — Daily Financial Snapshot

Daily routine that pulls data from Stripe and Omie to generate an HTML snapshot of financial health.
Daily routine that pulls data from Stripe, Omie and Evo Academy to generate an HTML snapshot of financial health.

**Always respond in English.**

Expand Down Expand Up @@ -47,15 +47,49 @@ Use the `/int-omie` skill to fetch:
- Fetch invoices pending issuance
- Count invoices issued in the current month


## Step 2.5 — Collect Evo Academy data (silently)

Call the Evo Academy Analytics API directly:
- **Base URL:** `$EVO_ACADEMY_BASE_URL` (env var)
- **Auth:** `Authorization: Bearer $EVO_ACADEMY_API_KEY`

### 2.5a. Summary do dia
```
GET /api/v1/analytics/summary?period=today
```
Captura: `revenue.total`, `orders.completed`, `orders.pending`, `orders.failed`, `subscriptions.active`, `students.new_in_period`

### 2.5b. Orders completados hoje
```
GET /api/v1/analytics/orders?status=completed&created_after=YYYY-MM-DD&per_page=100
```
(hoje em BRT; converter para UTC → `created_after = date.today().isoformat()`)
- Itere paginação por cursor até `meta.has_more = false`
- Some `amount` de todos os orders → receita bruta Evo Academy do dia
- Separe por tipo: renovações (`is_renewal=true`) vs novos (`is_renewal=false`)
- Agrupe por produto: cursos, assinaturas, ingressos, outros

### 2.5c. MRR de assinaturas ativas (Evo Academy)
```
GET /api/v1/analytics/subscriptions?status=active&per_page=100
```
- Itere até `meta.has_more = false`
- Some `plan.price` de cada assinatura ativa → MRR Evo Academy

## Step 3 — Day's transactions

Consolidate all financial transactions for the day:
- Stripe charges (revenue)
- Evo Academy orders (revenue — courses / subscriptions / tickets)
- Payments recorded in Omie (expenses)
- Refunds

Format each transaction with: type (Revenue/Expense/Refund), description, amount, status.

**Total revenue = Stripe today + Evo Academy today**
**Total MRR = Stripe MRR + Evo Academy MRR**

## Step 4 — Classify financial health

Define the health badge (CSS class):
Expand Down Expand Up @@ -105,7 +139,8 @@ Create the directory `workspace/finance/reports/daily/` if it does not exist.
## Financial Pulse generated

**File:** workspace/finance/reports/daily/[C] YYYY-MM-DD-financial-pulse.html
**MRR:** R$ X,XXX | **Subscriptions:** N | **Churn:** X%
**MRR total:** R$ X,XXX (Stripe: R$ X,XXX | Evo Academy: R$ X,XXX)
**Receita hoje:** R$ X,XXX | **Subscriptions:** N | **Churn:** X%
**Alerts:** {N} attention points
```

Expand Down
44 changes: 31 additions & 13 deletions .claude/skills/fin-monthly-close-kickoff/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: fin-monthly-close-kickoff
description: "Monthly close kickoff — initiates the month-end closing process with a checklist, simplified P&L, pending reconciliations, receivables, payables, and action items for the finance team. Trigger when user says 'monthly close', 'start closing', 'closing kickoff', or on the 1st of each month."
description: "Monthly close kickoff — initiates the month-end closing process with a checklist, simplified P&L (Stripe + Omie + Evo Academy), pending reconciliations, receivables, payables, and action items for the finance team. Trigger when user says 'monthly close', 'start closing', 'closing kickoff', or on the 1st of each month."
---

# Monthly Close Kickoff
Expand Down Expand Up @@ -33,14 +33,30 @@ Use `/int-omie`:
- Invoices issued during the month
- Invoices that should have been issued but were not

### 2c. Outstanding receivables

### 2c. Revenue (Evo Academy)
Call `GET /api/v1/analytics/summary?period=30d` (env: `$EVO_ACADEMY_BASE_URL`, auth: `Bearer $EVO_ACADEMY_API_KEY`):
- `revenue.total` → receita bruta do mês
- `orders.completed / pending / refunded` → contagem por status
- `subscriptions.active / cancelled` → base e churn do mês

Fetch todos os orders do mês: `GET /api/v1/analytics/orders?status=completed&created_after=YYYY-MM-01&created_before=YYYY-MM-31&per_page=100`
- Itere por cursor até `has_more=false`
- Some `amount` → receita total do mês
- Separe por produto: Evo Academy (R$950/mês), Evolution Builder (R$970/mês), Curso Agentic Engineer (R$2k/mês), Beta Access (R$370/mês), one-time (Blueprint Pack, Fast Start Pro), Evo Setup (R$5/mês)
- Identifique renovações (`is_renewal=true`) vs novos clientes

Fetch assinaturas ativas no fim do mês: `GET /api/v1/analytics/subscriptions?status=active&per_page=100`
- MRR Evo Academy = soma de `plan.price` das ativas

### 2d. Outstanding receivables
- List all open receivables (from the month or earlier)
- Highlight overdue items

### 2d. Next month's payables
### 2e. Next month's payables
- List payables due in the current month (the upcoming month)

### 2e. Previous month (for comparison)
### 2f. Previous month (for comparison)
- Read the previous month's financial report from `workspace/finance/reports/monthly/` if it exists
- Or use data from the last monthly close

Expand All @@ -51,6 +67,7 @@ Structure the income statement with:
| Account | Actual | Prior Month | Variance |
|---------|--------|-------------|----------|
| Gross Revenue (Stripe) | | | |
| Gross Revenue (Evo Academy) | | | |
| Gross Revenue (Omie/Services) | | | |
| (-) Taxes | | | |
| **Net Revenue** | | | |
Expand All @@ -68,14 +85,15 @@ Structure the income statement with:
Generate a checklist with initial status for each item:

1. **Reconcile Stripe** — verify all charges match received payments
2. **Reconcile Omie** — verify entries and exits in the ERP are correct
3. **Issue pending invoices** — list invoices that need to be issued (finance team)
4. **Collect overdue accounts** — list clients with late payments
5. **Categorize expenses** — verify all expenses are categorized
6. **Review entries** — verify manual or atypical entries
7. **Calculate taxes** — verify month's tax obligations
8. **Generate final income statement** — after reconciliations, generate the definitive P&L
9. **Approve close** — the responsible person reviews and approves
2. **Reconcile Evo Academy** — verify orders and subscriptions match expected MRR
3. **Reconcile Omie** — verify entries and exits in the ERP are correct
4. **Issue pending invoices** — list invoices that need to be issued (finance team)
5. **Collect overdue accounts** — list clients with late payments
6. **Categorize expenses** — verify all expenses are categorized
7. **Review entries** — verify manual or atypical entries
8. **Calculate taxes** — verify month's tax obligations
9. **Generate final income statement** — after reconciliations, generate the definitive P&L
10. **Approve close** — the responsible person reviews and approves

Possible statuses:
- `done` (checkmark) — already completed automatically
Expand Down Expand Up @@ -161,7 +179,7 @@ Create the directory `workspace/finance/reports/monthly/` if it does not exist.
**File:** workspace/finance/reports/monthly/[C] YYYY-MM-monthly-close.html
**Month:** {reference month}
**Revenue:** R$ X,XXX | **Expenses:** R$ X,XXX | **Result:** R$ X,XXX
**Checklist:** X/9 completed
**Checklist:** X/10 completed
**Finance team pending items:** {N} items
```

Expand Down
34 changes: 30 additions & 4 deletions .claude/skills/fin-weekly-report/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
name: fin-weekly-report
description: "Weekly financial report — consolidates Stripe and Omie data for the week: revenue, expenses, cash flow projection, overdue accounts, and variance analysis. Trigger when user says 'financial weekly', 'weekly financial report', or 'financial summary of the week'."
description: "Weekly financial report — consolidates Stripe, Omie and Evo Academy data for the week: revenue (courses, subscriptions, tickets), expenses, cash flow projection, overdue accounts, and variance analysis. Trigger when user says 'financial weekly', 'weekly financial report', or 'financial summary of the week'."
---

# Financial Weekly — Weekly Financial Report

Weekly routine that consolidates the week's financial data: revenue, expenses, Stripe, Omie, projected cash flow, and analysis.
Weekly routine that consolidates the week's financial data: revenue, expenses, Stripe, Omie, Evo Academy, projected cash flow, and analysis.

**Always respond in English.**

Expand All @@ -24,8 +24,25 @@ Use `/int-omie` to fetch:
- Confirmed receipts for the week
- Invoices issued during the week


### 1c. Evo Academy — revenue
Call `GET /api/v1/analytics/summary?period=7d` (env: `$EVO_ACADEMY_BASE_URL`, auth: `Bearer $EVO_ACADEMY_API_KEY`):
- `revenue.total` → receita bruta da semana
- `orders.completed` → número de vendas
- `subscriptions.active` / `subscriptions.cancelled` → net change

Fetch orders da semana: `GET /api/v1/analytics/orders?status=completed&created_after=YYYY-MM-DD&per_page=100`
- Itere por cursor até `has_more=false`
- Some `amount` → receita total Evo Academy na semana
- Separe: renovações vs novos, one-time vs assinatura

Fetch assinaturas novas na semana: `GET /api/v1/analytics/subscriptions?status=active&created_after=YYYY-MM-DD&per_page=100`
- MRR adicionado = soma dos `plan.price` de assinaturas criadas na semana

Group revenue by category:
- Stripe Subscriptions
- Evo Academy — Courses & Subscriptions
- Evo Academy — One-time (tickets, packs)
- Services / Consulting
- Partnerships
- Other
Expand Down Expand Up @@ -59,10 +76,19 @@ Consolidate the week's Omie metrics:
- Invoices issued during the week
- Confirmed receipts

## Step 4.5 — Detailed Evo Academy metrics

Consolidate Evo Academy's week metrics:
- MRR (sum of all active subscription `plan.price`) and variance vs prior week
- New subscriptions vs cancellations
- One-time revenue (tickets, packs, live events)
- Top-selling products of the week
- Students enrolled (`students.new_in_period`)

## Step 5 — Cash flow projection (4 weeks)

Based on collected data, project:
- Expected inflows (Stripe recurring + receivables)
- Expected inflows (Stripe recurring + Evo Academy subscriptions + receivables)
- Expected outflows (payables + recurring expenses)
- Balance and cumulative by week

Expand Down Expand Up @@ -135,7 +161,7 @@ Create the directory `workspace/finance/reports/weekly/` if it does not exist.

**File:** workspace/finance/reports/weekly/[C] YYYY-WXX-financial-weekly.html
**Revenue:** R$ X,XXX ({var}%) | **Expenses:** R$ X,XXX ({var}%)
**MRR:** R$ X,XXX | **Projected 30d balance:** R$ XX,XXX
**MRR total:** R$ X,XXX (Stripe: R$ X,XXX | Evo Academy: R$ X,XXX) | **Projected 30d balance:** R$ XX,XXX
**Alerts:** {N} overdue accounts | {N} pending invoices
```

Expand Down
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
.env
__pycache__/
*.pyc
dashboard/data/
workspace/
ADWs/logs/
ADWs/__pycache__/
.claude/agent-memory/
Expand Down
8 changes: 8 additions & 0 deletions Dockerfile.dashboard
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ COPY dashboard/backend/ dashboard/backend/
COPY social-auth/ social-auth/
COPY scheduler.py ./

# Copy workspace assets the backend reads at runtime.
# Without these, /api/agents, /api/skills, /api/commands etc. all return empty
# and the UI shows "No agents found" / "No skills found" on a fresh deploy.
# .claude/agent-memory and .claude/.env are excluded by .dockerignore so user
# data and secrets stay out of the image.
COPY .claude/ .claude/
COPY docs/ docs/

# Copy built frontend from stage 1
COPY --from=frontend-build /frontend/dist dashboard/frontend/dist

Expand Down
53 changes: 17 additions & 36 deletions dashboard/backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,13 +637,16 @@ def serve_frontend(path):
port = int(cfg["port"])
except Exception:
pass
# Start scheduler in background thread
# Scheduler runs as a standalone process (scheduler.py) started by start-services.sh.
# A thread here would create a duplicate instance — all routines would fire 2-3x.
# One-off scheduled tasks (ScheduledTask model) are checked by the standalone scheduler
# via _run_pending_tasks, which is called from its own loop.
import threading

def _run_pending_tasks():
"""Check for pending scheduled tasks and execute them."""
from datetime import datetime as _dt, timezone as _tz
from models import ScheduledTask
from routes.tasks import _execute_task

try:
now = _dt.now(_tz.utc)
Expand All @@ -659,45 +662,23 @@ def _run_pending_tasks():

t = threading.Thread(target=_execute_task_with_context, args=(task.id,), daemon=True)
t.start()
except Exception as e:
pass # Don't crash scheduler loop on task errors
except Exception:
pass

def _execute_task_with_context(task_id):
with app.app_context():
from routes.tasks import _execute_task
_execute_task(task_id)

def _run_scheduler():
log_path = WORKSPACE / "ADWs" / "logs" / "scheduler.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
try:
import importlib.util
spec = importlib.util.spec_from_file_location("scheduler", WORKSPACE / "scheduler.py")
sched_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sched_module)
sched_module.setup_schedule()

import schedule as sched_lib
import time as _time
from datetime import datetime as _dt

with open(log_path, "a") as log:
log.write(f"\n[{_dt.now().strftime('%Y-%m-%d %H:%M:%S')}] Scheduler started ({len(sched_lib.get_jobs())} routines)\n")
log.flush()

while True:
sched_lib.run_pending()
# Check for one-off scheduled tasks
with app.app_context():
_run_pending_tasks()
_time.sleep(30)
except Exception as e:
with open(log_path, "a") as log:
log.write(f"Scheduler error: {e}\n")
print(f"Scheduler failed to start: {e}")

sched_thread = threading.Thread(target=_run_scheduler, daemon=True, name="scheduler")
sched_thread.start()
print(f" Scheduler started in background")
def _poll_scheduled_tasks():
"""Lightweight thread that only polls ScheduledTask — no routine scheduling."""
import time as _time
while True:
with app.app_context():
_run_pending_tasks()
_time.sleep(30)

task_thread = threading.Thread(target=_poll_scheduled_tasks, daemon=True, name="task-poller")
task_thread.start()

app.run(host="0.0.0.0", port=port, debug=False)
18 changes: 17 additions & 1 deletion dashboard/terminal-server/src/claude-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class ClaudeBridge {
_loadProviderConfig() {
const ALLOWED_CLI = new Set(['claude', 'openclaude']);
const ALLOWED_VARS = new Set([
'ANTHROPIC_API_KEY',
'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GEMINI',
'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX',
'OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_MODEL',
Expand Down Expand Up @@ -138,7 +139,22 @@ class ClaudeBridge {

async startSession(sessionId, options = {}) {
if (this.sessions.has(sessionId)) {
throw new Error(`Session ${sessionId} already exists`);
const existing = this.sessions.get(sessionId);
if (existing.active) {
// Idempotent: a duplicate startSession can arrive when the WebSocket
// reconnects through a reverse proxy (Traefik) and the frontend
// re-sends start_claude before learning the session is still alive.
// Returning the existing session instead of throwing prevents a
// confusing "Session already exists" toast on the user's terminal
// while keeping the original PTY intact.
console.log(`[bridge] startSession(${sessionId}) — already active, returning existing session`);
return existing;
}
// Orphaned dead session — clean up and restart
if (existing.process) {
try { existing.process.kill('SIGKILL'); } catch (_) {}
}
this.sessions.delete(sessionId);
}

const {
Expand Down
6 changes: 5 additions & 1 deletion dashboard/terminal-server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,11 @@ class TerminalServer {
if (!session) return;

if (session.active) {
this.sendToWebSocket(wsInfo.ws, { type: 'error', message: 'An agent is already running in this session' });
// Frontend may re-send start_claude on WebSocket reconnect (common
// through reverse proxies like Traefik). The session is already
// running — replay the buffer and tell the client it's attached
// instead of surfacing a misleading error toast.
this.sendToWebSocket(wsInfo.ws, { type: 'claude_started', sessionId: wsInfo.claudeSessionId });
return;
}

Expand Down
Loading