diff --git a/.forgejo/workflows/pipeline.yaml b/.forgejo/workflows/pipeline.yaml
index 735c2ef..fdcd8f5 100644
--- a/.forgejo/workflows/pipeline.yaml
+++ b/.forgejo/workflows/pipeline.yaml
@@ -11,31 +11,6 @@ jobs:
with:
node-version: 24
- uses: actions/checkout@v6
- - name: Cache Node.js modules
- uses: actions/cache@v4
- with:
- path: frontend/node_modules
- key: ${{ runner.os }}-node-${{ hashFiles('frontend/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-node-
- - name: Cache Cargo registry
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/bin/
- ~/.cargo/registry/index/
- ~/.cargo/registry/cache/
- ~/.cargo/git/db/
- key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
- restore-keys: |
- ${{ runner.os }}-cargo-
- - name: Cache Cargo target
- uses: actions/cache@v4
- with:
- path: target/
- key: ${{ runner.os }}-cargo-target-${{ hashFiles('Cargo.lock') }}
- restore-keys: |
- ${{ runner.os }}-cargo-target-
- run: |
cd frontend
npm install
diff --git a/.gitignore b/.gitignore
index 9bf488d..771536c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
/target
node_modules
-/frontend/dist
-.env
\ No newline at end of file
+/frontend/dist
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index 907bd9b..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,154 +0,0 @@
-# AGENTS.md
-
-## Overview
-
-This is **Antigravity Agent** — an autonomous AI agent platform built with a Rust backend and a vanilla JS frontend. Users create "tasks" (goals), and the system dispatches an LLM-powered agent to accomplish them. Tasks can be run on-demand or on a cron schedule. Each execution produces logs and a final answer, all persisted to PostgreSQL.
-
-## Architecture
-
-```
-┌─────────────────────────────────────────────────────────┐
-│ Frontend (Vite + Vanilla JS) │
-│ - OAuth login via Authentik │
-│ - Dashboard: recent runs, task list, logs, answers │
-│ - Polls /api/tasks every 3s for live updates │
-└──────────────────────┬──────────────────────────────────┘
- │ /api/*
-┌──────────────────────▼──────────────────────────────────┐
-│ Axum HTTP Server (src/server/) │
-│ - CORS, CSP, rate limiting (1 MB body) │
-│ - Cookie-based + Bearer token auth │
-│ - Routes: tasks CRUD, runs, auth (callback/refresh) │
-├─────────────────────────────────────────────────────────┤
-│ Domain Layer (src/domain/) │
-│ ├─ agent/ Agent loop, LLM API client, tool defs │
-│ ├─ auth JwksVerifier, Authenticator (OIDC) │
-│ └─ tasks Task execution, run management │
-├─────────────────────────────────────────────────────────┤
-│ Scheduler (src/scheduler.rs) │
-│ - tokio-cron-scheduler for recurring task execution │
-├─────────────────────────────────────────────────────────┤
-│ Entities (src/entities/) │
-│ - task, task_run (SeaORM models) │
-├─────────────────────────────────────────────────────────┤
-│ PostgreSQL │
-│ - Migrations managed via sea-orm-migration │
-└─────────────────────────────────────────────────────────┘
-```
-
-## Project Structure
-
-```
-bot/
-├── Cargo.toml # Workspace root (members: ".", "migration")
-├── src/
-│ ├── main.rs # Entrypoint: tracing init → server::start()
-│ ├── config.rs # Config struct loaded from env vars
-│ ├── error.rs # AppError enum (thiserror) → Axum responses
-│ ├── scheduler.rs # Cron job scheduler (wraps tokio-cron-scheduler)
-│ ├── tests.rs # Unit tests for error handling & config
-│ ├── domain/
-│ │ ├── agent/
-│ │ │ ├── mod.rs # Agent struct: agentic loop with turn/time limits
-│ │ │ ├── api.rs # LLM request/response types, Tavily search client
-│ │ │ └── tools.rs # Tool definitions (google_search, finish) & dispatch
-│ │ ├── auth.rs # JwksVerifier (RSA/JWKS), Authenticator (code exchange, refresh)
-│ │ └── tasks.rs # Task execution logic, response DTOs
-│ ├── entities/
-│ │ ├── task.rs # SeaORM entity: tasks table
-│ │ └── task_run.rs # SeaORM entity: task_runs table (belongs_to task)
-│ └── server/
-│ ├── mod.rs # App bootstrap: DB, scheduler, auth, router, CORS
-│ ├── auth.rs # Auth routes & AuthenticatedUser extractor
-│ └── tasks.rs # Task CRUD & run endpoints
-├── migration/
-│ └── src/ # SeaORM migrations (tasks, answer col, runs table, cron col)
-├── frontend/
-│ ├── index.html # SPA shell with glassmorphism dark theme
-│ ├── src/
-│ │ ├── main.js # All app logic: auth flow, task/run rendering, polling
-│ │ └── style.css # Styles
-│ ├── vite.config.js # Dev proxy: /api → localhost:3000
-│ └── package.json # Deps: vite, marked, dompurify
-└── .forgejo/workflows/
- └── pipeline.yaml # CI: build frontend + cargo build → deploy via systemd
-```
-
-## Agent System
-
-The agent (`src/domain/agent/`) is a turn-based autonomous loop:
-
-1. A system prompt is injected with the current date and instructions not to ask the user for clarification.
-2. The user's goal is sent as the initial message.
-3. Each turn calls the **Kimi K2.5** model via the Zen API (`https://opencode.ai/zen/v1/chat/completions`).
-4. The model can invoke tools:
- - **`google_search`** — web search via the Tavily API.
- - **`finish`** — signals completion and provides the final answer.
-5. Tool results are appended to the conversation and the loop continues.
-6. The loop terminates when `finish` is called, the turn limit is hit (`AGENT_MAX_TURNS`, default 20), or the time limit expires (`AGENT_MAX_DURATION_SECS`, default 120s).
-
-All turns and tool calls are logged. The final answer (if any) and the full log are persisted to the `task_runs` table.
-
-## Environment Variables
-
-| Variable | Required | Default | Description |
-|---------------------------|----------|---------|----------------------------------------------|
-| `DATABASE_URL` | ✅ | — | PostgreSQL connection string |
-| `PORT` | | `3000` | HTTP server port |
-| `ZEN_API_KEY` | | — | API key for Zen/Kimi LLM |
-| `TAVILY_API_KEY` | | — | API key for Tavily web search |
-| `AUTHENTIK_ISSUER` | ✅ | — | OIDC issuer URL (Authentik) |
-| `AUTHENTIK_CLIENT_ID` | ✅ | — | OAuth client ID |
-| `AUTHENTIK_CLIENT_SECRET` | ✅ | — | OAuth client secret |
-| `CORS_ALLOWED_ORIGINS` | | — | Comma-separated allowed origins (or mirror) |
-| `COOKIE_SECURE` | | `false` | Set `true` for HTTPS-only cookies |
-| `AGENT_MAX_TURNS` | | `20` | Max LLM turns per agent run |
-| `AGENT_MAX_DURATION_SECS` | | `120` | Max wall-clock seconds per agent run |
-
-## API Routes
-
-All task/run routes require authentication (cookie or Bearer token).
-
-| Method | Path | Description |
-|--------|------------------------|-----------------------------------|
-| GET | `/api/tasks` | List all tasks with their runs |
-| POST | `/api/tasks` | Create a new task |
-| GET | `/api/tasks/:id` | Get a single task with runs |
-| PUT | `/api/tasks/:id` | Update task goal/cron schedule |
-| POST | `/api/tasks/:id/runs` | Trigger a manual re-run |
-| GET | `/api/runs/recent` | Latest 50 runs across all tasks |
-| GET | `/api/auth/session` | Check current session |
-| GET | `/api/auth/callback` | OAuth code → token exchange |
-| POST | `/api/auth/refresh` | Refresh access token |
-| POST | `/api/auth/logout` | Clear auth cookies |
-
-## Database Schema
-
-**`tasks`** — `id` (UUID PK), `goal` (text), `cron` (text, nullable), `created_at` (timestamptz)
-
-**`task_runs`** — `id` (UUID PK), `task_id` (FK → tasks, cascade delete), `status` (text), `logs` (text), `answer` (text, nullable), `created_at` (timestamptz)
-
-Migrations are in `migration/src/` and run automatically on startup via `Migrator::up()`.
-
-## Development
-
-```bash
-# Backend (from repo root)
-cargo run # requires DATABASE_URL + Authentik vars
-
-# Frontend (from frontend/)
-npm install
-npm run dev # Vite dev server on :5173, proxies /api to :3000
-
-# Tests
-cargo test
-```
-
-## CI / Deployment
-
-The Forgejo Actions pipeline (`.forgejo/workflows/pipeline.yaml`) triggers on release publish:
-
-1. Builds the frontend (`npm install && npm run build`).
-2. Builds the Rust binary (`cargo build -r`).
-3. Uploads the binary as a release asset.
-4. Deploys to the host: copies binary + frontend dist, restarts the `bot` systemd user service.
diff --git a/Cargo.lock b/Cargo.lock
index b8d0c9c..1a9f8e6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -93,29 +93,12 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "anyhow"
-version = "1.0.101"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
-
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
-[[package]]
-name = "async-channel"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
-dependencies = [
- "concurrent-queue",
- "event-listener 2.5.3",
- "futures-core",
-]
-
[[package]]
name = "async-stream"
version = "0.3.6"
@@ -178,10 +161,9 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core",
- "base64 0.22.1",
"bytes",
"futures-util",
- "http 1.4.0",
+ "http",
"http-body",
"http-body-util",
"hyper",
@@ -197,10 +179,8 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
- "sha1",
"sync_wrapper",
"tokio",
- "tokio-tungstenite",
"tower",
"tower-layer",
"tower-service",
@@ -216,7 +196,7 @@ dependencies = [
"async-trait",
"bytes",
"futures-util",
- "http 1.4.0",
+ "http",
"http-body",
"http-body-util",
"mime",
@@ -237,11 +217,10 @@ dependencies = [
"axum",
"axum-core",
"bytes",
- "cookie",
- "fastrand 2.3.0",
+ "fastrand",
"futures-util",
"headers",
- "http 1.4.0",
+ "http",
"http-body",
"http-body-util",
"mime",
@@ -259,18 +238,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
-[[package]]
-name = "base64"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-
-[[package]]
-name = "base64"
-version = "0.21.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
-
[[package]]
name = "base64"
version = "0.22.1"
@@ -297,18 +264,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "binstring"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef"
-
-[[package]]
-name = "bitflags"
-version = "1.3.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
-
[[package]]
name = "bitflags"
version = "2.10.0"
@@ -368,12 +323,9 @@ version = "0.1.0"
dependencies = [
"axum",
"axum-extra",
- "base64 0.22.1",
+ "base64",
"chrono",
- "cookie",
"dashmap",
- "dotenvy",
- "isahc",
"jsonwebtoken",
"migration",
"reqwest",
@@ -381,14 +333,10 @@ dependencies = [
"sea-orm-migration",
"serde",
"serde_json",
- "thiserror 2.0.18",
"tokio",
"tokio-cron-scheduler",
"tower-http 0.5.2",
- "tracing",
- "tracing-subscriber",
"uuid",
- "web-push",
]
[[package]]
@@ -431,12 +379,6 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
-[[package]]
-name = "castaway"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
-
[[package]]
name = "cc"
version = "1.2.55"
@@ -523,17 +465,6 @@ version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
-[[package]]
-name = "coarsetime"
-version = "0.1.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2"
-dependencies = [
- "libc",
- "wasix",
- "wasm-bindgen",
-]
-
[[package]]
name = "colorchoice"
version = "1.0.4"
@@ -549,29 +480,12 @@ dependencies = [
"crossbeam-utils",
]
-[[package]]
-name = "const-oid"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b"
-
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
-[[package]]
-name = "cookie"
-version = "0.18.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
-dependencies = [
- "percent-encoding",
- "time",
- "version_check",
-]
-
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -660,43 +574,6 @@ dependencies = [
"typenum",
]
-[[package]]
-name = "ct-codecs"
-version = "1.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
-
-[[package]]
-name = "curl"
-version = "0.4.49"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc"
-dependencies = [
- "curl-sys",
- "libc",
- "openssl-probe",
- "openssl-sys",
- "schannel",
- "socket2",
- "windows-sys 0.59.0",
-]
-
-[[package]]
-name = "curl-sys"
-version = "0.4.85+curl-8.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0efa6142b5ecc05f6d3eaa39e6af4888b9d3939273fb592c92b7088a8cf3fdb"
-dependencies = [
- "cc",
- "libc",
- "libnghttp2-sys",
- "libz-sys",
- "openssl-sys",
- "pkg-config",
- "vcpkg",
- "windows-sys 0.59.0",
-]
-
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@@ -773,56 +650,17 @@ dependencies = [
"parking_lot_core",
]
-[[package]]
-name = "data-encoding"
-version = "2.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
-
-[[package]]
-name = "der"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4"
-dependencies = [
- "const-oid 0.6.2",
- "der_derive",
-]
-
-[[package]]
-name = "der"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
-dependencies = [
- "const-oid 0.9.6",
- "pem-rfc7468 0.6.0",
- "zeroize",
-]
-
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
- "const-oid 0.9.6",
- "pem-rfc7468 0.7.0",
+ "const-oid",
+ "pem-rfc7468",
"zeroize",
]
-[[package]]
-name = "der_derive"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8aed3b3c608dc56cf36c45fe979d04eda51242e6703d8d0bb03426ef7c41db6a"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "synstructure 0.12.6",
-]
-
[[package]]
name = "deranged"
version = "0.5.5"
@@ -893,7 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
- "const-oid 0.9.6",
+ "const-oid",
"crypto-common",
"subtle",
]
@@ -921,30 +759,12 @@ version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
- "der 0.7.10",
+ "der",
"digest",
"elliptic-curve",
"rfc6979",
- "signature 2.2.0",
- "spki 0.7.3",
-]
-
-[[package]]
-name = "ece"
-version = "2.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2ea1d2f2cc974957a4e2575d8e5bb494549bab66338d6320c2789abcfff5746"
-dependencies = [
- "base64 0.21.7",
- "byteorder",
- "hex",
- "hkdf",
- "lazy_static",
- "once_cell",
- "openssl",
- "serde",
- "sha2",
- "thiserror 1.0.69",
+ "signature",
+ "spki",
]
[[package]]
@@ -953,18 +773,8 @@ version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
- "pkcs8 0.10.2",
- "signature 2.2.0",
-]
-
-[[package]]
-name = "ed25519-compact"
-version = "2.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ce99a9e19c84beb4cc35ece85374335ccc398240712114c85038319ed709bd"
-dependencies = [
- "ct-codecs",
- "getrandom 0.3.4",
+ "pkcs8",
+ "signature",
]
[[package]]
@@ -1003,8 +813,8 @@ dependencies = [
"generic-array",
"group",
"hkdf",
- "pem-rfc7468 0.7.0",
- "pkcs8 0.10.2",
+ "pem-rfc7468",
+ "pkcs8",
"rand_core",
"sec1",
"subtle",
@@ -1047,12 +857,6 @@ dependencies = [
"windows-sys 0.48.0",
]
-[[package]]
-name = "event-listener"
-version = "2.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
-
[[package]]
name = "event-listener"
version = "5.4.1"
@@ -1064,15 +868,6 @@ dependencies = [
"pin-project-lite",
]
-[[package]]
-name = "fastrand"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
-dependencies = [
- "instant",
-]
-
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -1212,34 +1007,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
-[[package]]
-name = "futures-lite"
-version = "1.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
-dependencies = [
- "fastrand 1.9.0",
- "futures-core",
- "futures-io",
- "memchr",
- "parking",
- "pin-project-lite",
- "waker-fn",
-]
-
-[[package]]
-name = "futures-lite"
-version = "2.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
-dependencies = [
- "fastrand 2.3.0",
- "futures-core",
- "futures-io",
- "parking",
- "pin-project-lite",
-]
-
[[package]]
name = "futures-sink"
version = "0.3.31"
@@ -1297,11 +1064,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
- "js-sys",
"libc",
"r-efi",
"wasip2",
- "wasm-bindgen",
]
[[package]]
@@ -1332,7 +1097,7 @@ dependencies = [
"fnv",
"futures-core",
"futures-sink",
- "http 1.4.0",
+ "http",
"indexmap",
"slab",
"tokio",
@@ -1387,10 +1152,10 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
dependencies = [
- "base64 0.22.1",
+ "base64",
"bytes",
"headers-core",
- "http 1.4.0",
+ "http",
"httpdate",
"mime",
"sha1",
@@ -1402,7 +1167,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
- "http 1.4.0",
+ "http",
]
[[package]]
@@ -1441,30 +1206,6 @@ dependencies = [
"digest",
]
-[[package]]
-name = "hmac-sha1-compact"
-version = "1.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823"
-
-[[package]]
-name = "hmac-sha256"
-version = "1.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0f0ae375a85536cac3a243e3a9cda80a47910348abdea7e2c22f8ec556d586d"
-dependencies = [
- "digest",
-]
-
-[[package]]
-name = "hmac-sha512"
-version = "1.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5075d41b75a022af043a5bbf49b89abf17665d5aebf6f6ec64ffff207d87654d"
-dependencies = [
- "digest",
-]
-
[[package]]
name = "home"
version = "0.5.12"
@@ -1474,17 +1215,6 @@ dependencies = [
"windows-sys 0.61.2",
]
-[[package]]
-name = "http"
-version = "0.2.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
-dependencies = [
- "bytes",
- "fnv",
- "itoa",
-]
-
[[package]]
name = "http"
version = "1.4.0"
@@ -1502,7 +1232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes",
- "http 1.4.0",
+ "http",
]
[[package]]
@@ -1513,7 +1243,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
dependencies = [
"bytes",
"futures-core",
- "http 1.4.0",
+ "http",
"http-body",
"pin-project-lite",
]
@@ -1541,7 +1271,7 @@ dependencies = [
"futures-channel",
"futures-core",
"h2",
- "http 1.4.0",
+ "http",
"http-body",
"httparse",
"httpdate",
@@ -1559,7 +1289,7 @@ version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
- "http 1.4.0",
+ "http",
"hyper",
"hyper-util",
"rustls",
@@ -1591,11 +1321,11 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
- "base64 0.22.1",
+ "base64",
"bytes",
"futures-channel",
"futures-util",
- "http 1.4.0",
+ "http",
"http-body",
"hyper",
"ipnet",
@@ -1763,15 +1493,6 @@ dependencies = [
"syn 2.0.114",
]
-[[package]]
-name = "instant"
-version = "0.1.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
-dependencies = [
- "cfg-if",
-]
-
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -1794,33 +1515,6 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
-[[package]]
-name = "isahc"
-version = "1.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
-dependencies = [
- "async-channel",
- "castaway",
- "crossbeam-utils",
- "curl",
- "curl-sys",
- "encoding_rs",
- "event-listener 2.5.3",
- "futures-lite 1.13.0",
- "http 0.2.12",
- "log",
- "mime",
- "once_cell",
- "polling",
- "slab",
- "sluice",
- "tracing",
- "tracing-futures",
- "url",
- "waker-fn",
-]
-
[[package]]
name = "itoa"
version = "1.0.17"
@@ -1843,63 +1537,23 @@ version = "10.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1"
dependencies = [
- "base64 0.22.1",
+ "base64",
"ed25519-dalek",
"getrandom 0.2.17",
"hmac",
"js-sys",
"p256",
"p384",
- "pem 3.0.6",
+ "pem",
"rand",
- "rsa 0.9.10",
+ "rsa",
"serde",
"serde_json",
"sha2",
- "signature 2.2.0",
+ "signature",
"simple_asn1",
]
-[[package]]
-name = "jwt-simple"
-version = "0.11.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "357892bb32159d763abdea50733fadcb9a8e1c319a9aa77592db8555d05af83e"
-dependencies = [
- "anyhow",
- "binstring",
- "coarsetime",
- "ct-codecs",
- "ed25519-compact",
- "hmac-sha1-compact",
- "hmac-sha256",
- "hmac-sha512",
- "k256",
- "p256",
- "p384",
- "rand",
- "rsa 0.7.2",
- "serde",
- "serde_json",
- "spki 0.6.0",
- "thiserror 1.0.69",
- "zeroize",
-]
-
-[[package]]
-name = "k256"
-version = "0.13.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
-dependencies = [
- "cfg-if",
- "ecdsa",
- "elliptic-curve",
- "once_cell",
- "sha2",
- "signature 2.2.0",
-]
-
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1921,23 +1575,13 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
-[[package]]
-name = "libnghttp2-sys"
-version = "0.1.11+1.64.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b6c24e48a7167cffa7119da39d577fa482e66c688a4aac016bee862e1a713c4"
-dependencies = [
- "cc",
- "libc",
-]
-
[[package]]
name = "libredox"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
"libc",
"redox_syscall 0.7.0",
]
@@ -1952,18 +1596,6 @@ dependencies = [
"vcpkg",
]
-[[package]]
-name = "libz-sys"
-version = "1.1.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7"
-dependencies = [
- "cc",
- "libc",
- "pkg-config",
- "vcpkg",
-]
-
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -2055,7 +1687,7 @@ dependencies = [
"bytes",
"encoding_rs",
"futures-util",
- "http 1.4.0",
+ "http",
"httparse",
"memchr",
"mime",
@@ -2080,15 +1712,6 @@ dependencies = [
"tempfile",
]
-[[package]]
-name = "nu-ansi-term"
-version = "0.50.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
-dependencies = [
- "windows-sys 0.61.2",
-]
-
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -2180,7 +1803,7 @@ version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
"cfg-if",
"foreign-types",
"libc",
@@ -2304,36 +1927,16 @@ dependencies = [
"windows-link",
]
-[[package]]
-name = "pem"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb"
-dependencies = [
- "base64 0.13.1",
- "once_cell",
- "regex",
-]
-
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
- "base64 0.22.1",
+ "base64",
"serde_core",
]
-[[package]]
-name = "pem-rfc7468"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac"
-dependencies = [
- "base64ct",
-]
-
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@@ -2376,26 +1979,6 @@ dependencies = [
"siphasher",
]
-[[package]]
-name = "pin-project"
-version = "1.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
-dependencies = [
- "pin-project-internal",
-]
-
-[[package]]
-name = "pin-project-internal"
-version = "1.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.114",
-]
-
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -2408,37 +1991,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
-[[package]]
-name = "pkcs1"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719"
-dependencies = [
- "der 0.6.1",
- "pkcs8 0.9.0",
- "spki 0.6.0",
- "zeroize",
-]
-
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
- "der 0.7.10",
- "pkcs8 0.10.2",
- "spki 0.7.3",
-]
-
-[[package]]
-name = "pkcs8"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
-dependencies = [
- "der 0.6.1",
- "spki 0.6.0",
+ "der",
+ "pkcs8",
+ "spki",
]
[[package]]
@@ -2447,8 +2008,8 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
- "der 0.7.10",
- "spki 0.7.3",
+ "der",
+ "spki",
]
[[package]]
@@ -2457,22 +2018,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
-[[package]]
-name = "polling"
-version = "2.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
-dependencies = [
- "autocfg",
- "bitflags 1.3.2",
- "cfg-if",
- "concurrent-queue",
- "libc",
- "log",
- "pin-project-lite",
- "windows-sys 0.48.0",
-]
-
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -2636,7 +2181,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
]
[[package]]
@@ -2645,7 +2190,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
]
[[package]]
@@ -2692,12 +2237,12 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
- "base64 0.22.1",
+ "base64",
"bytes",
"encoding_rs",
"futures-core",
"h2",
- "http 1.4.0",
+ "http",
"http-body",
"http-body-util",
"hyper",
@@ -2779,43 +2324,22 @@ dependencies = [
"syn 1.0.109",
]
-[[package]]
-name = "rsa"
-version = "0.7.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c"
-dependencies = [
- "byteorder",
- "digest",
- "num-bigint-dig",
- "num-integer",
- "num-iter",
- "num-traits",
- "pkcs1 0.4.1",
- "pkcs8 0.9.0",
- "rand_core",
- "signature 1.6.4",
- "smallvec",
- "subtle",
- "zeroize",
-]
-
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
- "const-oid 0.9.6",
+ "const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
- "pkcs1 0.7.5",
- "pkcs8 0.10.2",
+ "pkcs1",
+ "pkcs8",
"rand_core",
- "signature 2.2.0",
- "spki 0.7.3",
+ "signature",
+ "spki",
"subtle",
"zeroize",
]
@@ -2851,7 +2375,7 @@ version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
"errno",
"libc",
"linux-raw-sys",
@@ -2955,7 +2479,7 @@ dependencies = [
"serde_json",
"sqlx",
"strum 0.26.3",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"url",
@@ -3052,7 +2576,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
- "thiserror 2.0.18",
+ "thiserror",
]
[[package]]
@@ -3091,31 +2615,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
- "der 0.7.10",
+ "der",
"generic-array",
- "pkcs8 0.10.2",
+ "pkcs8",
"subtle",
"zeroize",
]
-[[package]]
-name = "sec1_decode"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6326ddc956378a0739200b2c30892dccaf198992dfd7323274690b9e188af23"
-dependencies = [
- "der 0.4.5",
- "pem 0.8.3",
- "thiserror 1.0.69",
-]
-
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -3251,16 +2764,6 @@ dependencies = [
"libc",
]
-[[package]]
-name = "signature"
-version = "1.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
-dependencies = [
- "digest",
- "rand_core",
-]
-
[[package]]
name = "signature"
version = "2.2.0"
@@ -3285,7 +2788,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
- "thiserror 2.0.18",
+ "thiserror",
"time",
]
@@ -3301,17 +2804,6 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
-[[package]]
-name = "sluice"
-version = "0.5.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
-dependencies = [
- "async-channel",
- "futures-core",
- "futures-io",
-]
-
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -3340,16 +2832,6 @@ dependencies = [
"lock_api",
]
-[[package]]
-name = "spki"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
-dependencies = [
- "base64ct",
- "der 0.6.1",
-]
-
[[package]]
name = "spki"
version = "0.7.3"
@@ -3357,7 +2839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
- "der 0.7.10",
+ "der",
]
[[package]]
@@ -3379,14 +2861,14 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
- "base64 0.22.1",
+ "base64",
"bigdecimal",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
- "event-listener 5.4.1",
+ "event-listener",
"futures-core",
"futures-intrusive",
"futures-io",
@@ -3404,7 +2886,7 @@ dependencies = [
"serde_json",
"sha2",
"smallvec",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tokio",
"tokio-stream",
@@ -3459,9 +2941,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
- "base64 0.22.1",
+ "base64",
"bigdecimal",
- "bitflags 2.10.0",
+ "bitflags",
"byteorder",
"bytes",
"chrono",
@@ -3484,7 +2966,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"rand",
- "rsa 0.9.10",
+ "rsa",
"rust_decimal",
"serde",
"sha1",
@@ -3492,7 +2974,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"uuid",
@@ -3506,9 +2988,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
- "base64 0.22.1",
+ "base64",
"bigdecimal",
- "bitflags 2.10.0",
+ "bitflags",
"byteorder",
"chrono",
"crc",
@@ -3535,7 +3017,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"uuid",
@@ -3562,7 +3044,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"url",
@@ -3662,18 +3144,6 @@ dependencies = [
"futures-core",
]
-[[package]]
-name = "synstructure"
-version = "0.12.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
- "unicode-xid",
-]
-
[[package]]
name = "synstructure"
version = "0.13.2"
@@ -3691,7 +3161,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
"core-foundation",
"system-configuration-sys",
]
@@ -3718,40 +3188,20 @@ version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
- "fastrand 2.3.0",
+ "fastrand",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
-]
-
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "thiserror-impl 2.0.18",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.114",
+ "thiserror-impl",
]
[[package]]
@@ -3905,18 +3355,6 @@ dependencies = [
"tokio",
]
-[[package]]
-name = "tokio-tungstenite"
-version = "0.24.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
-dependencies = [
- "futures-util",
- "log",
- "tokio",
- "tungstenite",
-]
-
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -3982,9 +3420,9 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
"bytes",
- "http 1.4.0",
+ "http",
"http-body",
"http-body-util",
"pin-project-lite",
@@ -3998,10 +3436,10 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
- "bitflags 2.10.0",
+ "bitflags",
"bytes",
"futures-util",
- "http 1.4.0",
+ "http",
"http-body",
"iri-string",
"pin-project-lite",
@@ -4052,28 +3490,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-futures"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
-dependencies = [
- "pin-project",
- "tracing",
-]
-
-[[package]]
-name = "tracing-log"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
-dependencies = [
- "log",
- "once_cell",
- "tracing-core",
]
[[package]]
@@ -4083,15 +3499,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
- "nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
- "smallvec",
"thread_local",
"tracing",
"tracing-core",
- "tracing-log",
]
[[package]]
@@ -4100,24 +3513,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
-[[package]]
-name = "tungstenite"
-version = "0.24.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
-dependencies = [
- "byteorder",
- "bytes",
- "data-encoding",
- "http 1.4.0",
- "httparse",
- "log",
- "rand",
- "sha1",
- "thiserror 1.0.69",
- "utf-8",
-]
-
[[package]]
name = "typenum"
version = "1.19.0"
@@ -4175,12 +3570,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -4205,12 +3594,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "valuable"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
-
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -4223,12 +3606,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
-[[package]]
-name = "waker-fn"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
-
[[package]]
name = "want"
version = "0.3.1"
@@ -4259,15 +3636,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
-[[package]]
-name = "wasix"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1757e0d1f8456693c7e5c6c629bdb54884e032aa0bb53c155f6a39f94440d332"
-dependencies = [
- "wasi",
-]
-
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
@@ -4327,28 +3695,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "web-push"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f2332e5400bb42c21bcab3ca2cd3400ab4b1d5ecbe276b533ce9acb59c56602"
-dependencies = [
- "async-trait",
- "base64 0.13.1",
- "chrono",
- "ece",
- "futures-lite 2.6.1",
- "http 0.2.12",
- "isahc",
- "jwt-simple",
- "log",
- "pem 3.0.6",
- "sec1_decode",
- "serde",
- "serde_derive",
- "serde_json",
-]
-
[[package]]
name = "web-sys"
version = "0.3.85"
@@ -4475,15 +3821,6 @@ dependencies = [
"windows-targets 0.52.6",
]
-[[package]]
-name = "windows-sys"
-version = "0.59.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
-dependencies = [
- "windows-targets 0.52.6",
-]
-
[[package]]
name = "windows-sys"
version = "0.60.2"
@@ -4744,7 +4081,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
- "synstructure 0.13.2",
+ "synstructure",
]
[[package]]
@@ -4785,7 +4122,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
- "synstructure 0.13.2",
+ "synstructure",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 9878cd9..e995879 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,8 +12,8 @@ tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
-axum = { version = "0.7", features = ["ws"] }
-tower-http = { version = "0.5", features = ["cors", "set-header", "limit"] }
+axum = "0.7"
+tower-http = { version = "0.5", features = ["cors"] }
sea-orm = { version = "1.1", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }
sea-orm-migration = "1.1"
uuid = { version = "1.8", features = ["v4", "serde"] }
@@ -22,11 +22,4 @@ tokio-cron-scheduler = "0.15.1"
dashmap = "6.1.0"
jsonwebtoken = { version = "10.3.0", features = ["rsa", "rust_crypto"] }
base64 = "0.22.1"
-web-push = { version = "0.10.0", features = ["isahc-client"] }
-isahc = "1.7"
-axum-extra = { version = "0.9", features = ["typed-header", "cookie"] }
-cookie = "0.18"
-thiserror = "2.0.18"
-dotenvy = "0.15.7"
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+axum-extra = { version = "0.9", features = ["typed-header"] }
diff --git a/frontend/index.html b/frontend/index.html
index edcefbf..6567fa6 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,13 +3,9 @@
-
-
-
-
-
-
+
Antigravity Agent Dashboard
+
-
-
-
-
⌘
@@ -75,56 +63,22 @@
-
-
-
-
-
-
-
-
-
Establish connection with the neural assistant.
-
-
-
-
-
-
-
-
-
-
-
- | Directive |
- Status |
- Date |
-
-
-
-
-
-
-
-
+
+
+
+
+ | Directive |
+ Status |
+ Date |
+
+
+
+
+
+
@@ -144,9 +98,6 @@
-
@@ -224,7 +175,6 @@
-
diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png
deleted file mode 100644
index a5ea0e4..0000000
Binary files a/frontend/public/apple-touch-icon.png and /dev/null differ
diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png
deleted file mode 100644
index b23b3c7..0000000
Binary files a/frontend/public/icon-192.png and /dev/null differ
diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png
deleted file mode 100644
index 2905528..0000000
Binary files a/frontend/public/icon-512.png and /dev/null differ
diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json
deleted file mode 100644
index 5978a0c..0000000
--- a/frontend/public/manifest.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "name": "Antigravity Agency Dashboard",
- "short_name": "Agency",
- "description": "Autonomous AI Agent Dashboard",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#0a0a0c",
- "theme_color": "#5d5dff",
- "icons": [
- {
- "src": "icon-192.png",
- "sizes": "192x192",
- "type": "image/png"
- },
- {
- "src": "icon-512.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ]
-}
\ No newline at end of file
diff --git a/frontend/public/sw.js b/frontend/public/sw.js
deleted file mode 100644
index e976c34..0000000
--- a/frontend/public/sw.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const CACHE_NAME = 'agency-cache-v4';
-const ASSETS = [
- '/',
- '/index.html',
- '/manifest.json',
- '/icon-192.png',
- '/icon-512.png'
-];
-
-// Force immediate update to the latest SW
-self.addEventListener('install', (event) => {
- event.waitUntil(
- caches.open(CACHE_NAME).then((cache) => {
- return cache.addAll(ASSETS);
- }).then(() => self.skipWaiting())
- );
-});
-
-// Clean up old caches and take control of all clients immediately
-self.addEventListener('activate', (event) => {
- event.waitUntil(
- caches.keys().then((cacheNames) => {
- return Promise.all(
- cacheNames.map((cacheName) => {
- if (cacheName !== CACHE_NAME) {
- console.log('Deleting old cache:', cacheName);
- return caches.delete(cacheName);
- }
- })
- );
- }).then(() => self.clients.claim())
- );
-});
-
-self.addEventListener('fetch', (event) => {
- // Only intercept http/https requests
- if (!event.request.url.startsWith('http')) return;
-
- event.respondWith(
- caches.match(event.request).then((cachedResponse) => {
- if (cachedResponse) {
- return cachedResponse;
- }
-
- return fetch(event.request).catch((error) => {
- // If network fetch fails and it's a navigation request, return index.html
- if (event.request.mode === 'navigate') {
- return caches.match('/index.html');
- }
-
- // For assets, return a failure response instead of throwing.
- // Re-throwing (or returning a rejected promise) causes the browser to show
- // the "unexpected error" interception UI.
- console.warn('Fetch failed for:', event.request.url, error);
-
- return new Response('Network error occurred', {
- status: 503,
- statusText: 'Service Unavailable',
- headers: new Headers({ 'Content-Type': 'text/plain' })
- });
- });
- })
- );
-});
-
-self.addEventListener('push', (event) => {
- let data = { title: 'Notification', body: 'New update from Agency' };
- try {
- if (event.data) {
- data = event.data.json();
- }
- } catch (e) {
- console.error('Error parsing push data:', e);
- }
-
- const options = {
- body: data.body,
- icon: '/icon-192.png',
- badge: '/icon-192.png',
- vibrate: [100, 50, 100],
- data: {
- dateOfArrival: Date.now(),
- primaryKey: '1',
- taskId: data.task_id,
- runId: data.run_id
- }
- };
-
- event.waitUntil(
- self.registration.showNotification(data.title, options)
- );
-});
-
-self.addEventListener('notificationclick', (event) => {
- event.notification.close();
-
- const taskId = event.notification.data.taskId;
- const runId = event.notification.data.runId;
-
- let url = '/';
- if (taskId && runId) {
- url = `/?taskId=${taskId}&runId=${runId}`;
- }
-
- event.waitUntil(
- clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
- // Check if there is already a window open and focus it, or open a new one
- for (let client of windowClients) {
- if ('focus' in client) {
- // Navigate the existing client to the new URL if it's the same app
- return client.navigate(url).then(c => c.focus());
- }
- }
- if (clients.openWindow) {
- return clients.openWindow(url);
- }
- })
- );
-});
diff --git a/frontend/src/main.js b/frontend/src/main.js
index ffae909..3085bd6 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,6 +1,5 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
-import './style.css';
const API_URL = '/api';
// These should ideally be environment-specific
@@ -17,11 +16,10 @@ const state = {
selectedRunId: null,
currentView: 'dashboard', // 'dashboard' or 'task'
isEditing: false,
- isAuthenticated: false,
- chatMessages: [],
- activeDashboardTab: 'chat', // 'chat' or 'activity'
- swRegistration: null
+ token: localStorage.getItem('auth_token'),
+ refreshToken: localStorage.getItem('refresh_token')
};
+
// DOM elements
const loginOverlay = document.getElementById('login-overlay');
const callbackOverlay = document.getElementById('callback-overlay');
@@ -57,71 +55,32 @@ const schedulePresets = document.getElementById('schedule-presets');
const toggleCustomCronBtn = document.getElementById('toggle-custom-cron');
const customCronContainer = document.getElementById('custom-cron-container');
const presetBtns = document.querySelectorAll('.btn-preset');
-const chatMessagesEl = document.getElementById('chat-messages');
-const chatForm = document.getElementById('chat-form');
-const chatInput = document.getElementById('chat-input');
-const clearChatBtn = document.getElementById('clear-chat-btn');
-const chatSendBtn = document.getElementById('chat-send-btn');
-const sidebarEl = document.querySelector('.sidebar');
-const menuToggle = document.getElementById('menu-toggle');
-const sidebarOverlay = document.getElementById('sidebar-overlay');
-
-function updateState(newState) {
- Object.assign(state, newState);
- renderApp();
-}
-
-function showToast(message, type = 'info') {
- const container = document.getElementById('toast-container');
- const toast = document.createElement('div');
- toast.className = `toast ${type}`;
-
- const icons = {
- success: '✓',
- error: '✕',
- info: 'ℹ'
- };
-
- toast.innerHTML = `
- ${icons[type] || 'ℹ'}
- ${message}
- `;
-
- container.appendChild(toast);
-
- // Auto remove
- setTimeout(() => {
- toast.style.animation = 'fadeOut 0.3s forwards';
- setTimeout(() => toast.remove(), 300);
- }, 4000);
-}
-
-function renderApp() {
- renderTaskList();
-
- if (state.currentView === 'dashboard') {
- fetchRecentRuns();
- renderChat();
- } else if (state.selectedTaskId) {
- const task = state.tasks.find(t => t.id === state.selectedTaskId);
- if (task) {
- renderRunHistory(task);
- showTaskView(task);
- }
- }
-}
// Wrapper for fetch to include Authorization header
async function fetchWithAuth(url, options = {}) {
- let response = await fetch(url, { ...options, credentials: 'include' });
+ if (!state.token) {
+ showLogin();
+ throw new Error('Not authenticated');
+ }
- if (response.status === 401) {
+ const headers = {
+ ...options.headers,
+ 'Authorization': `Bearer ${state.token}`
+ };
+
+ let response = await fetch(url, { ...options, headers });
+
+ if (response.status === 401 && state.refreshToken) {
// Try to refresh token
try {
const success = await attemptTokenRefresh();
if (success) {
- // Retry original request
- response = await fetch(url, { ...options, credentials: 'include' });
+ // Retry original request with new token
+ const newHeaders = {
+ ...options.headers,
+ 'Authorization': `Bearer ${state.token}`
+ };
+ response = await fetch(url, { ...options, headers: newHeaders });
}
} catch (error) {
console.error('Token refresh failed:', error);
@@ -141,59 +100,65 @@ async function attemptTokenRefresh() {
const response = await fetch(`${API_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ refresh_token: '' })
+ body: JSON.stringify({ refresh_token: state.refreshToken })
});
if (response.ok) {
- return true;
+ const data = await response.json();
+ if (data.access_token) {
+ state.token = data.access_token;
+ localStorage.setItem('auth_token', data.access_token);
+ if (data.refresh_token) {
+ state.refreshToken = data.refresh_token;
+ localStorage.setItem('refresh_token', data.refresh_token);
+ }
+ return true;
+ }
}
} catch (error) {
console.error('Error during token refresh:', error);
}
return false;
}
-// I'll replace the fetchTasks function and add updateState
+
async function fetchTasks() {
try {
const response = await fetchWithAuth(`${API_URL}/tasks`);
const newTasks = await response.json();
- // Check for deep link in URL
- const params = new URLSearchParams(window.location.search);
- const urlTaskId = params.get('taskId');
- const urlRunId = params.get('runId');
-
- if (urlTaskId && !state.selectedTaskId) {
- state.selectedTaskId = urlTaskId;
- state.selectedRunId = urlRunId;
- }
-
- // Check if we should follow the latest run
- let newSelectedRunId = state.selectedRunId;
+ // Check if we should follow the latest run (if we were already watching it)
+ let shouldFollowLatest = false;
if (state.selectedTaskId) {
- const currentTask = newTasks.find(t => t.id === state.selectedTaskId);
+ const currentTask = state.tasks.find(t => t.id === state.selectedTaskId);
if (currentTask && currentTask.runs && currentTask.runs.length > 0) {
- // If we don't have a selected run or the runs changed, we might want to update
- if (!state.selectedRunId || (state.tasks.find(t => t.id === state.selectedTaskId)?.runs?.length !== currentTask.runs.length)) {
- // Only auto-switch if we are "following" the latest
- const wasFollowingLatest = state.tasks.find(t => t.id === state.selectedTaskId)?.runs?.[0]?.id === state.selectedRunId;
- if (wasFollowingLatest || !state.selectedRunId) {
- newSelectedRunId = currentTask.runs[0].id;
- }
+ const latestRunId = currentTask.runs[currentTask.runs.length - 1].id;
+ if (state.selectedRunId === latestRunId) {
+ shouldFollowLatest = true;
}
+ } else if (!state.selectedRunId) {
+ shouldFollowLatest = true;
}
}
- updateState({
- tasks: newTasks,
- selectedRunId: newSelectedRunId
- });
+ state.tasks = newTasks;
+ renderTaskList();
- // If we just loaded from a deep link, clear the params and select it
- if (urlTaskId) {
- window.history.replaceState({}, document.title, "/");
- selectTask(urlTaskId, urlRunId);
+ // If we are on the dashboard, refresh it too
+ if (state.currentView === 'dashboard') {
+ fetchRecentRuns();
+ }
+
+ // If a task is selected, update it
+ if (state.selectedTaskId) {
+ const task = state.tasks.find((t) => t.id === state.selectedTaskId);
+ if (task) {
+ if (shouldFollowLatest && task.runs && task.runs.length > 0) {
+ state.selectedRunId = task.runs[task.runs.length - 1].id;
+ }
+
+ renderRunHistory(task);
+ showTaskView(task);
+ }
}
} catch (error) {
console.error('Error fetching tasks:', error);
@@ -212,7 +177,13 @@ async function fetchRecentRuns() {
function renderTaskList() {
const sortedTasks = [...state.tasks].sort((a, b) => {
- return new Date(b.created_at) - new Date(a.created_at);
+ const aDate = a.runs && a.runs.length > 0
+ ? new Date(a.runs[0].created_at)
+ : new Date(a.created_at);
+ const bDate = b.runs && b.runs.length > 0
+ ? new Date(b.runs[0].created_at)
+ : new Date(b.created_at);
+ return bDate - aDate;
});
taskListEl.innerHTML = sortedTasks
@@ -252,8 +223,8 @@ function selectTask(id, runId = null) {
if (runId) {
state.selectedRunId = runId;
} else if (task.runs && task.runs.length > 0) {
- // Default to latest run if not specified (index 0 is newest)
- state.selectedRunId = task.runs[0].id;
+ // Default to latest run if not specified
+ state.selectedRunId = task.runs[task.runs.length - 1].id;
} else {
state.selectedRunId = null;
}
@@ -261,24 +232,6 @@ function selectTask(id, runId = null) {
renderTaskList();
renderRunHistory(task);
showTaskView(task);
-
- // Fetch and update subscription status
- fetch(`${API_URL}/tasks/${id}/subscription`)
- .then(res => res.json())
- .then(data => {
- const btn = document.getElementById('notify-task-btn');
- if (data.isSubscribed) {
- btn.classList.add('notified');
- } else {
- btn.classList.remove('notified');
- }
- })
- .catch(err => console.error('Failed to fetch subscription status', err));
-
- // Close sidebar on mobile after selection
- if (window.innerWidth <= 768) {
- closeMobileMenu();
- }
}
function renderRunHistory(task) {
@@ -311,28 +264,6 @@ function showDashboard() {
renderTaskList();
fetchRecentRuns();
- renderDashboardTabs();
-}
-
-function renderDashboardTabs() {
- const tabs = document.querySelectorAll('.tab-btn');
- const panels = document.querySelectorAll('.tab-panel');
-
- tabs.forEach(tab => {
- if (tab.dataset.tab === state.activeDashboardTab) {
- tab.classList.add('active');
- } else {
- tab.classList.remove('active');
- }
- });
-
- panels.forEach(panel => {
- if (panel.id === `${state.activeDashboardTab}-tab-panel`) {
- panel.classList.add('active');
- } else {
- panel.classList.remove('active');
- }
- });
}
function renderDashboard(recentRuns) {
@@ -423,62 +354,6 @@ function escapeHtml(text) {
return div.innerHTML;
}
-function renderChat() {
- if (!chatMessagesEl) return;
-
- if (state.chatMessages.length === 0) {
- chatMessagesEl.innerHTML = `
-
-
Start a conversation with the assistant.
-
- `;
- return;
- }
-
- chatMessagesEl.innerHTML = state.chatMessages
- .map(msg => `
-
- ${DOMPurify.sanitize(marked.parse(msg.content || ''))}
-
- `)
- .join('');
-
- chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight;
-}
-
-async function sendChatMessage(text) {
- const userMessage = { role: 'user', content: text };
- state.chatMessages.push(userMessage);
- renderChat();
-
- chatInput.value = '';
- chatInput.disabled = true;
- chatSendBtn.disabled = true;
-
- try {
- const response = await fetchWithAuth(`${API_URL}/chat`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ messages: state.chatMessages })
- });
-
- if (!response.ok) throw new Error('Chat API failed');
-
- const result = await response.json();
- state.chatMessages.push(result.message);
- renderChat();
- } catch (error) {
- console.error('Chat error:', error);
- showToast('Failed to get chat response.', 'error');
- state.chatMessages.push({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' });
- renderChat();
- } finally {
- chatInput.disabled = false;
- chatSendBtn.disabled = false;
- chatInput.focus();
- }
-}
-
// Event Listeners
rerunBtn.addEventListener('click', async () => {
if (!state.selectedTaskId) return;
@@ -493,10 +368,9 @@ rerunBtn.addEventListener('click', async () => {
state.tasks[index] = updatedTask;
}
selectTask(updatedTask.id);
- showToast('Task rerun successfully!', 'success');
} catch (error) {
- console.error('Failed to rerun task:', error);
- showToast('Failed to rerun task.', 'error');
+ console.error('Error running task:', error);
+ alert('Failed to run task.');
}
});
@@ -572,19 +446,6 @@ toggleCustomCronBtn.addEventListener('click', () => {
customCronContainer.classList.toggle('hidden');
});
-chatForm.addEventListener('submit', (e) => {
- e.preventDefault();
- const text = chatInput.value.trim();
- if (text) {
- sendChatMessage(text);
- }
-});
-
-clearChatBtn.addEventListener('click', () => {
- state.chatMessages = [];
- renderChat();
-});
-
cronInput.addEventListener('input', () => {
// If user types manually, update presets active state
updateScheduleUI(cronInput.value);
@@ -628,107 +489,23 @@ newTaskForm.addEventListener('submit', async (e) => {
state.isEditing = false;
selectTask(updatedTask.id);
renderTaskList();
- showToast(state.isEditing ? 'Task updated successfully' : 'Task created successfully', 'success');
} catch (error) {
- console.error('Save task failed:', error);
- showToast('Failed to execute task. Check console.', 'error');
+ console.error('Error creating task:', error);
+ alert('Failed to execute task. Check console.');
}
});
-async function checkSession() {
- try {
- const response = await fetch(`${API_URL}/auth/session`, { credentials: 'include' });
- if (response.ok) {
- state.isAuthenticated = true;
- return true;
- }
- } catch (error) {
- console.error('Session check failed:', error);
- }
- state.isAuthenticated = false;
- return false;
-}
-
-let socket = null;
-let reconnectDelay = 1000;
-
-function connectWebSocket() {
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const wsUrl = `${protocol}//${window.location.host}/api/ws`;
-
- console.log('Connecting to WebSocket:', wsUrl);
- socket = new WebSocket(wsUrl);
-
- socket.onopen = () => {
- console.log('WebSocket connected');
- reconnectDelay = 1000;
- // Initial fetch to sync state
- fetchTasks();
- };
-
- socket.onmessage = (event) => {
+let isPolling = false;
+async function startAutoRefresh() {
+ setInterval(async () => {
+ if (isPolling) return;
+ isPolling = true;
try {
- const { type, data } = JSON.parse(event.data);
- console.log('WebSocket event:', type, data);
-
- switch (type) {
- case 'TaskCreated':
- state.tasks.unshift(data);
- renderApp();
- showToast('New task created', 'success');
- break;
- case 'TaskUpdated':
- case 'RunFinished':
- const index = state.tasks.findIndex(t => t.id === data.id);
- if (index !== -1) {
- const wasSelected = state.selectedTaskId === data.id;
- state.tasks[index] = data;
- if (wasSelected) {
- // Update selected run if we were following latest
- const wasFollowingLatest = state.selectedRunId === state.tasks[index].runs?.[1]?.id || !state.selectedRunId;
- if (wasFollowingLatest && data.runs && data.runs.length > 0) {
- state.selectedRunId = data.runs[0].id;
- }
- }
- } else {
- state.tasks.unshift(data);
- }
- renderApp();
- if (type === 'RunFinished') {
- showToast(`Task run completed: ${data.goal}`, 'info');
- }
- break;
- case 'RunStarted':
- const taskIndex = state.tasks.findIndex(t => t.id === data.task_id);
- if (taskIndex !== -1) {
- // We don't have the full task update here, but we can update status
- // For simplicity, we just trigger a fetch or wait for RunFinished
- // But let's at least show it's running in the UI if selected
- if (state.tasks[taskIndex].runs) {
- // Prepend a dummy run or just fetch
- fetchTasks();
- }
- }
- showToast(`Task started: ${data.goal}`, 'info');
- break;
- }
- } catch (e) {
- console.error('Error handling WebSocket message:', e);
+ await fetchTasks();
+ } finally {
+ isPolling = false;
}
- };
-
- socket.onclose = () => {
- console.log('WebSocket disconnected. Reconnecting...');
- setTimeout(() => {
- reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
- connectWebSocket();
- }, reconnectDelay);
- };
-
- socket.onerror = (error) => {
- console.error('WebSocket error:', error);
- socket.close();
- };
+ }, 3000);
}
async function showLogin() {
@@ -737,12 +514,10 @@ async function showLogin() {
}
async function logout() {
- try {
- await fetch(`${API_URL}/auth/logout`, { method: 'POST', credentials: 'include' });
- } catch (error) {
- console.error('Logout failed:', error);
- }
- state.isAuthenticated = false;
+ state.token = null;
+ state.refreshToken = null;
+ localStorage.removeItem('auth_token');
+ localStorage.removeItem('refresh_token');
showLogin();
}
@@ -756,20 +531,16 @@ async function handleCallback() {
loginOverlay.classList.add('hidden');
try {
- const response = await fetch(`${API_URL}/auth/callback?code=${code}&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}`, {
- credentials: 'include'
- });
- const contentType = response.headers.get("content-type");
- let data;
- if (contentType && contentType.includes("application/json")) {
- data = await response.json();
- } else {
- const text = await response.text();
- throw new Error(`Expected JSON but got ${contentType}. Body: ${text.substring(0, 100)}`);
- }
+ const response = await fetch(`${API_URL}/auth/callback?code=${code}&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}`);
+ const data = await response.json();
- if (response.ok) {
- state.isAuthenticated = true;
+ if (data.access_token) {
+ state.token = data.access_token;
+ localStorage.setItem('auth_token', data.access_token);
+ if (data.refresh_token) {
+ state.refreshToken = data.refresh_token;
+ localStorage.setItem('refresh_token', data.refresh_token);
+ }
callbackOverlay.classList.add('hidden');
appEl.classList.remove('hidden');
initializeApp();
@@ -777,14 +548,14 @@ async function handleCallback() {
throw new Error('No access token in response');
}
} catch (error) {
- console.error('Callback failed:', error);
- showToast('Authentication failed.', 'error');
+ console.error('Auth callback failed:', error);
+ alert('Authentication failed.');
showLogin();
}
}
loginBtn.addEventListener('click', () => {
- const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?client_id=${AUTH_CONFIG.clientId}&response_type=code&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}&scope=openid profile email offline_access`;
+ const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?client_id=${AUTH_CONFIG.clientId}&response_type=code&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}&scope=openid profile email`;
window.location.href = authUrl;
});
@@ -792,51 +563,17 @@ logoutBtn.addEventListener('click', () => {
logout();
});
-document.querySelectorAll('.tab-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- state.activeDashboardTab = btn.dataset.tab;
- renderDashboardTabs();
- });
-});
-
-function toggleMobileMenu() {
- sidebarEl.classList.toggle('open');
- menuToggle.classList.toggle('active');
- sidebarOverlay.classList.toggle('hidden');
- document.body.style.overflow = sidebarEl.classList.contains('open') ? 'hidden' : '';
-}
-
-function closeMobileMenu() {
- sidebarEl.classList.remove('open');
- menuToggle.classList.remove('active');
- sidebarOverlay.classList.add('hidden');
- document.body.style.overflow = '';
-}
-
-menuToggle.addEventListener('click', toggleMobileMenu);
-sidebarOverlay.addEventListener('click', closeMobileMenu);
-
async function initializeApp() {
- const hasSession = await checkSession();
-
- if (hasSession) {
- appEl.classList.remove('hidden');
- loginOverlay.classList.add('hidden');
-
- // Handle deep links from notifications
- const params = new URLSearchParams(window.location.search);
- const taskId = params.get('taskId');
- const runId = params.get('runId');
- if (taskId) {
- state.selectedTaskId = taskId;
- state.selectedRunId = runId;
- }
-
- await fetchTasks();
- connectWebSocket();
- } else {
+ if (!state.token) {
showLogin();
+ return;
}
+
+ appEl.classList.remove('hidden');
+ loginOverlay.classList.add('hidden');
+
+ await fetchTasks();
+ startAutoRefresh();
}
// Check for callback on load
@@ -845,116 +582,3 @@ if (window.location.pathname === '/callback' || window.location.search.includes(
} else {
initializeApp();
}
-
-// Register Service Worker for PWA
-if ('serviceWorker' in navigator) {
- window.addEventListener('load', () => {
- navigator.serviceWorker.register('/sw.js')
- .then(reg => {
- console.log('SW registered', reg);
- state.swRegistration = reg;
- })
- .catch(err => {
- console.error('SW registration failed:', err);
- if (window.isSecureContext === false) {
- console.error('Context is NOT secure. Service Workers require HTTPS or localhost.');
- }
- });
- });
-}
-
-async function setupPush() {
- if (!state.swRegistration) {
- console.warn('SW registration not available');
- return false;
- }
-
- try {
- const vapidResponse = await fetch(`${API_URL}/notifications/vapid-key`);
- const { publicKey } = await vapidResponse.json();
-
- // Always clear existing subscription to ensure we use latest VAPID key
- const existingSub = await state.swRegistration.pushManager.getSubscription();
- if (existingSub) {
- await existingSub.unsubscribe();
- console.log('Unsubscribed existing push subscription');
- }
-
- const subscription = await state.swRegistration.pushManager.subscribe({
- userVisibleOnly: true,
- applicationServerKey: urlBase64ToUint8Array(publicKey)
- });
-
- await fetch(`${API_URL}/notifications/register`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- endpoint: subscription.endpoint,
- p256dh: b64(subscription.getKey('p256dh')),
- auth: b64(subscription.getKey('auth'))
- })
- });
- console.log('Push registered');
- return true;
- } catch (err) {
- console.warn('Push registration failed:', err);
- return false;
- }
-}
-
-function b64(buffer) {
- const binary = String.fromCharCode.apply(null, new Uint8Array(buffer));
- return btoa(binary)
- .replace(/\+/g, '-')
- .replace(/\//g, '_')
- .replace(/=/g, '');
-}
-
-function urlBase64ToUint8Array(base64String) {
- const padding = '='.repeat((4 - base64String.length % 4) % 4);
- const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
- const rawData = window.atob(base64);
- const outputArray = new Uint8Array(rawData.length);
- for (let i = 0; i < rawData.length; ++i) {
- outputArray[i] = rawData.charCodeAt(i);
- }
- return outputArray;
-}
-
-async function toggleTaskSubscription(taskId) {
- const btn = document.getElementById('notify-task-btn');
- const isNotified = btn.classList.contains('notified');
- const method = isNotified ? 'DELETE' : 'POST';
-
- // If trying to enable but no push subscription, try setting it up first (user gesture here)
- if (!isNotified && 'Notification' in window) {
- if (Notification.permission !== 'granted') {
- const permission = await Notification.requestPermission();
- if (permission !== 'granted') {
- showToast('Notification permission denied', 'error');
- return;
- }
- }
-
- const sub = await state.swRegistration.pushManager.getSubscription();
- if (!sub) {
- const success = await setupPush();
- if (!success) {
- showToast('Failed to initialize push notifications', 'error');
- return;
- }
- }
- }
-
- try {
- await fetch(`${API_URL}/tasks/${taskId}/subscribe`, { method });
- btn.classList.toggle('notified');
- showToast(isNotified ? 'Notifications disabled' : 'Notifications enabled');
- } catch (err) {
- showToast('Failed to update notifications');
- }
-}
-
-document.getElementById('notify-task-btn').addEventListener('click', () => {
- if (state.selectedTaskId) toggleTaskSubscription(state.selectedTaskId);
-});
diff --git a/frontend/src/style.css b/frontend/src/style.css
index c2c08b6..742bc21 100644
--- a/frontend/src/style.css
+++ b/frontend/src/style.css
@@ -25,7 +25,6 @@ body {
color: var(--text-main);
line-height: 1.5;
height: 100vh;
- height: 100dvh;
overflow: hidden;
}
@@ -89,15 +88,9 @@ body {
.dashboard-content {
flex: 1;
overflow-y: auto;
- border-radius: 16px;
+ border-radius: 12px;
display: flex;
flex-direction: column;
- width: 100%;
- max-width: 900px;
- background: rgba(255, 255, 255, 0.02);
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
- border: 1px solid var(--glass-border);
- min-height: 0;
}
.activity-table {
@@ -131,158 +124,6 @@ body {
background: rgba(255, 255, 255, 0.03);
}
-.dashboard-content-grid {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 32px;
- flex: 1;
- min-height: 0;
- width: 100%;
-}
-
-.dashboard-tabs {
- display: flex;
- gap: 12px;
- margin-top: 24px;
-}
-
-.tab-btn {
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid var(--glass-border);
- color: var(--text-dim);
- padding: 10px 20px;
- border-radius: 100px;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: var(--transition);
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.tab-btn:hover {
- background: rgba(255, 255, 255, 0.08);
- color: var(--text-main);
-}
-
-.tab-btn.active {
- background: var(--primary);
- color: white;
- border-color: var(--primary);
- box-shadow: 0 4px 15px var(--primary-glow);
-}
-
-.tab-panel {
- display: none;
- width: 100%;
- flex-direction: column;
- align-items: center;
- flex: 1;
- min-height: 0;
-}
-
-.tab-panel.active {
- display: flex;
-}
-
-.chat-container {
- display: flex;
- flex-direction: column;
- border-radius: 16px;
- overflow: hidden;
- background: rgba(255, 255, 255, 0.02);
- width: 100%;
- max-width: 900px;
- flex: 1;
- min-height: 0;
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
- border: 1px solid var(--glass-border);
-}
-
-.chat-header {
- padding: 16px 20px;
- background: rgba(255, 255, 255, 0.03);
- border-bottom: 1px solid var(--glass-border);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.chat-header h3 {
- font-size: 14px;
- font-weight: 600;
- color: var(--text-dim);
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.chat-messages {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.chat-empty-state {
- margin: auto;
- text-align: center;
- color: var(--text-dim);
- font-style: italic;
- font-size: 13px;
- opacity: 0.6;
-}
-
-.chat-message {
- max-width: 80%;
- padding: 12px 18px;
- border-radius: 16px;
- font-size: 15px;
- line-height: 1.5;
- word-wrap: break-word;
-}
-
-.chat-message.user {
- align-self: flex-end;
- background: var(--primary);
- color: white;
- border-bottom-right-radius: 4px;
-}
-
-.chat-message.assistant {
- align-self: flex-start;
- background: rgba(255, 255, 255, 0.08);
- color: var(--text-main);
- border-bottom-left-radius: 4px;
-}
-
-.chat-form {
- padding: 16px;
- background: rgba(255, 255, 255, 0.02);
- border-top: 1px solid var(--glass-border);
- display: flex;
- gap: 10px;
-}
-
-.chat-form input {
- flex: 1;
- background: rgba(0, 0, 0, 0.2);
- border: 1px solid var(--glass-border);
- border-radius: 12px;
- padding: 12px 20px;
- color: var(--text-main);
- font-size: 15px;
- outline: none;
- transition: var(--transition);
-}
-
-.chat-form input:focus {
- border-color: var(--primary);
-}
-
.activity-table .status-badge {
display: inline-block;
}
@@ -955,79 +796,8 @@ textarea:focus {
}
.btn-sm {
- padding: 6px 16px;
+ padding: 6px 12px;
font-size: 12px;
- width: auto;
- justify-content: center;
-}
-
-/* Toast System */
-#toast-container {
- position: fixed;
- bottom: 24px;
- right: 24px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- z-index: 2000;
-}
-
-.toast {
- min-width: 300px;
- padding: 16px 20px;
- border-radius: 12px;
- background: var(--bg-sidebar);
- border: 1px solid var(--glass-border);
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- gap: 12px;
- animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.toast.error {
- border-left: 4px solid var(--status-failed);
-}
-
-.toast.success {
- border-left: 4px solid var(--status-completed);
-}
-
-.toast.info {
- border-left: 4px solid var(--primary);
-}
-
-.toast-icon {
- font-size: 18px;
-}
-
-.toast-message {
- font-size: 14px;
- font-weight: 500;
-}
-
-@keyframes slideIn {
- from {
- transform: translateX(100%);
- opacity: 0;
- }
-
- to {
- transform: translateX(0);
- opacity: 1;
- }
-}
-
-@keyframes fadeOut {
- from {
- opacity: 1;
- transform: scale(1);
- }
-
- to {
- opacity: 0;
- transform: scale(0.95);
- }
}
.logout-btn {
@@ -1044,242 +814,4 @@ textarea:focus {
.logout-btn:hover {
color: var(--primary);
-}
-
-/* Mobile Menu Toggle */
-.menu-toggle {
- display: none;
- position: fixed;
- top: calc(16px + env(safe-area-inset-top));
- right: calc(16px + env(safe-area-inset-right));
- z-index: 1100;
- background: var(--primary);
- border: none;
- width: 44px;
- height: 44px;
- border-radius: 10px;
- cursor: pointer;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- gap: 5px;
- box-shadow: 0 4px 15px var(--primary-glow);
- transition: var(--transition);
-}
-
-.menu-toggle .bar {
- display: block;
- width: 20px;
- height: 2px;
- background: white;
- border-radius: 2px;
- transition: var(--transition);
-}
-
-.menu-toggle.active .bar:nth-child(1) {
- transform: translateY(7px) rotate(45deg);
-}
-
-.menu-toggle.active .bar:nth-child(2) {
- opacity: 0;
-}
-
-.menu-toggle.active .bar:nth-child(3) {
- transform: translateY(-7px) rotate(-45deg);
-}
-
-.sidebar-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- backdrop-filter: blur(4px);
- z-index: 1000;
- transition: opacity 0.3s ease;
-}
-
-/* Responsive Styles */
-#notify-task-btn.notified {
- color: var(--primary);
- background: var(--primary-glow);
-}
-
-#notify-task-btn.notified .notify-icon {
- animation: ring 0.5s ease;
-}
-
-@keyframes ring {
- 0% {
- transform: rotate(0);
- }
-
- 25% {
- transform: rotate(15deg);
- }
-
- 50% {
- transform: rotate(-15deg);
- }
-
- 75% {
- transform: rotate(10deg);
- }
-
- 100% {
- transform: rotate(0);
- }
-}
-
-@media (max-width: 768px) {
- .menu-toggle {
- display: flex;
- }
-
- .sidebar {
- position: fixed;
- left: -320px;
- top: 0;
- height: 100%;
- z-index: 1050;
- transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 10px 0 30px rgba(0, 0, 0, 0.5);
- padding-top: env(safe-area-inset-top);
- padding-bottom: env(safe-area-inset-bottom);
- }
-
- .sidebar.open {
- left: 0;
- }
-
- .dashboard-view,
- .task-view {
- padding: 20px;
- padding-top: calc(80px + env(safe-area-inset-top));
- padding-left: max(20px, env(safe-area-inset-left));
- padding-right: max(20px, env(safe-area-inset-right));
- padding-bottom: max(20px, env(safe-area-inset-bottom));
- height: auto;
- min-height: 100%;
- overflow-y: auto;
- overflow-x: hidden;
- width: 100%;
- max-width: 100vw;
- }
-
- .dashboard-header h2 {
- font-size: 24px;
- }
-
- .task-content {
- flex-direction: column;
- height: auto;
- overflow: visible;
- width: 100%;
- gap: 16px;
- }
-
- .run-history {
- width: 100%;
- max-height: 200px;
- flex-shrink: 0;
- margin-bottom: 8px;
- }
-
- .run-details {
- flex-direction: column;
- height: auto;
- overflow: visible;
- gap: 16px;
- width: 100%;
- }
-
- .answer-container {
- width: 100%;
- margin-bottom: 8px;
- border-left: none;
- border-top: 4px solid var(--primary);
- }
-
- .logs-container {
- width: 100%;
- height: 400px;
- flex-shrink: 0;
- }
-
- .view-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 16px;
- }
-
- .header-main {
- flex-direction: column;
- align-items: flex-start;
- gap: 12px;
- width: 100%;
- }
-
- .header-main h2 {
- font-size: 20px;
- line-height: 1.3;
- }
-
- .header-actions {
- width: 100%;
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- }
-
- .header-actions .btn {
- flex: 1 1 auto;
- justify-content: center;
- }
-
- .modal {
- width: 95%;
- padding: 20px;
- max-height: 90%;
- overflow-y: auto;
- }
-
- .preset-group {
- grid-template-columns: 1fr;
- }
-
- .chat-container {
- height: 500px;
- }
-
- .chat-form input,
- textarea {
- font-size: 16px !important;
- }
-
- /* Force word break for long text in containers */
- .answer-output,
- .activity-table td {
- word-break: break-word;
- }
-
- .activity-table td {
- padding: 12px 10px;
- }
-
- /* Hide Date on very small mobile to prevent table overflow */
- @media (max-width: 480px) {
-
- .activity-table th:last-child,
- .activity-table td:last-child {
- display: none;
- }
- }
-
- /* Prevent horizontal scroll on the entire app */
- #app {
- width: 100vw;
- overflow-x: hidden;
- }
}
\ No newline at end of file
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index a06ef36..7ce721b 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -4,9 +4,8 @@ export default defineConfig({
server: {
proxy: {
'/api': {
- target: 'http://localhost:3001',
+ target: 'http://localhost:3000',
changeOrigin: true,
- ws: true,
}
}
}
diff --git a/migration/src/lib.rs b/migration/src/lib.rs
index cad08e0..e206140 100644
--- a/migration/src/lib.rs
+++ b/migration/src/lib.rs
@@ -4,7 +4,6 @@ mod m20220101_000001_create_table;
mod m20260210_000002_add_answer_column;
mod m20260210_000003_separate_runs;
mod m20260210_000004_add_cron_column;
-mod m20260212_000005_notifications;
pub struct Migrator;
@@ -16,7 +15,6 @@ impl MigratorTrait for Migrator {
Box::new(m20260210_000002_add_answer_column::Migration),
Box::new(m20260210_000003_separate_runs::Migration),
Box::new(m20260210_000004_add_cron_column::Migration),
- Box::new(m20260212_000005_notifications::Migration),
]
}
}
diff --git a/migration/src/m20260212_000005_notifications.rs b/migration/src/m20260212_000005_notifications.rs
deleted file mode 100644
index fdfdaac..0000000
--- a/migration/src/m20260212_000005_notifications.rs
+++ /dev/null
@@ -1,115 +0,0 @@
-use sea_orm_migration::prelude::*;
-
-#[derive(DeriveMigrationName)]
-pub struct Migration;
-
-#[async_trait::async_trait]
-impl MigrationTrait for Migration {
- async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
- // Push Subscriptions table
- manager
- .create_table(
- Table::create()
- .table(PushSubscriptions::Table)
- .if_not_exists()
- .col(
- ColumnDef::new(PushSubscriptions::Id)
- .uuid()
- .not_null()
- .primary_key(),
- )
- .col(
- ColumnDef::new(PushSubscriptions::UserSub)
- .string()
- .not_null(),
- )
- .col(
- ColumnDef::new(PushSubscriptions::Endpoint)
- .string()
- .not_null(),
- )
- .col(
- ColumnDef::new(PushSubscriptions::P256dh)
- .string()
- .not_null(),
- )
- .col(ColumnDef::new(PushSubscriptions::Auth).string().not_null())
- .col(
- ColumnDef::new(PushSubscriptions::CreatedAt)
- .timestamp_with_time_zone()
- .not_null(),
- )
- .to_owned(),
- )
- .await?;
-
- // Task Subscriptions table
- manager
- .create_table(
- Table::create()
- .table(TaskSubscriptions::Table)
- .if_not_exists()
- .col(
- ColumnDef::new(TaskSubscriptions::Id)
- .uuid()
- .not_null()
- .primary_key(),
- )
- .col(
- ColumnDef::new(TaskSubscriptions::UserSub)
- .string()
- .not_null(),
- )
- .col(ColumnDef::new(TaskSubscriptions::TaskId).uuid().not_null())
- .col(
- ColumnDef::new(TaskSubscriptions::CreatedAt)
- .timestamp_with_time_zone()
- .not_null(),
- )
- .foreign_key(
- ForeignKey::create()
- .name("fk-task-subscription-task-id")
- .from(TaskSubscriptions::Table, TaskSubscriptions::TaskId)
- .to(Tasks::Table, Tasks::Id)
- .on_delete(ForeignKeyAction::Cascade),
- )
- .to_owned(),
- )
- .await
- }
-
- async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
- manager
- .drop_table(Table::drop().table(TaskSubscriptions::Table).to_owned())
- .await?;
- manager
- .drop_table(Table::drop().table(PushSubscriptions::Table).to_owned())
- .await
- }
-}
-
-#[derive(DeriveIden)]
-enum PushSubscriptions {
- Table,
- Id,
- UserSub,
- Endpoint,
- P256dh,
- Auth,
- CreatedAt,
-}
-
-#[derive(DeriveIden)]
-enum TaskSubscriptions {
- Table,
- Id,
- UserSub,
- TaskId,
- CreatedAt,
-}
-
-#[derive(DeriveIden)]
-enum Tasks {
- Table,
- Id,
-}
diff --git a/openapi/calendar/openapi.json b/openapi/calendar/openapi.json
deleted file mode 100644
index 8c8908c..0000000
--- a/openapi/calendar/openapi.json
+++ /dev/null
@@ -1,593 +0,0 @@
-{
- "openapi": "3.1.0",
- "info": {
- "title": "calendar",
- "description": "",
- "license": {
- "name": ""
- },
- "version": "0.1.0"
- },
- "paths": {
- "/auth/me": {
- "get": {
- "tags": [
- "crate::handlers::auth"
- ],
- "operationId": "me",
- "responses": {
- "200": {
- "description": "Current user profile",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CurrentUser"
- }
- }
- }
- },
- "401": {
- "description": "Unauthorized"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- }
- },
- "/events": {
- "get": {
- "tags": [
- "crate::handlers::event"
- ],
- "operationId": "list_events",
- "parameters": [
- {
- "name": "upcoming",
- "in": "query",
- "required": false,
- "schema": {
- "type": [
- "boolean",
- "null"
- ]
- }
- }
- ],
- "responses": {
- "200": {
- "description": "List of events",
- "content": {
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- }
- },
- "401": {
- "description": "Unauthorized"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- },
- "post": {
- "tags": [
- "crate::handlers::event"
- ],
- "operationId": "create_event",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CreateEventRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Event created successfully",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- },
- "400": {
- "description": "Invalid request payload"
- },
- "401": {
- "description": "Unauthorized"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- }
- },
- "/events/{id}": {
- "get": {
- "tags": [
- "crate::handlers::event"
- ],
- "operationId": "get_event",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "Event database id",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int32"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Event details",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- },
- "401": {
- "description": "Unauthorized"
- },
- "404": {
- "description": "Event not found"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- },
- "put": {
- "tags": [
- "crate::handlers::event"
- ],
- "operationId": "update_event",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "Event database id",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int32"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CreateEventRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Event updated successfully",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- },
- "400": {
- "description": "Invalid request payload"
- },
- "401": {
- "description": "Unauthorized"
- },
- "404": {
- "description": "Event not found"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- },
- "delete": {
- "tags": [
- "crate::handlers::event"
- ],
- "operationId": "delete_event",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "Event database id",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int32"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Event deleted successfully"
- },
- "401": {
- "description": "Unauthorized"
- },
- "404": {
- "description": "Event not found"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- }
- },
- "/service/v1/events": {
- "get": {
- "tags": [
- "crate::handlers::service"
- ],
- "operationId": "service_list_events",
- "parameters": [
- {
- "name": "user_id",
- "in": "query",
- "required": false,
- "schema": {
- "type": [
- "integer",
- "null"
- ],
- "format": "int32"
- }
- },
- {
- "name": "upcoming",
- "in": "query",
- "required": false,
- "schema": {
- "type": [
- "boolean",
- "null"
- ]
- }
- }
- ],
- "responses": {
- "200": {
- "description": "List of events",
- "content": {
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- }
- },
- "401": {
- "description": "Unauthorized"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- },
- "post": {
- "tags": [
- "crate::handlers::service"
- ],
- "operationId": "service_create_event",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ServiceCreateEventRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Event created successfully",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- },
- "400": {
- "description": "Invalid request payload"
- },
- "401": {
- "description": "Unauthorized"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- }
- },
- "/service/v1/events/{id}": {
- "get": {
- "tags": [
- "crate::handlers::service"
- ],
- "operationId": "service_get_event",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "Event database id",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int32"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "Event details",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- },
- "401": {
- "description": "Unauthorized"
- },
- "404": {
- "description": "Event not found"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- },
- "put": {
- "tags": [
- "crate::handlers::service"
- ],
- "operationId": "service_update_event",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "Event database id",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int32"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ServiceCreateEventRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "Event updated successfully",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Model"
- }
- }
- }
- },
- "400": {
- "description": "Invalid request payload"
- },
- "401": {
- "description": "Unauthorized"
- },
- "404": {
- "description": "Event not found"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- },
- "delete": {
- "tags": [
- "crate::handlers::service"
- ],
- "operationId": "service_delete_event",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "Event database id",
- "required": true,
- "schema": {
- "type": "integer",
- "format": "int32"
- }
- }
- ],
- "responses": {
- "204": {
- "description": "Event deleted successfully"
- },
- "401": {
- "description": "Unauthorized"
- },
- "404": {
- "description": "Event not found"
- }
- },
- "security": [
- {
- "oidc": []
- }
- ]
- }
- }
- },
- "components": {
- "schemas": {
- "CreateEventRequest": {
- "type": "object",
- "required": [
- "name",
- "from",
- "to"
- ],
- "properties": {
- "from": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "to": {
- "type": "string"
- }
- }
- },
- "CurrentUser": {
- "type": "object",
- "required": [
- "id",
- "sub",
- "email",
- "name"
- ],
- "properties": {
- "email": {
- "type": "string"
- },
- "id": {
- "type": "integer",
- "format": "int32"
- },
- "name": {
- "type": "string"
- },
- "sub": {
- "type": "string"
- }
- }
- },
- "Model": {
- "type": "object",
- "required": [
- "id",
- "name",
- "from",
- "to"
- ],
- "properties": {
- "from": {
- "type": "string",
- "format": "date-time"
- },
- "id": {
- "type": "integer",
- "format": "int64"
- },
- "name": {
- "type": "string"
- },
- "to": {
- "type": "string",
- "format": "date-time"
- },
- "user_id": {
- "type": [
- "integer",
- "null"
- ],
- "format": "int32"
- }
- }
- },
- "ServiceCreateEventRequest": {
- "type": "object",
- "required": [
- "name",
- "from",
- "to"
- ],
- "properties": {
- "from": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "to": {
- "type": "string"
- },
- "user_id": {
- "type": [
- "integer",
- "null"
- ],
- "format": "int32"
- }
- }
- }
- }
- },
- "tags": [
- {
- "name": "calendar",
- "description": "Calendar Management API"
- }
- ]
-}
\ No newline at end of file
diff --git a/src/agent.rs b/src/agent.rs
new file mode 100644
index 0000000..2bbc042
--- /dev/null
+++ b/src/agent.rs
@@ -0,0 +1,146 @@
+use chrono::Utc;
+
+use crate::api::{ChatRequest, ChatResponse, Message, Tool};
+use crate::tools;
+
+pub struct Agent {
+ client: reqwest::Client,
+ url: String,
+ zen_api_key: Option,
+ tavily_api_key: Option,
+ messages: Vec,
+ tools: Option>,
+ logs: String,
+ answer: Option,
+}
+
+impl Agent {
+ pub fn new(
+ zen_api_key: Option,
+ tavily_api_key: Option,
+ initial_message: String,
+ ) -> Result> {
+ let intro = format!(
+ "You are an autonomous agent. You have access to tools that can help
+ you achieve your goals. Use them wisely. The user is unable to respond to you
+ so do not ask for clarification and use the
+ answer tool once you to give your final answer. current date is {}",
+ Utc::now().format("%B %d, %Y %H:%M:%S UTC").to_string()
+ );
+ println!("initial_message: {}", intro);
+ let messages = vec![
+ Message {
+ role: "system".to_string(),
+ content: Some(intro),
+ tool_calls: None,
+ tool_call_id: None,
+ },
+ Message {
+ role: "user".to_string(),
+ content: Some(initial_message),
+ tool_calls: None,
+ tool_call_id: None,
+ },
+ ];
+
+ let tools = Some(tools::get_tools());
+
+ let client = reqwest::Client::builder()
+ .timeout(std::time::Duration::from_secs(60))
+ .build()?;
+
+ Ok(Self {
+ client,
+ url: "https://opencode.ai/zen/v1/chat/completions".to_string(),
+ zen_api_key,
+ tavily_api_key,
+ messages,
+ tools,
+ logs: String::new(),
+ answer: None,
+ })
+ }
+
+ fn log(&mut self, message: &str) {
+ println!("{}", message);
+ self.logs.push_str(message);
+ self.logs.push('\n');
+ }
+
+ pub async fn run(&mut self) -> Result<(String, Option), Box> {
+ let mut file_written = false;
+
+ while !file_written {
+ let request = ChatRequest {
+ model: "kimi-k2.5".to_string(),
+ messages: self.messages.clone(),
+ tools: self.tools.clone(),
+ };
+
+ self.log(&format!(
+ "--- Sending request to Zen API (Role: {}) ---",
+ self.messages.last().unwrap().role
+ ));
+
+ let mut request_builder = self.client.post(&self.url).json(&request);
+
+ if let Some(key) = &self.zen_api_key {
+ request_builder =
+ request_builder.header("Authorization", format!("Bearer {}", key));
+ }
+
+ let response = request_builder.send().await?;
+
+ if !response.status().is_success() {
+ let status = response.status();
+ let error_text = response.text().await?;
+ self.log(&format!("Error: API request failed with status {}", status));
+ self.log(&format!("Error details: {}", error_text));
+ return Err(format!("API request failed: {}", status).into());
+ }
+
+ let chat_response: ChatResponse = response.json().await?;
+ let assistant_message = chat_response.choices.get(0).unwrap().message.clone();
+
+ self.messages.push(assistant_message.clone());
+
+ if let Some(content) = &assistant_message.content {
+ if !content.is_empty() {
+ self.log(&format!("\nAssistant response:\n{}\n", content));
+ }
+ }
+
+ if let Some(tool_calls) = assistant_message.tool_calls {
+ for tool_call in tool_calls {
+ let (tool_message, written, tool_answer) =
+ tools::handle_tool_call(&tool_call, &self.tavily_api_key).await?;
+
+ if let Some(ans) = tool_answer {
+ self.answer = Some(ans);
+ }
+
+ if let Some(content) = &tool_message.content {
+ self.log(&format!(
+ "Tool result ({}): {}",
+ tool_call.function.name, content
+ ));
+ }
+
+ self.messages.push(tool_message);
+ if written {
+ file_written = true;
+ }
+ }
+ // Continue the loop to send tool results back
+ continue;
+ }
+
+ // No more tool calls from assistant, but we only exit if file was written
+ if !file_written {
+ self.log("--- Assistant didn't use write_file yet. Waiting for next turn... ---");
+ }
+ }
+
+ Ok((self.logs.clone(), self.answer.clone()))
+ }
+}
diff --git a/src/domain/agent/api.rs b/src/api.rs
similarity index 91%
rename from src/domain/agent/api.rs
rename to src/api.rs
index cbd93a8..2d2ae37 100644
--- a/src/domain/agent/api.rs
+++ b/src/api.rs
@@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
pub role: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub content: Option,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_calls: Option>,
@@ -28,7 +29,6 @@ pub struct FunctionCall {
pub struct ChatRequest {
pub model: String,
pub messages: Vec,
- #[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option>,
}
@@ -72,11 +72,8 @@ pub async fn perform_search(
query: &str,
api_key: &str,
) -> Result> {
- tracing::info!(query = %query, "Performing Tavily web search");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
- .connect_timeout(std::time::Duration::from_secs(10))
- .pool_idle_timeout(std::time::Duration::from_secs(60))
.build()?;
let response = client
.post("https://api.tavily.com/search")
@@ -97,7 +94,6 @@ pub async fn perform_search(
}
let search_data: TavilyResponse = response.json().await?;
- tracing::info!("Web search yielded {} results", search_data.results.len());
let mut results_text = String::new();
for (i, result) in search_data.results.iter().enumerate() {
diff --git a/src/domain/auth.rs b/src/auth.rs
similarity index 69%
rename from src/domain/auth.rs
rename to src/auth.rs
index a180185..0cf0f67 100644
--- a/src/domain/auth.rs
+++ b/src/auth.rs
@@ -10,17 +10,10 @@ pub struct Claims {
pub exp: usize,
pub iat: usize,
pub iss: String,
- pub aud: Audience,
+ pub aud: String,
}
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(untagged)]
-pub enum Audience {
- Single(String),
- Multiple(Vec),
-}
-
-#[derive(Debug, Deserialize, Clone)]
+#[derive(Debug, Deserialize)]
struct Jwk {
#[serde(rename = "kty")]
_kty: String,
@@ -38,18 +31,15 @@ struct Jwks {
pub struct JwksVerifier {
issuer: String,
- audience: String,
jwks_uri: String,
keys: Arc>>,
client: Client,
}
impl JwksVerifier {
- pub async fn new(issuer: String, audience: String) -> Result> {
- let client = Client::builder()
- .timeout(std::time::Duration::from_secs(30))
- .connect_timeout(std::time::Duration::from_secs(10))
- .build()?;
+ pub async fn new(issuer: String) -> Result> {
+ let client = Client::new();
+ // Authentik OIDC discovery
let discovery_url = format!(
"{}/.well-known/openid-configuration",
issuer.trim_end_matches('/')
@@ -63,7 +53,6 @@ impl JwksVerifier {
let verifier = Self {
issuer,
- audience,
jwks_uri,
keys: Arc::new(RwLock::new(Vec::new())),
client,
@@ -84,29 +73,18 @@ impl JwksVerifier {
let header = decode_header(token)?;
let kid = header.kid.ok_or("Missing kid in token header")?;
- let jwk = {
- let keys = self.keys.read().await;
- keys.iter().find(|k| k.kid == kid).cloned()
- };
-
- let jwk = match jwk {
- Some(key) => key,
- None => {
- self.refresh_keys().await?;
- let keys = self.keys.read().await;
- keys.iter()
- .find(|k| k.kid == kid)
- .cloned()
- .ok_or("Key not found in JWKS")?
- }
- };
+ let keys = self.keys.read().await;
+ let jwk = keys
+ .iter()
+ .find(|k| k.kid == kid)
+ .ok_or("Key not found in JWKS")?;
let decoding_key = DecodingKey::from_rsa_components(&jwk.n, &jwk.e)?;
let mut validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&[self.issuer.clone()]);
- validation.set_audience(&[self.audience.clone()]);
- validation.validate_aud = true;
+ // Aud validation might need careful config, usually it's the client_id
+ validation.validate_aud = false;
let token_data = decode::(token, &decoding_key, &validation)?;
Ok(token_data.claims)
@@ -126,10 +104,7 @@ impl Authenticator {
client_id: String,
client_secret: String,
) -> Result> {
- let client = Client::builder()
- .timeout(std::time::Duration::from_secs(30))
- .connect_timeout(std::time::Duration::from_secs(10))
- .build()?;
+ let client = Client::new();
let discovery_url = format!(
"{}/.well-known/openid-configuration",
issuer.trim_end_matches('/')
@@ -196,27 +171,4 @@ impl Authenticator {
Ok(res)
}
-
- pub async fn client_credentials(
- &self,
- scope: &str,
- ) -> Result> {
- let params = [
- ("grant_type", "client_credentials"),
- ("client_id", &self.client_id),
- ("client_secret", &self.client_secret),
- ("scope", scope),
- ];
-
- let res = self
- .client
- .post(&self.token_url)
- .form(¶ms)
- .send()
- .await?
- .json()
- .await?;
-
- Ok(res)
- }
}
diff --git a/src/config.rs b/src/config.rs
deleted file mode 100644
index b81a727..0000000
--- a/src/config.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-use crate::error::{AppError, AppResult};
-use std::env;
-
-#[derive(Clone, Debug)]
-pub struct Config {
- pub database_url: String,
- pub port: u16,
- pub zen_api_key: Option,
- pub tavily_api_key: Option,
- pub authentik_issuer: String,
- pub authentik_client_id: String,
- pub authentik_client_secret: String,
- pub cors_allowed_origins: Option,
- pub cookie_secure: bool,
- pub agent_max_turns: u32,
- pub agent_max_duration_secs: u64,
- pub vapid_private_key: String,
- pub calendar_api_url: String,
-}
-
-impl Config {
- pub fn from_env() -> AppResult {
- dotenvy::dotenv().ok();
-
- let database_url = env::var("DATABASE_URL")
- .map_err(|_| AppError::Config("DATABASE_URL must be set".into()))?;
-
- let port = env::var("PORT")
- .ok()
- .and_then(|s| s.parse().ok())
- .unwrap_or(3000);
-
- let zen_api_key = env::var("ZEN_API_KEY").ok();
- let tavily_api_key = env::var("TAVILY_API_KEY").ok();
-
- let authentik_issuer = env::var("AUTHENTIK_ISSUER")
- .map_err(|_| AppError::Config("AUTHENTIK_ISSUER must be set".into()))?;
- let authentik_client_id = env::var("AUTHENTIK_CLIENT_ID")
- .map_err(|_| AppError::Config("AUTHENTIK_CLIENT_ID must be set".into()))?;
- let authentik_client_secret = env::var("AUTHENTIK_CLIENT_SECRET")
- .map_err(|_| AppError::Config("AUTHENTIK_CLIENT_SECRET must be set".into()))?;
-
- let cors_allowed_origins = env::var("CORS_ALLOWED_ORIGINS").ok();
-
- let cookie_secure = env::var("COOKIE_SECURE")
- .map(|v| v == "true")
- .unwrap_or(false);
-
- let agent_max_turns = env::var("AGENT_MAX_TURNS")
- .ok()
- .and_then(|s| s.parse().ok())
- .unwrap_or(20);
-
- let agent_max_duration_secs = env::var("AGENT_MAX_DURATION_SECS")
- .ok()
- .and_then(|s| s.parse().ok())
- .unwrap_or(120);
-
- let vapid_private_key = env::var("VAPID_PRIVATE_KEY")
- .map_err(|_| AppError::Config("VAPID_PRIVATE_KEY must be set".into()))?;
-
- let calendar_api_url =
- env::var("CALENDAR_API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
-
- Ok(Config {
- database_url,
- port,
- zen_api_key,
- tavily_api_key,
- authentik_issuer,
- authentik_client_id,
- authentik_client_secret,
- cors_allowed_origins,
- cookie_secure,
- agent_max_turns,
- agent_max_duration_secs,
- vapid_private_key,
- calendar_api_url,
- })
- }
-}
diff --git a/src/domain/agent/mod.rs b/src/domain/agent/mod.rs
deleted file mode 100644
index 8f738cc..0000000
--- a/src/domain/agent/mod.rs
+++ /dev/null
@@ -1,322 +0,0 @@
-pub mod api;
-pub mod tools;
-
-use chrono::Utc;
-use sea_orm::DatabaseConnection;
-use std::sync::Arc;
-use std::time::{Duration, Instant};
-
-use self::api::{ChatRequest, ChatResponse, Message, Tool};
-
-pub struct Agent {
- db: DatabaseConnection,
- client: reqwest::Client,
- url: String,
- zen_api_key: Option,
- tavily_api_key: Option,
- calendar_client: Arc,
- pub user_sub: Option,
- pub messages: Vec,
- tools: Option>,
- logs: String,
- answer: Option,
-}
-
-use crate::error::{AppError, AppResult};
-
-impl Agent {
- pub fn new(
- db: DatabaseConnection,
- zen_api_key: Option,
- tavily_api_key: Option,
- calendar_client: Arc,
- user_sub: Option,
- initial_message: String,
- ) -> AppResult {
- let intro = format!(
- "You are an autonomous agent. You have access to tools that can help
- you achieve your goals. Use them wisely. The user is unable to respond to you
- so do not ask for clarification and use the
- answer tool once you to give your final answer. current date is {}",
- Utc::now().format("%B %d, %Y %H:%M:%S UTC").to_string()
- );
- let messages = vec![
- Message {
- role: "system".to_string(),
- content: Some(intro),
- tool_calls: None,
- tool_call_id: None,
- },
- Message {
- role: "user".to_string(),
- content: Some(initial_message),
- tool_calls: None,
- tool_call_id: None,
- },
- ];
-
- Self::with_messages(
- db,
- zen_api_key,
- tavily_api_key,
- calendar_client,
- user_sub,
- messages,
- )
- }
-
- pub fn with_messages(
- db: DatabaseConnection,
- zen_api_key: Option,
- tavily_api_key: Option,
- calendar_client: Arc,
- user_sub: Option,
- messages: Vec,
- ) -> AppResult {
- let tools = Some(tools::get_tools());
-
- let client = reqwest::Client::builder()
- .timeout(std::time::Duration::from_secs(120))
- .connect_timeout(std::time::Duration::from_secs(10))
- .tcp_keepalive(std::time::Duration::from_secs(30))
- .pool_idle_timeout(std::time::Duration::from_secs(60))
- .build()
- .map_err(|e| AppError::Internal(format!("Failed to build HTTP client: {}", e)))?;
-
- Ok(Self {
- db,
- client,
- url: "https://opencode.ai/zen/v1/chat/completions".to_string(),
- zen_api_key,
- tavily_api_key,
- calendar_client,
- user_sub,
- messages,
- tools,
- logs: String::new(),
- answer: None,
- })
- }
-
- fn log(&mut self, message: &str) {
- self.logs.push_str(message);
- self.logs.push('\n');
- }
-
- pub async fn run(
- &mut self,
- config: &crate::config::Config,
- ) -> AppResult<(String, Option)> {
- let mut finished = false;
- let start_time = Instant::now();
- let max_duration = Duration::from_secs(config.agent_max_duration_secs);
- let max_turns = config.agent_max_turns;
- let mut turns = 0;
-
- while !finished {
- if start_time.elapsed() > max_duration {
- return Err(AppError::Internal("Agent run timed out".into()));
- }
-
- if turns >= max_turns {
- return Err(AppError::Internal("Agent run exceeded max turns".into()));
- }
-
- tracing::info!("Turn {}", turns);
-
- turns += 1;
- let current_role = self
- .messages
- .last()
- .map(|m| m.role.as_str())
- .unwrap_or("unknown");
- self.log(&format!(
- "\n[Turn {}] Sending request (Last role: {})",
- turns, current_role
- ));
-
- let assistant_message =
- match tokio::time::timeout(Duration::from_secs(180), self.execute_turn()).await {
- Ok(res) => res?,
- Err(_) => {
- tracing::error!("Agent execution turn timed out after 180s");
- return Err(AppError::Internal("Agent execution turn timed out".into()));
- }
- };
-
- if let Some(tool_calls) = &assistant_message.tool_calls {
- tracing::info!("Assistant tool calls: {:#?}", tool_calls);
- if tool_calls.iter().any(|tc| tc.function.name == "answer") {
- finished = true;
- }
- }
-
- if self.answer.is_some() {
- tracing::info!("Answer: {}", self.answer.as_ref().unwrap());
- finished = true;
- }
-
- if !finished {
- self.messages.push(Message {
- role: "system".to_string(),
- content: Some(
- "continue, use the finish tool to submit your final answer".to_string(),
- ),
- tool_calls: None,
- tool_call_id: None,
- });
- }
-
- tracing::info!("Finished turn");
- }
-
- self.log("\n--- Execution Finished ---");
- Ok((self.logs.clone(), self.answer.clone()))
- }
-
- pub async fn execute_turn(&mut self) -> AppResult {
- let max_sub_turns = 20;
- let mut sub_turns = 0;
-
- loop {
- tracing::info!("Sub turn {}", sub_turns);
-
- sub_turns += 1;
- if sub_turns > max_sub_turns {
- return Err(AppError::Internal(
- "Interaction cycle turn limit exceeded".into(),
- ));
- }
-
- let chat_response = self.call_llm().await?;
- let assistant_message = chat_response
- .choices
- .get(0)
- .ok_or_else(|| AppError::Internal("Missing assistant response".into()))?
- .message
- .clone();
-
- tracing::info!("Assistant message: {:#?}", assistant_message);
-
- self.messages.push(assistant_message.clone());
-
- if let Some(content) = &assistant_message.content {
- tracing::info!("Assistant content: {}", content);
- if !content.is_empty() {
- self.log(&format!("\nAssistant: {}", content));
- }
- }
-
- if let Some(tool_calls) = &assistant_message.tool_calls {
- tracing::info!("Assistant tool calls: {:#?}", tool_calls);
- let mut is_final_cycle = false;
- let mut final_answer = None;
-
- for tool_call in tool_calls {
- self.log(&format!("Calling tool: {}", tool_call.function.name));
-
- let (tool_message, is_final, tool_answer) = tools::handle_tool_call(
- tool_call,
- &self.tavily_api_key,
- &self.db,
- &self.calendar_client,
- self.user_sub.as_deref(),
- )
- .await
- .map_err(|e| AppError::Internal(format!("Tool execution failed: {}", e)))?;
-
- if let Some(ans) = tool_answer {
- self.answer = Some(ans.clone());
- final_answer = Some(ans);
- self.log("Interaction marked as finished by tool.");
- }
-
- if let Some(content) = &tool_message.content {
- self.log(&format!("Tool result: {}", content));
- }
-
- self.messages.push(tool_message);
- if is_final {
- is_final_cycle = true;
- }
- }
-
- if is_final_cycle {
- tracing::info!("Final answer: {}", final_answer.as_ref().unwrap());
- return Ok(Message {
- role: "assistant".to_string(),
- content: final_answer.or(assistant_message.content),
- tool_calls: None,
- tool_call_id: None,
- });
- }
-
- continue;
- }
-
- return Ok(assistant_message);
- }
- }
-
- async fn call_llm(&self) -> AppResult {
- let request = ChatRequest {
- model: "kimi-k2.5".to_string(),
- messages: self.messages.clone(),
- tools: self.tools.clone(),
- };
-
- let mut request_builder = self
- .client
- .post(&self.url)
- .json(&request)
- .timeout(Duration::from_secs(60));
-
- if let Some(key) = &self.zen_api_key {
- request_builder = request_builder.header("Authorization", format!("Bearer {}", key));
- }
-
- let start = std::time::Instant::now();
- let response = request_builder.send().await.map_err(|e| {
- let duration = start.elapsed();
- let is_timeout = e.is_timeout();
- let is_connect = e.is_connect();
- tracing::error!(
- "Network error after {:?} during LLM call (Timeout: {}, Connect: {}): {:?}",
- duration,
- is_timeout,
- is_connect,
- e
- );
- AppError::Network(e)
- })?;
-
- let duration = start.elapsed();
- tracing::info!("LLM request completed in {:?}", duration);
-
- if !response.status().is_success() {
- let status = response.status();
- let body_text = response
- .text()
- .await
- .unwrap_or_else(|_| "Unknown body".into());
- let err = format!("API request failed: {} - {}", status, body_text);
- tracing::error!("{}", err);
- return Err(AppError::Internal(err));
- }
-
- let response_text = response.text().await.map_err(|e| {
- let err = format!("Failed to read response text: {}", e);
- tracing::error!("{}", err);
- AppError::Internal(err)
- })?;
-
- serde_json::from_str(&response_text).map_err(|e| {
- let err = format!(
- "Failed to parse LLM response: {} | Raw Body: {}",
- e, response_text
- );
- tracing::error!("{}", err);
- AppError::Internal(err)
- })
- }
-}
diff --git a/src/domain/agent/tools.rs b/src/domain/agent/tools.rs
deleted file mode 100644
index 2b257c0..0000000
--- a/src/domain/agent/tools.rs
+++ /dev/null
@@ -1,287 +0,0 @@
-use super::api::{self, FunctionDefinition, Message, Tool, ToolCall};
-use sea_orm::{
- ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect,
-};
-use serde::Deserialize;
-use std::sync::Arc;
-use uuid::Uuid;
-
-#[derive(Deserialize)]
-struct GoogleSearchArgs {
- query: String,
-}
-
-#[derive(Deserialize)]
-struct FinishArgs {
- result: String,
-}
-
-#[derive(Deserialize)]
-struct ListRunsArgs {
- from: Option,
- to: Option,
- task_ids: Option>,
-}
-
-pub fn get_tools() -> Vec {
- vec![
- Tool {
- tool_type: "function".to_string(),
- function: FunctionDefinition {
- name: "google_search".to_string(),
- description: "Search the web for information".to_string(),
- parameters: serde_json::json!({
- "type": "object",
- "properties": {
- "query": {
- "type": "string",
- "description": "The search query"
- }
- },
- "required": ["query"]
- }),
- },
- },
- Tool {
- tool_type: "function".to_string(),
- function: FunctionDefinition {
- name: "finish".to_string(),
- description: "Finish the task and provide a final answer".to_string(),
- parameters: serde_json::json!({
- "type": "object",
- "properties": {
- "result": {
- "type": "string",
- "description": "The final detailed answer to the task"
- }
- },
- "required": ["result"]
- }),
- },
- },
- Tool {
- tool_type: "function".to_string(),
- function: FunctionDefinition {
- name: "list_tasks".to_string(),
- description: "List all existing tasks and their goals".to_string(),
- parameters: serde_json::json!({
- "type": "object",
- "properties": {},
- }),
- },
- },
- Tool {
- tool_type: "function".to_string(),
- function: FunctionDefinition {
- name: "list_runs".to_string(),
- description: "List task runs with optional filters for date and task IDs"
- .to_string(),
- parameters: serde_json::json!({
- "type": "object",
- "properties": {
- "from": {
- "type": "string",
- "description": "ISO 8601 date string to filter runs from"
- },
- "to": {
- "type": "string",
- "description": "ISO 8601 date string to filter runs to"
- },
- "task_ids": {
- "type": "array",
- "items": { "type": "string", "format": "uuid" },
- "description": "List of task IDs to filter runs for"
- },
-
- },
- }),
- },
- },
- Tool {
- tool_type: "function".to_string(),
- function: FunctionDefinition {
- name: "calendar_list_events".to_string(),
- description: "List calendar events".to_string(),
- parameters: serde_json::json!({
- "type": "object",
- "properties": {
- "upcoming": {
- "type": "boolean",
- "description": "If true, only upcoming events will be listed"
- }
- }
- }),
- },
- },
- Tool {
- tool_type: "function".to_string(),
- function: FunctionDefinition {
- name: "calendar_create_event".to_string(),
- description: "Create a new calendar event".to_string(),
- parameters: serde_json::json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the event"
- },
- "from": {
- "type": "string",
- "description": "Start time in ISO 8601 format (e.g., 2023-10-27T10:00:00Z)"
- },
- "to": {
- "type": "string",
- "description": "End time in ISO 8601 format (e.g., 2023-10-27T11:00:00Z)"
- }
- },
- "required": ["name", "from", "to"]
- }),
- },
- },
- ]
-}
-
-pub async fn handle_tool_call(
- tool_call: &ToolCall,
- tavily_api_key: &Option,
- db: &DatabaseConnection,
- calendar: &Arc,
- user_sub: Option<&str>,
-) -> Result<(Message, bool, Option), Box> {
- let mut answer = None;
- let name = &tool_call.function.name;
-
- let (content, written) = if name == "google_search" {
- let args: GoogleSearchArgs = serde_json::from_str(&tool_call.function.arguments)?;
- let query = &args.query;
-
- let search_result = if let Some(key) = tavily_api_key {
- match api::perform_search(query, key).await {
- Ok(results) => results,
- Err(e) => format!("Search error: {}", e),
- }
- } else {
- "Error: TAVILY_API_KEY is not set. Cannot perform real search.".to_string()
- };
- (search_result, false)
- } else if name == "finish" {
- let args: FinishArgs = serde_json::from_str(&tool_call.function.arguments)?;
- let result = &args.result;
-
- answer = Some(result.clone());
- (result.clone(), true)
- } else if name == "list_tasks" {
- tracing::info!("Listing tasks from database");
- use crate::entities::task;
- let tasks = task::Entity::find()
- .order_by_desc(task::Column::CreatedAt)
- .all(db)
- .await?;
-
- let mut out = String::from("Tasks:\n");
- for t in tasks {
- out.push_str(&format!("- ID: {}, Goal: {}\n", t.id, t.goal));
- }
- (out, false)
- } else if name == "list_runs" {
- use crate::entities::task_run;
- tracing::info!(
- "Listing runs from database, args: {}",
- tool_call.function.arguments
- );
- let args: ListRunsArgs = serde_json::from_str(&tool_call.function.arguments)?;
-
- tracing::info!(
- "Listing runs from database with filters: from={:?}, to={:?}, task_ids={:?}",
- args.from,
- args.to,
- args.task_ids
- );
-
- let query = task_run::Entity::find();
-
- let mut condition = Condition::all();
-
- if let Some(from_str) = args.from {
- if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&from_str) {
- condition = condition.add(task_run::Column::CreatedAt.gte(dt));
- }
- }
-
- if let Some(to_str) = args.to {
- if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&to_str) {
- condition = condition.add(task_run::Column::CreatedAt.lte(dt));
- }
- }
-
- if let Some(id_strs) = args.task_ids {
- let mut valid_ids = Vec::new();
- for id_str in id_strs {
- match id_str.parse::() {
- Ok(uuid) => valid_ids.push(uuid),
- Err(e) => tracing::warn!("Skipping invalid UUID '{}' from LLM: {}", id_str, e),
- }
- }
- if !valid_ids.is_empty() {
- condition = condition.add(task_run::Column::TaskId.is_in(valid_ids));
- }
- }
-
- let runs = query
- .filter(condition)
- .order_by_desc(task_run::Column::CreatedAt)
- .limit(20)
- .all(db)
- .await?;
-
- let mut out = String::from("Recent Runs:\n");
- for r in runs {
- out.push_str(&format!(
- "- ID: {}, Task ID: {}, Status: {}, Created At: {}, Answer: {:?}\n",
- r.id, r.task_id, r.status, r.created_at, r.answer
- ));
- }
- (out, false)
- } else if name == "calendar_list_events" {
- let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
- let upcoming = args["upcoming"].as_bool();
- match calendar
- .list_events(user_sub.map(|s| s.to_string()), upcoming)
- .await
- {
- Ok(events) => {
- tracing::info!("{:#?}", events);
- (serde_json::to_string(&events)?, false)
- }
- Err(e) => (format!("Error listing events: {}", e), false),
- }
- } else if name == "calendar_create_event" {
- let args: serde_json::Value = serde_json::from_str(&tool_call.function.arguments)?;
- let name_val = args["name"].as_str().unwrap_or_default();
- let from_val = args["from"].as_str().unwrap_or_default();
- let to_val = args["to"].as_str().unwrap_or_default();
- match calendar
- .create_event(user_sub.map(|s| s.to_string()), name_val, from_val, to_val)
- .await
- {
- Ok(event) => (
- format!("Event created: {}", serde_json::to_string(&event)?),
- false,
- ),
- Err(e) => (format!("Error creating event: {}", e), false),
- }
- } else {
- (format!("Error: Unknown tool {}", name), false)
- };
-
- Ok((
- Message {
- role: "tool".to_string(),
- content: Some(content),
- tool_calls: None,
- tool_call_id: Some(tool_call.id.clone()),
- },
- written,
- answer,
- ))
-}
diff --git a/src/domain/calendar/mod.rs b/src/domain/calendar/mod.rs
deleted file mode 100644
index c6f49ae..0000000
--- a/src/domain/calendar/mod.rs
+++ /dev/null
@@ -1,298 +0,0 @@
-use crate::domain::auth::Authenticator;
-use crate::error::{AppError, AppResult};
-use chrono::{DateTime, Duration, Utc};
-use reqwest::Client;
-use serde::{Deserialize, Serialize};
-use std::sync::Arc;
-use tokio::sync::RwLock;
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct CalendarEvent {
- pub id: Option,
- pub name: String,
- pub from: DateTime,
- pub to: DateTime,
- pub user_sub: Option,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct CreateEventRequest {
- pub name: String,
- pub from: String,
- pub to: String,
- pub user_sub: Option,
-}
-struct TokenState {
- access_token: String,
- expires_at: DateTime,
-}
-
-pub struct CalendarClient {
- base_url: String,
- client: Client,
- authenticator: Arc,
- token_state: RwLock