diff --git a/.env.example b/.env.example index fd41b47..f61e3ef 100644 --- a/.env.example +++ b/.env.example @@ -59,8 +59,8 @@ CHECKPOINT_BACKEND=memory # Slack — leave unset to disable notifications # SLACK_WEBHOOK_URL=https://hooks.slack.com/services/xxx/yyy/zzz -# Proactive polling — set POLL_INTERVAL_MINUTES>0 to enable -# POLL_INTERVAL_MINUTES=5 # how often to check (minutes); 0 = disabled +# Proactive polling — set POLL_INTERVAL_SECONDS>0 to enable +# POLL_INTERVAL_SECONDS=300 # how often to check (seconds); 0 = disabled; 20 for dev testing # POLL_ERROR_THRESHOLD=5.0 # Lambda error rate % that triggers an investigation # POLL_REINVESTIGATE_HOURS=1 # don't re-investigate the same alarm within N hours diff --git a/CLAUDE.md b/CLAUDE.md index 00a2ea7..834af7b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -99,7 +99,7 @@ Everything below is built and working in the codebase: - Starts automatically on app startup if `event_consumer_enabled=True`, `sqs_queue_url` is set, or `init.json` marks the wizard as completed. ### Proactive Polling -- `polling_loop()` runs every `POLL_INTERVAL_MINUTES` minutes (disabled by default at 0); checks CloudWatch alarms in ALARM state and Lambda error rates above `POLL_ERROR_THRESHOLD`; auto-investigates new anomalies and posts to Slack. In-memory dedup via `_last_investigated` map. +- `polling_loop()` runs every `POLL_INTERVAL_SECONDS` seconds (disabled by default at 0); checks CloudWatch alarms in ALARM state and Lambda error rates above `POLL_ERROR_THRESHOLD`; auto-investigates new anomalies and posts to Slack. In-memory dedup via `_last_investigated` map. ### Frontend - React 18 + TypeScript + Vite + Tailwind CSS + `@tailwindcss/typography` @@ -211,7 +211,7 @@ JWT_EXPIRE_MINUTES=1440 # 24 hours SLACK_WEBHOOK_URL= # leave unset to disable # Proactive polling -POLL_INTERVAL_MINUTES=0 # 0 = disabled +POLL_INTERVAL_SECONDS=0 # 0 = disabled; set to e.g. 300 (5 min) to enable POLL_ERROR_THRESHOLD=5.0 # Lambda error rate % to trigger investigation POLL_REINVESTIGATE_HOURS=1 # dedup window @@ -254,7 +254,7 @@ EventBridge rules (9 event types) ### Startup sequence ``` db.init() → init_agent(checkpointer) - optional: asyncio.create_task(polling_loop()) if POLL_INTERVAL_MINUTES > 0 + optional: asyncio.create_task(polling_loop()) if POLL_INTERVAL_SECONDS > 0 optional: asyncio.create_task(event_consumer_loop()) if SQS configured or init.json present ``` diff --git a/README.md b/README.md index 9579b03..2f26ea1 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ and gives actionable mitigation plans — without the AWS DevOps Agent price tag - **Event-driven incident detection** — EventBridge → SQS → long-poll consumer; 9 EventBridge rules cover CloudWatch alarms, ECS task failures, Lambda async errors, RDS events, EC2 state changes, CodePipeline failures, and AWS Health events; runs alongside the metric poller — see [docs/event_detection.md](docs/event_detection.md) - **Context enrichment** — before the LLM runs, deterministic boto3 calls fetch facts about the affected resource (alarm details, recent logs, function config, etc.) to reduce tool call count and speed up investigations - **SNS alert delivery** — after each event-driven investigation, findings are published to a configured SNS topic as structured JSON; fires alongside Slack so both channels receive results simultaneously -- **Monitoring dashboard** — live incident feed showing all event-driven investigations: confidence level, affected service, root cause summary, SNS status; each alert has a detail page with an **Investigate** button that opens a pre-seeded chat — see [docs/monitoring.md](docs/monitoring.md) +- **Monitoring dashboard** — live incident feed showing all event-driven investigations: confidence level (or FAILED badge), affected service, root cause summary, SNS status; each alert links back to its original investigation session via **View investigation** so you can follow up without losing context — see [docs/monitoring.md](docs/monitoring.md) - **AWS Configuration settings tab** — admin-only editable tab in Settings for SNS Topic ARN, SQS Queue URL, and AWS Region; shared org-wide (server-side `init.json`); includes an inline IAM permission checker per service - **Web UI** — React + Vite SPA served by FastAPI: - **Chat page** — streaming responses, collapsible tool call inspector, cost/latency card, stop button; supports `?prompt=` deeplink for pre-seeded investigations from the Monitoring dashboard @@ -251,7 +251,7 @@ docs/ # Feature reference — auth, schema, skills, databases, - [x] **Event-driven incident detection** — EventBridge → SQS → long-poll consumer; 9 EventBridge rules covering CloudWatch alarms, ECS, Lambda, RDS, EC2, CodePipeline, and AWS Health; runs in parallel with the metric poller; see [docs/event_detection.md](docs/event_detection.md) - [x] **SNS alert delivery** — findings published to SNS after each event-driven investigation; fires alongside Slack; structured JSON payload for easy downstream processing - [x] **Context enrichment** — deterministic boto3 calls per event type before LLM runs; reduces tool call count by front-loading relevant resource facts -- [x] **Monitoring dashboard** — live incident feed, per-service health summary, alert detail page, investigate deeplink to pre-seeded chat; see [docs/monitoring.md](docs/monitoring.md) +- [x] **Monitoring dashboard** — live incident feed, per-service health summary, alert detail page; "View investigation" opens the original agent session for follow-up; failed investigations flagged separately; see [docs/monitoring.md](docs/monitoring.md) - [x] **AWS Configuration settings tab** — admin-only editable tab for SNS/SQS/region config; shared org-wide via server-side `init.json`; inline IAM permission checker ### Later diff --git a/docs/databases.md b/docs/databases.md index 6828118..0bc539e 100644 --- a/docs/databases.md +++ b/docs/databases.md @@ -44,6 +44,10 @@ Uses `aiosqlite` for the app tables and `langgraph-checkpoint-sqlite` for the LangGraph checkpointer. Both share the same `.db` file via separate connections with WAL mode enabled. +SQLite also stores app-level configuration such as init wizard completion and event +monitoring infrastructure state in `app_config`. `data/init.json` is only a local +cache/fallback. + The file and its parent directory are created automatically on first start. **When to use:** Single-server deployments, personal use, hobbyist setups where @@ -81,6 +85,8 @@ DATABASE_URL=postgresql://user:password@localhost:5432/opendevops Uses `psycopg3` + `AsyncConnectionPool` for the app tables and `langgraph-checkpoint-postgres` for the LangGraph checkpointer. The checkpointer schema is created automatically via `AsyncPostgresSaver.setup()`. +Init wizard and event-infrastructure state are stored in the `app_config` table so +all app instances share the same setup status. **When to use:** Production deployments, team environments, when you need full dashboard analytics, multi-instance horizontal scaling. diff --git a/docs/event_detection.md b/docs/event_detection.md index 817557b..0316cde 100644 --- a/docs/event_detection.md +++ b/docs/event_detection.md @@ -30,7 +30,7 @@ AWS Service (ECS, Lambda, RDS, EC2…) ### Option A — Init wizard (recommended) -Go to **Settings → AWS Configuration** after first login. Enter your SNS Topic ARN and AWS Region, save, then click **Run checks** to verify IAM permissions. The SQS queue and EventBridge rules are created automatically the first time the server starts with valid config. +Go to **Settings → AWS Configuration** after first login. Enter your AWS Region and optional SNS Topic ARN, save, then click **Run checks** to verify IAM permissions. Click **Create Infrastructure** to create the SQS queue, EventBridge rules, and aggregate Lambda CloudWatch alarm. ### Option B — Manual `.env` @@ -40,7 +40,7 @@ EVENT_CONSUMER_ENABLED=true # or leave unset — consumer also starts if SNS_TOPIC_ARN=arn:aws:sns:us-east-1:123456789012:opendevops-alerts ``` -The consumer starts automatically on server startup if either `EVENT_CONSUMER_ENABLED=true` or `SQS_QUEUE_URL` is set. +The consumer starts automatically on server startup if either `EVENT_CONSUMER_ENABLED=true`, `SQS_QUEUE_URL` is set, or the init wizard has enabled event infrastructure in persistent app config. --- @@ -50,15 +50,15 @@ The consumer starts automatically on server startup if either `EVENT_CONSUMER_EN | Rule | Events captured | |---|---| -| `opendevops-cloudwatch-alarms` | CloudWatch Alarm state changes | -| `opendevops-ecs-failures` | ECS task stopped with non-zero exit / OOM | -| `opendevops-lambda-errors` | Lambda async invocation failures | -| `opendevops-rds-events` | RDS instance failures and failovers | +| `opendevops-alarm-state` | CloudWatch Alarm state changes to `ALARM` | +| `opendevops-lambda-failure` | Lambda async invocation failures | +| `opendevops-lambda-throttle` | Lambda throttling errors | +| `opendevops-ecs-task-stopped` | ECS task stopped with non-zero exit / OOM | | `opendevops-ec2-state` | EC2 instance state changes | -| `opendevops-asg-events` | Auto Scaling launch / terminate failures | -| `opendevops-codepipeline` | CodePipeline stage failures | -| `opendevops-ssm-compliance` | SSM patch compliance failures | -| `opendevops-health-events` | AWS Health personal health events | +| `opendevops-rds-events` | RDS instance failures and failovers | +| `opendevops-health` | AWS Health personal health events | +| `opendevops-codedeploy-failure` | CodeDeploy deployment failures | +| `opendevops-guardduty` | GuardDuty findings | --- @@ -82,13 +82,14 @@ Context is appended to the investigation prompt, capped at 3 000 chars to stay w `event_consumer_loop()` in `src/agent/event_consumer.py`: -1. Receives up to 10 messages per poll with a 20-second long-poll wait +1. Receives up to 5 messages per poll with a 20-second long-poll wait 2. Filters noise: only processes events where `_is_real_failure()` returns true (e.g. skips EC2 `running` state changes, healthy RDS events) -3. Calls `collect_context(event)` to enrich the prompt -4. Runs a full agent investigation via `agent.ainvoke` -5. Persists the result to the `alerts` table via `monitor_store.add_alert()` -6. Delivers findings via SNS and Slack (if configured) -7. Deletes the SQS message +3. Deduplicates: checks an in-memory set (`_in_progress`) and the database to skip events already being investigated or investigated within the last 3 minutes +4. Calls `collect_context(event)` to enrich the prompt +5. Generates a `session_id` and runs a full agent investigation via `agent.ainvoke`, persisting the session and messages to the database (`source = 'event'`) +6. Persists the result to the `alerts` table (linked to the session via `session_id`) via `monitor_store.add_alert()` +7. Delivers findings via SNS and Slack (if configured); failed investigations are flagged with status `'failed'` but are still persisted and notified +8. Deletes the SQS message The consumer is started as an `asyncio.Task` in the FastAPI lifespan and shut down cleanly on server stop. @@ -120,9 +121,10 @@ The IAM role/user used by the agent needs these additional permissions for event ``` sns:Publish, sns:GetTopicAttributes sqs:CreateQueue, sqs:SetQueueAttributes, sqs:GetQueueAttributes -sqs:ReceiveMessage, sqs:DeleteMessage, sqs:ListQueues +sqs:ReceiveMessage, sqs:DeleteMessage, sqs:DeleteQueue, sqs:ListQueues events:PutRule, events:PutTargets, events:ListRules events:RemoveTargets, events:DeleteRule +cloudwatch:PutMetricAlarm, cloudwatch:DeleteAlarms, cloudwatch:DescribeAlarms ``` Use **Settings → AWS Configuration → Run checks** to validate all permissions before going live. @@ -131,15 +133,19 @@ Use **Settings → AWS Configuration → Run checks** to validate all permission ## Persistence -Completed investigations are stored in the `alerts` table across all three storage backends (memory, SQLite, PostgreSQL). The Monitoring dashboard reads from this table. +Each event-driven investigation creates a full session (messages, tool calls, usage events) in the database, linked to the alert via `session_id`. This powers the "View investigation" button on the Monitoring page, which opens the original chat instead of starting a fresh one. + +Sessions created by the event consumer carry `source = 'event'` and are hidden from the sidebar by default. They become visible after the user sends a follow-up message in the chat (`user_interacted` is then set to `true`). + +The `alerts` table records `status` (`'completed'` or `'failed'`), `dedup_key` (MD5 fingerprint of stable event fields for cross-run deduplication), and `session_id` (FK to the session that produced it). -For PostgreSQL, run the migration before starting the server: +All three storage backends (memory, SQLite, PostgreSQL) support alerts. For PostgreSQL, run migrations before starting the server: ```bash uv run python scripts/setup_db.py ``` -For SQLite, the `alerts` table is created automatically on first start. +For SQLite, schema migrations are applied automatically on first start. --- @@ -151,7 +157,7 @@ For SQLite, the `alerts` table is created automatically on first start. | `src/agent/event_infra.py` | Create/teardown SQS queue + EventBridge rules | | `src/agent/context_collectors.py` | Per-event-type boto3 context enrichment | | `src/agent/monitor_store.py` | In-memory service status + DB-backed alert persistence | -| `src/agent/init_store.py` | Persist init config to `data/init.json` | +| `src/agent/init_store.py` | Persist init config to DB-backed `app_config` with `data/init.json` fallback/cache | | `src/agent/permission_checker.py` | IAM permission validation | | `src/tools/sns.py` | SNS publish wrapper | | `src/api/routers/init.py` | Setup wizard API endpoints | diff --git a/docs/iam_setup.md b/docs/iam_setup.md new file mode 100644 index 0000000..4ee5a57 --- /dev/null +++ b/docs/iam_setup.md @@ -0,0 +1,168 @@ +# IAM Setup Guide + +This guide covers how to create an AWS IAM user (or role) with the permissions OpenDevOps Agent needs, and how to plug the credentials into your environment. + +--- + +## Credential options + +| Method | When to use | +|---|---| +| **IAM user + access key** | Self-hosted server, Docker, Railway, Render, local dev without AWS CLI profiles | +| **Named profile** (`~/.aws/credentials`) | Local dev with AWS CLI already configured | +| **IAM role** | EC2 / ECS / Lambda — attach the role to the instance/task, no keys needed | + +For a typical self-hosted deployment, create an IAM user and paste the keys into `.env`. + +--- + +## Step 1 — Create the IAM user + +1. Open **IAM → Users → Create user** in the AWS console +2. Username: `opendevops-agent` (or any name you prefer) +3. Select **"Attach policies directly"** +4. Create and attach the inline policy below (see [Step 2](#step-2--iam-policy)) +5. Open the user → **Security credentials → Create access key** +6. Choose **"Other"** as the use case +7. Copy the **Access key ID** and **Secret access key** — you won't see the secret again + +--- + +## Step 2 — IAM policy + +Create a **customer-managed policy** named `OpenDevOpsAgentPolicy` with this JSON: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ReadOnlyInvestigation", + "Effect": "Allow", + "Action": [ + "cloudwatch:DescribeAlarms", + "cloudwatch:DescribeAlarmHistory", + "cloudwatch:GetMetricStatistics", + "cloudwatch:GetMetricData", + "logs:DescribeLogGroups", + "logs:FilterLogEvents", + "logs:StartQuery", + "logs:GetQueryResults", + "cloudtrail:LookupEvents", + "ecs:ListClusters", + "ecs:DescribeClusters", + "ecs:ListServices", + "ecs:DescribeServices", + "ecs:ListTasks", + "ecs:DescribeTasks", + "lambda:ListFunctions", + "lambda:GetFunction", + "lambda:GetFunctionConfiguration", + "ec2:DescribeInstances", + "ec2:DescribeInstanceStatus", + "rds:DescribeDBInstances", + "rds:DescribeEvents", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + "sts:GetCallerIdentity" + ], + "Resource": "*" + }, + { + "Sid": "EventMonitoringSetup", + "Effect": "Allow", + "Action": [ + "sqs:CreateQueue", + "sqs:DeleteQueue", + "sqs:GetQueueUrl", + "sqs:GetQueueAttributes", + "sqs:SetQueueAttributes", + "sqs:ListQueues", + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:ChangeMessageVisibility", + "sqs:SendMessage", + "events:PutRule", + "events:PutTargets", + "events:ListRules", + "events:RemoveTargets", + "events:DeleteRule", + "cloudwatch:PutMetricAlarm", + "cloudwatch:DeleteAlarms" + ], + "Resource": "*" + }, + { + "Sid": "SNSNotifications", + "Effect": "Allow", + "Action": [ + "sns:Publish", + "sns:GetTopicAttributes" + ], + "Resource": "*" + } + ] +} +``` + +### What each block does + +| Block | Purpose | +|---|---| +| `ReadOnlyInvestigation` | Read-only access to CloudWatch, CloudTrail, ECS, Lambda, EC2, RDS, IAM — used by the investigation agent tools | +| `EventMonitoringSetup` | Create/manage the SQS queue and EventBridge rules during the setup wizard; poll SQS at runtime for incoming events | +| `SNSNotifications` | Optional — publish alert summaries to an SNS topic after investigations; check topic attributes during permission check. Remove this block if you're not using SNS. | + +### Minimal policy (investigation only, no event monitoring) + +If you only want the chat interface and manual investigations — no automatic event detection — you can attach just the `ReadOnlyInvestigation` statement and skip the rest. + +--- + +## Step 3 — Add credentials to `.env` + +```bash +# Option A — explicit keys (server, Docker, CI) +AWS_ACCESS_KEY_ID=AKIA... +AWS_SECRET_ACCESS_KEY=... +AWS_REGION=us-east-1 + +# Option B — named profile (local dev) +AWS_PROFILE=opendevops-agent +AWS_REGION=us-east-1 +``` + +For Option B, add a profile to `~/.aws/credentials`: + +```ini +[opendevops-agent] +aws_access_key_id = AKIA... +aws_secret_access_key = ... +``` + +--- + +## Step 4 — Verify in the setup wizard + +After starting the app, the setup wizard's **Step 3 (Permission Check)** calls each service with a lightweight read and shows a pass/fail result for: + +| Service | Required | +|---|---| +| CloudWatch | Yes | +| Lambda | Yes | +| SQS | Yes | +| EventBridge | Yes | +| ECS | No | +| RDS | No | +| EC2 | No | +| IAM / STS | No | +| SNS | No | +| CloudTrail | No | + +Required services must pass for event monitoring to work. Optional services are used when available — missing permissions will simply return empty results for that service. + +--- + +## Using an IAM role instead of a user + +If running on EC2, ECS, or Lambda, attach the same policy to the instance/task role instead. Remove `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_PROFILE` from `.env` — the SDK picks up the role automatically via the instance metadata service. diff --git a/docs/monitoring.md b/docs/monitoring.md index 63e3563..49865f3 100644 --- a/docs/monitoring.md +++ b/docs/monitoring.md @@ -13,7 +13,7 @@ OpenDevOps Agent has two complementary detection modes. Both write to the same ` | **How it works** | Agent polls CloudWatch alarms + Lambda error rates on a timer | AWS fires EventBridge rules → SQS → consumer → agent | | **Setup** | Set `POLL_INTERVAL_MINUTES` in `.env` | Create infrastructure via Settings → AWS Configuration | | **Detection latency** | Up to N minutes (your poll interval) | 2–5 min for alarm-based events; near-instant for direct events (ECS, RDS, GuardDuty) | -| **CloudWatch alarm required** | No — reads Lambda error rate directly via API | Yes — EventBridge only fires when an alarm changes state | +| **CloudWatch alarm required** | No — reads Lambda error rate directly via API | Only for metric/alarm detections; direct AWS service events do not need a CloudWatch alarm | | **Works without AWS setup** | Yes — no SQS/EventBridge needed | No — requires SQS queue + EventBridge rules | | **Event types covered** | CloudWatch alarms in ALARM state + Lambda error rates | CloudWatch alarms, ECS task failures, Lambda async errors, RDS events, EC2 state changes, CodePipeline failures, GuardDuty findings, AWS Health events | | **Appears in /monitoring** | Yes | Yes | @@ -131,7 +131,7 @@ When you create infrastructure (Settings → AWS Configuration → Create Infras - Trips when any Lambda has ≥ 1 error in a 60-second window - Triggers the `opendevops-alarm-state` EventBridge rule → SQS → consumer -This means you get event-driven Lambda coverage without creating per-function alarms. +This means you get event-driven Lambda coverage without creating per-function alarms. Because it is account-wide and trips on a single error, it is intentionally easy to test but can be noisy in busy accounts. For production, consider replacing it with scoped per-function or per-service alarms once you know which workloads matter. ### Setup @@ -140,6 +140,8 @@ Go to **Settings → AWS Configuration → Create Infrastructure**. This creates - 9 EventBridge rules (all prefixed `opendevops-`) - Aggregate CloudWatch alarm: `opendevops-lambda-errors-aggregate` +Wizard and infrastructure state is stored in the database for SQLite/PostgreSQL deployments (`app_config`) and mirrored to `data/init.json` as a local cache/fallback. Memory mode keeps only local process/file state. Teardown deletes only infrastructure created by the wizard; queues supplied through `.env` or pasted manually are disconnected from the app but not deleted. + To remove it, click **Teardown**. See [event_detection.md](event_detection.md) for full setup details and IAM permission requirements. ### Context collectors @@ -205,7 +207,7 @@ Invokes the Lambda with a bad payload, then exits. The `opendevops-lambda-errors Each alert card shows: -- **Confidence badge** — `HIGH` (red), `MEDIUM` (amber), or `LOW` (grey) +- **Status badge** — `HIGH` (red), `MEDIUM` (amber), `LOW` (grey), or `FAILED` (rose) if the agent hit its tool limit or encountered an error - **Service** — the affected AWS service and resource name - **Error** — the root cause summary from the agent - **Time** — when the event was detected @@ -221,7 +223,16 @@ Shows the full investigation result: - Resolution steps produced by the agent - Timestamp and SNS notification status -**Investigate button** — opens a new chat session with a pre-seeded prompt for deeper analysis. The prompt auto-submits when the chat page loads. +**Investigate / View investigation button** — behaviour depends on whether the investigation session was persisted: + +- **View investigation** — shown for event-driven and poller alerts. Opens the original investigation chat so you can see the agent's tool calls, reasoning, and evidence, and send follow-up messages. +- **Investigate** — shown for older alerts (created before session persistence was added). Opens a new chat with a pre-seeded prompt for deeper analysis. + +### Session continuity and sidebar + +When the event consumer or poller runs an investigation, it creates a session (`source = 'event'`) and links it to the resulting alert via `session_id`. These sessions are **hidden from the sidebar by default** — they represent automated runs the user did not initiate. + +If you open a "View investigation" chat and send at least one follow-up message, the session is **promoted to the sidebar** (`user_interacted = true`) so you can navigate back to it without going through the Monitoring page again. ### Service Health Panel diff --git a/docs/schema.md b/docs/schema.md index 2b75a31..482e8dc 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -15,6 +15,8 @@ psql $DATABASE_URL -f migrations/001_initial.sql psql $DATABASE_URL -f migrations/002_soft_delete.sql psql $DATABASE_URL -f migrations/003_usage_events_metadata.sql psql $DATABASE_URL -f migrations/004_users_rbac.sql +psql $DATABASE_URL -f migrations/005_alerts.sql +psql $DATABASE_URL -f migrations/006_app_config.sql ``` All migrations are idempotent (`IF NOT EXISTS`, `IF column does not already exist`). @@ -134,6 +136,28 @@ Structured root-cause analysis rows extracted from agent final answers. Table ex ### `api_keys` *(future — Phase 3)* Hashed API keys for programmatic access. Table exists but not yet implemented. +### `alerts` +Persisted event-driven and proactive polling investigation results shown on `/monitoring`. + +| Column | Type | Notes | +|---|---|---| +| `id` | UUID PK | `gen_random_uuid()` | +| `service` | TEXT | Affected service/resource label | +| `error` | TEXT | Root cause summary | +| `resolution` | TEXT | Mitigation steps joined as text | +| `confidence` | TEXT | `HIGH`, `MEDIUM`, or `LOW` | +| `sns_sent` | BOOLEAN | Whether SNS publish succeeded | +| `created_at` | TIMESTAMPTZ | | + +### `app_config` +Application-level key/value configuration shared by all server instances. The init wizard stores its setup and event-infrastructure state under key `init`. `data/init.json` is kept as a local cache/fallback for memory mode and bootstrap recovery. + +| Column | Type | Notes | +|---|---|---| +| `key` | TEXT PK | e.g. `init` | +| `value` | JSONB | Setup state, AWS region, SQS URL, rule ARNs | +| `updated_at` | TIMESTAMPTZ | | + --- ## Entity Relationship diff --git a/docs/ui.md b/docs/ui.md index 0e36db2..66df26e 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -90,7 +90,7 @@ Accessible from the sidebar for all authenticated users. Each card shows the aff Shows active configuration. Four tabs (admins see all four; regular users see the last three): -**AWS Configuration** *(admin only, default tab for admins)* — editable fields for SNS Topic ARN, SQS Queue URL, and AWS Region. Values are saved to `data/init.json` server-side and shared across all admin users. Includes: +**AWS Configuration** *(admin only, default tab for admins)* — editable fields for SNS Topic ARN, SQS Queue URL, and AWS Region. Values are stored in database-backed app config for SQLite/PostgreSQL deployments and mirrored to `data/init.json` as a local cache/fallback. Includes: - **Event Monitoring** section — shows Enabled / Disabled status; **Create Infrastructure** button provisions the SQS queue and 9 EventBridge rules; **Teardown** button removes them (with confirmation). - **Run checks** button that validates IAM permissions for every AWS service the agent uses. @@ -109,9 +109,9 @@ Accessible at `/init` before any users exist. A 4-step wizard guides the admin t 1. **Create account** — email + password for the admin user 2. **AWS Configuration** — region selector and optional SNS Topic ARN for alert delivery 3. **Permission check** — verifies IAM permissions per service; required services (CloudWatch, Lambda, SQS, EventBridge) are highlighted; "Continue anyway" is always available -4. **Enable event monitoring** — creates the SQS queue and 9 EventBridge rules; shows a success summary with queue URL and rule list; includes a **Skip** option with a disclaimer listing what won't work without it +4. **Enable event monitoring** — creates the SQS queue, 9 EventBridge rules, and aggregate Lambda alarm; shows a success summary with queue URL and rule list; includes a **Skip** option that finishes onboarding without event infrastructure -The page redirects away automatically once an admin account exists. +The page redirects away automatically once onboarding is complete. Event monitoring can still be enabled or torn down later from Settings without forcing users back through onboarding. --- diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d5c650f..076c2c8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,7 +11,7 @@ import MonitoringPage from './pages/MonitoringPage'; import AlertDetailPage from './pages/AlertDetailPage'; import InitPage from './pages/InitPage'; import ProtectedRoute from './components/ProtectedRoute'; -import { fetchSessions, deleteSession as apiDeleteSession, renameSession as apiRenameSession } from './lib/api'; +import { apiFetch, fetchSessions, deleteSession as apiDeleteSession, renameSession as apiRenameSession } from './lib/api'; import { useAuth } from './context/AuthContext'; import type { Session } from './types'; @@ -29,13 +29,46 @@ function LogoutPage() { } function RedirectToSession() { - const stored = localStorage.getItem('devops-session-id'); - const id = stored ?? (() => { - const newId = crypto.randomUUID(); - localStorage.setItem('devops-session-id', newId); - return newId; - })(); - return ; + const [ready, setReady] = useState<'init' | 'chat' | null>(null); + const { user, authRequired, loading } = useAuth(); + + useEffect(() => { + if (loading) return; + + let cancelled = false; + const routeAfterInitCheck = async () => { + if (authRequired && user?.role !== 'admin') { + if (!cancelled) setReady('chat'); + return; + } + + try { + const r = await apiFetch('/api/init/status'); + const d = await r.json(); + const setupComplete = Boolean(d.setup_complete ?? d.initialized); + const accountReady = Boolean(d.has_user || !d.auth_enabled); + if (!cancelled) setReady(setupComplete && accountReady ? 'chat' : 'init'); + } catch { + if (!cancelled) setReady('chat'); + } + }; + + setReady(null); + void routeAfterInitCheck(); + return () => { cancelled = true; }; + }, [authRequired, loading, user?.role]); + + if (ready === 'init') return ; + if (ready === 'chat') { + const stored = localStorage.getItem('devops-session-id'); + const id = stored ?? (() => { + const newId = crypto.randomUUID(); + localStorage.setItem('devops-session-id', newId); + return newId; + })(); + return ; + } + return null; } const PAGE_SIZE = 15; diff --git a/frontend/src/pages/AlertDetailPage.tsx b/frontend/src/pages/AlertDetailPage.tsx index 133219e..37df994 100644 --- a/frontend/src/pages/AlertDetailPage.tsx +++ b/frontend/src/pages/AlertDetailPage.tsx @@ -21,9 +21,12 @@ export default function AlertDetailPage() { const investigate = () => { if (!alert) return; - const prompt = `Investigate this incident further:\n\nService: ${alert.service}\nError: ${alert.error}\nConfidence: ${alert.confidence}\nTime: ${alert.timestamp}\n\nPlease provide deeper root cause analysis, check related services, and suggest preventive measures.`; - const sessionId = crypto.randomUUID(); - navigate(`/chat/${sessionId}?prompt=${encodeURIComponent(prompt)}`); + if (alert.session_id) { + navigate(`/chat/${alert.session_id}`); + } else { + const prompt = `Investigate this incident further:\n\nService: ${alert.service}\nError: ${alert.error}\nConfidence: ${alert.confidence}\nTime: ${alert.timestamp}\n\nPlease provide deeper root cause analysis, check related services, and suggest preventive measures.`; + navigate(`/chat/${crypto.randomUUID()}?prompt=${encodeURIComponent(prompt)}`); + } }; if (loading) { @@ -50,16 +53,23 @@ export default function AlertDetailPage() {

{alert.service}

- {alert.confidence} + {alert.status === 'failed' ? ( + + FAILED + + ) : ( + {alert.confidence} + )}
{new Date(alert.timestamp).toLocaleString()} @@ -72,7 +82,7 @@ export default function AlertDetailPage() {
diff --git a/frontend/src/pages/InitPage.tsx b/frontend/src/pages/InitPage.tsx index 15a25ba..e8aec13 100644 --- a/frontend/src/pages/InitPage.tsx +++ b/frontend/src/pages/InitPage.tsx @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { ArrowRight, Loader2, Terminal, Eye, EyeOff, CheckCircle, XCircle, AlertTriangle, ChevronRight, Check } from 'lucide-react'; import { cn } from '../lib/utils'; +import { useAuth } from '../context/AuthContext'; +import { apiFetch } from '../lib/api'; const inp = 'w-full text-base text-gray-900 dark:text-white bg-gray-50 dark:bg-[#18181B] border border-gray-200 dark:border-[#27272A] rounded-xl px-4 py-3.5 outline-none focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/10 transition-all placeholder:text-gray-400 dark:placeholder:text-[#52525B]'; @@ -43,11 +45,13 @@ type Step = 1 | 2 | 3 | 4; export default function InitPage() { const navigate = useNavigate(); + const { user, login, authRequired, loading: authLoading } = useAuth(); const [step, setStep] = useState(1); // Step 1 - const [username, setUsername] = useState('admin'); + const [orgName, setOrgName] = useState(''); + const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showPw, setShowPw] = useState(false); @@ -67,24 +71,35 @@ export default function InitPage() { const [error, setError] = useState(''); useEffect(() => { - fetch('/api/init/status') + if (authLoading) return; + if (user && user.role !== 'admin') { navigate('/', { replace: true }); return; } + apiFetch('/api/init/status') .then(r => r.json()) - .then(d => { if (d.has_user) navigate('/', { replace: true }); }) + .then(d => { + setRegion(d.aws_region || 'us-east-1'); + setSnsArn(d.sns_topic_arn || ''); + if (d.initialized && (d.has_user || !d.auth_enabled)) { + navigate('/', { replace: true }); + return; + } + setStep((user || !authRequired) ? 2 : 1); + }) .catch(() => {}); - }, [navigate]); + }, [authLoading, authRequired, navigate, user]); // ─── Step 1: create admin account ───────────────────────────────────────── const createUser = async () => { - if (!username.trim() || !password.trim()) { setError('Both fields are required'); return; } + if (!username.trim() || !password.trim()) { setError('Email and password are required'); return; } setLoading(true); setError(''); try { - const r = await fetch('/api/init/create-user', { + const r = await apiFetch('/api/init/create-user', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username.trim(), password }), }); const data = await r.json(); - if (data.error) { setError(data.error); return; } + if (!r.ok || data.error) { setError(data.detail || data.error || 'Failed to create account'); return; } + if (authRequired) await login(username.trim(), password); setStep(2); } finally { setLoading(false); @@ -95,10 +110,10 @@ export default function InitPage() { const saveConfig = async () => { setLoading(true); setError(''); try { - const r = await fetch('/api/init/setup', { + const r = await apiFetch('/api/init/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ aws_region: region, sns_topic_arn: snsArn, sqs_queue_url: '' }), + body: JSON.stringify({ aws_region: region, sns_topic_arn: snsArn, sqs_queue_url: '', org_name: orgName.trim() }), }); if (!r.ok) { setError('Failed to save configuration'); return; } setStep(3); @@ -111,7 +126,7 @@ export default function InitPage() { const checkPerms = async () => { setLoading(true); setError(''); try { - const r = await fetch('/api/init/check-permissions', { method: 'POST' }); + const r = await apiFetch('/api/init/check-permissions', { method: 'POST' }); const d = await r.json() as { permissions: Record }; setPerms(d.permissions); } catch { @@ -125,18 +140,35 @@ export default function InitPage() { const createInfra = async () => { setLoading(true); setError(''); try { - const r = await fetch('/api/init/complete', { method: 'POST' }); - const d = await r.json() as { initialized: boolean; error: string | null }; - if (!d.initialized) { setError(d.error || 'Infrastructure setup failed'); return; } + const r = await apiFetch('/api/init/complete', { method: 'POST' }); + const d = await r.json() as { initialized: boolean; error?: string | null; detail?: string }; + if (!r.ok || !d.initialized) { setError(d.error || d.detail || 'Infrastructure setup failed'); return; } setInfraDone(true); // Fetch the queue URL from init status to display in summary - const s = await fetch('/api/init/status').then(res => res.json()); + const s = await apiFetch('/api/init/status').then(res => res.json()); setQueueUrl(s.sqs_queue_url || 'opendevops-agent-events'); } finally { setLoading(false); } }; + const finishWithoutInfra = async () => { + setLoading(true); setError(''); + try { + const r = await apiFetch('/api/init/skip-services', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ services: ['event-monitoring'] }), + }); + if (!r.ok) throw new Error('finish failed'); + navigate('/', { replace: true }); + } catch { + setError('Failed to finish setup'); + } finally { + setLoading(false); + } + }; + const requiredPassed = Object.entries(perms) .filter(([svc]) => SVC[svc]?.required) .every(([, r]) => r.passed); @@ -188,8 +220,8 @@ export default function InitPage() {

- - setUsername(e.target.value)} placeholder="admin" className={inp} /> + + setUsername(e.target.value)} placeholder="you@example.com" className={inp} />
@@ -199,7 +231,7 @@ export default function InitPage() { value={password} onChange={e => setPassword(e.target.value)} onKeyDown={e => e.key === 'Enter' && createUser()} - placeholder="At least 6 characters" + placeholder="At least 8 characters" className={inp + ' pr-11'} />