From 7f5c744c822e12f69fe515e769f32ac3e797ea4f Mon Sep 17 00:00:00 2001 From: Abhijeet Yadav Date: Wed, 15 Apr 2026 19:16:09 +0530 Subject: [PATCH 01/48] doc(readme): update team name --- README.md | 87 +------------------------------------------------------ 1 file changed, 1 insertion(+), 86 deletions(-) diff --git a/README.md b/README.md index c5c886b3e..bcefec4fd 100644 --- a/README.md +++ b/README.md @@ -1,86 +1 @@ -# HackToFuture 4.0 — Template - -Welcome to your official HackToFuture 4 repository. - -This repository template will be used for development, tracking progress, and final submission of your project. Ensure that all work is committed here within the allowed hackathon duration. - ---- - -### Instructions for the teams: - -- Fork the Repository and name the forked repo in this convention: hacktofuture4-team_id (for eg: hacktofuture4-A01) - ---- - -## Rules - -- Work must be done ONLY in the forked repository -- Only Four Contributors are allowed. -- After 36 hours, Please make PR to the Main Repository. A Form will be sent to fill the required information. -- Do not copy code from other teams -- All commits must be from individual GitHub accounts -- Please provide meaningful commits for tracking. -- Do not share your repository with other teams -- Final submission must be pushed before the deadline -- Any violation may lead to disqualification - ---- - -# The Final README Template - -## Problem Statement / Idea - -Clearly describe the problem you are solving. - -- What is the problem? -- Why is it important? -- Who are the target users? - ---- - -## Proposed Solution - -Explain your approach: - -- What are you building? -- How does it solve the problem? -- What makes your solution unique? - ---- - -## Features - -List the core features of your project: - -- Feature 1 -- Feature 2 -- Feature 3 - ---- - -## Tech Stack - -Mention all technologies used: - -- Frontend: -- Backend: -- Database: -- APIs / Services: -- Tools / Libraries: - ---- - -## Project Setup Instructions - -Provide clear steps to run your project: - -```bash -# Clone the repository -git clone - -# Install dependencies -... - -# Run the project -... -``` +# LiL KiDs -- D06 From 4ac3cbff29a57ef227e0d94c5dc8606853c9a706 Mon Sep 17 00:00:00 2001 From: abjt01 Date: Thu, 16 Apr 2026 01:19:04 +0530 Subject: [PATCH 02/48] Initial project setup with engine implementation and configuration files --- .env.example | 93 +++++++++++++++++++++++++++++++++++++++++ .gitignore | 61 +++++++++++++++++++++++++++ README.md | 15 +++++++ engine/main.py | 35 ++++++++++++++++ engine/requirements.txt | 0 5 files changed, 204 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 engine/main.py create mode 100644 engine/requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..45a52e445 --- /dev/null +++ b/.env.example @@ -0,0 +1,93 @@ +# ───────────────────────────────────────────────────────────────────────────── +# REKALL — Environment Variables +# Copy this file to .env and fill in your values. +# Never commit .env to version control. +# ───────────────────────────────────────────────────────────────────────────── + + +# ── AI / LLM (required) ─────────────────────────────────────────────────────── +# Groq — free tier at console.groq.com +# Used by all five agents and the RLM Zoom & Scan engine +GROQ_API_KEY=gsk_... + + +# ── PostgreSQL (required) ───────────────────────────────────────────────────── +# Docker Compose (uses service name "postgres"): +DATABASE_URL=postgresql://rekall:rekall@postgres:5432/rekall +# Local dev outside Docker: +# DATABASE_URL=postgresql://rekall:rekall@localhost:5432/rekall +# Neon / Supabase: +# DATABASE_URL=postgresql://user:password@host/dbname?sslmode=require + + +# ── ChromaDB (required) ─────────────────────────────────────────────────────── +# Defaults match the docker-compose chromadb service +CHROMADB_HOST=chromadb +CHROMADB_PORT=8000 + + +# ── GitHub (required for real PR creation and log fetching) ─────────────────── +# Personal access token — needs repo + workflow scopes +# Create at: github.com/settings/tokens → Fine-grained tokens +# Scopes needed: Contents (read/write), Pull Requests (read/write), Actions (read) +GITHUB_TOKEN=ghp_... + +# The repo REKALL will open PRs on (your sample-ci-sad repo or any monitored repo) +GITHUB_OWNER=your-github-username +GITHUB_REPO=sample-ci-sad + + +# ── Slack (optional) ────────────────────────────────────────────────────────── +# Fires a rich Block Kit message on every governance decision + incident outcome. +# Set SLACK_ENABLED=true once you have the webhook URL, otherwise leave false. +# +# Setup: +# 1. Go to api.slack.com/apps → Create New App → From scratch +# 2. Enable "Incoming Webhooks" and add to a channel +# 3. Copy the Webhook URL below +# 4. Set SLACK_ENABLED=true +# +SLACK_ENABLED=false +SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../... + + +# ── Notion (optional) ───────────────────────────────────────────────────────── +# Appends one row to a Notion database for every resolved/failed/rejected incident. +# Each row = structured incident log: type, source, fix tier, confidence, +# risk score, reward delta, reviewed by, notes, timestamp. +# Set NOTION_ENABLED=true once both token and database ID are set. +# +# Setup: +# 1. Go to notion.so/my-integrations → New integration +# - Give it read/write access to the database page +# - Copy the "Internal Integration Secret" below as NOTION_TOKEN +# 2. Create a database page in Notion with these properties: +# Incident ID — Title +# Status — Select (options: Resolved, Failed, Rejected) +# Failure Type — Select (test | deploy | infra | security | oom | unknown) +# Source — Select (github_actions | gitlab | simulator | ...) +# Fix Tier — Select (T1 Human Vault | T2 Synthetic Cache | T3 LLM Synthesis) +# Decision — Select (auto_apply | create_pr | block_await_human) +# Confidence — Number +# Risk Score — Number +# Reward Delta — Number +# Fix Description — Rich text +# Reviewed By — Rich text +# Notes — Rich text +# Resolved At — Date +# 3. Share the database with your integration (open database → Share → invite integration) +# 4. Copy the database ID from the URL: +# notion.so/your-workspace/?v=... +# It's the 32-char hex string before the "?" +# +NOTION_ENABLED=false +NOTION_TOKEN=secret_... +NOTION_DATABASE_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + +# ── CORS ────────────────────────────────────────────────────────────────────── +CORS_ORIGINS=http://localhost:3000 + + +# ── Frontend ────────────────────────────────────────────────────────────────── +NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..f77e6e3af --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# ───────────────────────────────────────────── +# REKALL — .gitignore +# ───────────────────────────────────────────── + +# Environment +.env +.env.local +.env.*.local + +# ── Go ─────────────────────────────────────── +backend/bin/ +backend/coverage.out +backend/coverage.html +*.exe +*.test + +# ── Python ─────────────────────────────────── +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ +.eggs/ +.venv/ +venv/ +env/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +htmlcov/ +.coverage +*.cover + +# ── Node / Next.js ─────────────────────────── +frontend/node_modules/ +frontend/.next/ +frontend/out/ +frontend/.vercel/ +frontend/playwright-report/ +frontend/test-results/ +frontend/coverage/ + +# ── ChromaDB ───────────────────────────────── +chroma_data/ +.chroma/ + +# ── Docker ─────────────────────────────────── +*.log + +# ── OS ─────────────────────────────────────── +.DS_Store +Thumbs.db + +# ── IDE ────────────────────────────────────── +.idea/ +.vscode/ +*.swp +*.swo diff --git a/README.md b/README.md index bcefec4fd..df80b9404 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ # LiL KiDs -- D06 + +## What is REKALL? + +Most CI/CD pipelines treat every failure as a fresh incident. An engineer reads the log, finds the runbook, applies the fix, moves on. No memory is built. The same Postgres connection error fires next month and the whole process repeats. + +REKALL breaks that cycle. It combines a **five-agent LangGraph pipeline** with a **tiered memory vault** that learns from every incident. When a failure occurs: + +1. The pipeline detects and diagnoses it automatically +2. The vault is searched for a matching fix (human-approved first, AI-cached second) +3. Risk is scored and the fix is auto-applied, queued as a PR, or escalated to a human +4. The outcome updates vault confidence — the system gets measurably better over time + +Every decision is streamed live to the dashboard. Every fix is auditable. The vault compounds. + +--- \ No newline at end of file diff --git a/engine/main.py b/engine/main.py new file mode 100644 index 000000000..a3173d6f9 --- /dev/null +++ b/engine/main.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import asyncio +import logging +import os +from contextlib import asynccontextmanager +from typing import Any, Dict, Optional + +import httpx +from fastapi import FastAPI, BackgroundTasks, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel +from pydantic_settings import BaseSettings + + +# ───────────────────────────────────────────── +# Config +# ───────────────────────────────────────────── + +class Settings(BaseSettings): + groq_api_key: str = "" + chromadb_host: str = "localhost" + chromadb_port: int = 8001 + go_backend_url: str = "http://localhost:8000" # callback target + log_level: str = "INFO" + + class Config: + env_file = "../../.env" + env_file_encoding = "utf-8" + extra = "ignore" + + +settings = Settings() +logging.basicConfig(level=settings.log_level) +log = logging.getLogger("rekall.engine") diff --git a/engine/requirements.txt b/engine/requirements.txt new file mode 100644 index 000000000..e69de29bb From 0176300f069b16520dbeb5338a21524a82609144 Mon Sep 17 00:00:00 2001 From: abjt01 Date: Thu, 16 Apr 2026 02:40:56 +0530 Subject: [PATCH 03/48] Update engine main module --- engine/main.py | 253 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/engine/main.py b/engine/main.py index a3173d6f9..6422b46eb 100644 --- a/engine/main.py +++ b/engine/main.py @@ -33,3 +33,256 @@ class Config: settings = Settings() logging.basicConfig(level=settings.log_level) log = logging.getLogger("rekall.engine") + + +# ───────────────────────────────────────────── +# App +# ───────────────────────────────────────────── + +@asynccontextmanager +async def lifespan(app: FastAPI): + log.info("Engine service starting up") + yield + log.info("Engine service shutting down") + + +app = FastAPI( + title="REKALL Engine Service", + description="AI agent pipeline — LangGraph + vault + RL", + version="1.0.0", + lifespan=lifespan, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + + +# ───────────────────────────────────────────── +# Request / response models +# ───────────────────────────────────────────── + +class PipelineRunRequest(BaseModel): + incident_id: str + payload: Dict[str, Any] + + +class PipelineLearnRequest(BaseModel): + incident_id: str + fix_proposal_id: str + result: str # success | failure | rejected + reviewed_by: str = "human" + notes: Optional[str] = None + fix_tier: Optional[str] = None # T1_human | T2_synthetic | T3_llm + vault_entry_id: Optional[str] = None # vault entry that was selected + + +class PipelineResponse(BaseModel): + ok: bool + message: str = "" + + +# ───────────────────────────────────────────── +# Endpoints +# ───────────────────────────────────────────── + +@app.get("/health") +async def health(): + return {"ok": True, "service": "rekall-engine"} + + +@app.post("/pipeline/run", response_model=PipelineResponse) +async def run_pipeline(req: PipelineRunRequest, background_tasks: BackgroundTasks): + """ + Start the agent pipeline for an incident. + Returns immediately; work runs in the background. + """ + background_tasks.add_task(_run_pipeline_async, req.incident_id, req.payload) + return PipelineResponse(ok=True, message="pipeline started") + + +@app.post("/pipeline/learn", response_model=PipelineResponse) +async def learn(req: PipelineLearnRequest): + """ + Submit an outcome so LearningAgent can update vault confidence. + """ + try: + await _run_learning( + req.incident_id, req.fix_proposal_id, req.result, + req.reviewed_by, req.notes, req.fix_tier, req.vault_entry_id, + ) + return PipelineResponse(ok=True, message="learning complete") + except NotImplementedError: + # rekall_engine agents are placeholders — acknowledged gracefully + return PipelineResponse(ok=True, message="learning placeholder (engine not yet implemented)") + except Exception as exc: + log.exception("learning failed: %s", exc) + raise HTTPException(status_code=500, detail=str(exc)) + + +# ───────────────────────────────────────────── +# Pipeline execution +# ───────────────────────────────────────────── + +async def _run_pipeline_async(incident_id: str, payload: Dict[str, Any]) -> None: + """ + Drive the rekall_engine pipeline and relay agent log events back to the + Go backend via its /internal/agent-log endpoint. + When rekall_engine agents are not yet implemented this falls back to a + stepped emulation that keeps the dashboard alive. + """ + log.info("Pipeline started for incident %s", incident_id) + + try: + # Use the real engine graph — run_pipeline returns final state dict + # and emits AgentLogEntry objects to a queue as it runs. + from rekall_engine.graph.orchestrator import run_pipeline # type: ignore + import asyncio as _asyncio + from rekall_engine.types import AgentLogEntry # type: ignore + + queue: _asyncio.Queue = _asyncio.Queue() + + # Run pipeline in background, draining the queue concurrently + pipeline_task = _asyncio.create_task( + run_pipeline(payload, incident_id, log_queue=queue) + ) + + # Drain log entries until sentinel (None) received + while True: + entry = await queue.get() + if entry is None: + break + if isinstance(entry, AgentLogEntry): + await _post_callback(incident_id, { + "type": "agent_log", + "data": { + "incident_id": entry.incident_id, + "step_name": entry.step_name, + "status": entry.status, + "detail": entry.detail, + }, + }) + + # Wait for pipeline to finish + final_state = await pipeline_task + + # Determine final status + gov = final_state.get("governance_decision") + if final_state.get("paused"): + final_status = "awaiting_approval" + else: + final_status = "resolved" + + await _post_callback(incident_id, { + "type": "status", + "data": { + "incident_id": incident_id, + "status": final_status, + "governance_decision": { + "risk_score": gov.risk_score if gov else 0.5, + "decision": gov.decision if gov else "block_await_human", + "risk_factors": gov.risk_factors if gov else [], + } if gov else None, + }, + }) + + except (NotImplementedError, ImportError): + log.warning("rekall_engine not yet implemented — running emulated pipeline") + await _emulated_pipeline(incident_id, payload) + except Exception as exc: + log.exception("pipeline error: %s", exc) + await _post_callback(incident_id, { + "type": "agent_log", + "data": { + "incident_id": incident_id, + "step_name": "error", + "status": "error", + "detail": str(exc), + }, + }) + + +async def _emulated_pipeline(incident_id: str, payload: Dict[str, Any]) -> None: + """ + Replays a realistic step-by-step timeline to the Go backend callback + when the real engine graph is not yet implemented. + """ + steps = [ + ("monitor", "Normalising failure event payload"), + ("diagnostic", "Fetching logs, git diff, and test reports"), + ("fix", "Searching memory vault: T1 → T2 → T3 fallback"), + ("governance", "Computing risk score across 6 dimensions"), + ("execute", "Applying fix / opening pull request"), + ("learning", "Updating vault confidence and logging RL episode"), + ] + + for step_name, detail in steps: + for status in ("running", "done"): + await _post_callback(incident_id, { + "type": "agent_log", + "data": { + "incident_id": incident_id, + "step_name": step_name, + "status": status, + "detail": detail, + }, + }) + if status == "running": + await asyncio.sleep(1.2) + + await _post_callback(incident_id, { + "type": "status", + "data": {"incident_id": incident_id, "status": "resolved"}, + }) + + +async def _run_learning( + incident_id: str, + fix_proposal_id: str, + result: str, + reviewed_by: str, + notes: Optional[str], + fix_tier: Optional[str] = None, + vault_entry_id: Optional[str] = None, +) -> None: + """ + Delegate to LearningAgent with properly typed Outcome and FixProposal. + """ + from rekall_engine.agents.learning import LearningAgent # type: ignore + from rekall_engine.types import Outcome, FixProposal # type: ignore + + outcome = Outcome( + incident_id=incident_id, + fix_proposal_id=fix_proposal_id, + result=result, # type: ignore[arg-type] + reviewed_by=reviewed_by, + notes=notes, + ) + fix = FixProposal( + incident_id=incident_id, + tier=fix_tier or "T3_llm", # type: ignore[arg-type] + vault_entry_id=vault_entry_id, + similarity_score=None, + fix_description="", + fix_commands=[], + fix_diff=None, + confidence=0.5, + ) + agent = LearningAgent() + await agent.run({"outcome": outcome, "fix_proposal": fix}) + + +async def _post_callback(incident_id: str, event: Dict[str, Any]) -> None: + """ + POST an event back to the Go backend's internal callback endpoint. + Failures are logged and swallowed — the pipeline continues regardless. + """ + url = f"{settings.go_backend_url}/internal/engine-callback" + try: + async with httpx.AsyncClient(timeout=5.0) as client: + await client.post(url, json=event) + except Exception as exc: + log.debug("callback failed (ok during dev): %s", exc) \ No newline at end of file From ac23675ddc038a3d38b9400c3e0f363722628d9f Mon Sep 17 00:00:00 2001 From: abjt01 Date: Thu, 16 Apr 2026 02:41:03 +0530 Subject: [PATCH 04/48] Add tests and pytest configuration for engine --- engine/pytest.ini | 0 engine/tests/conftest.py | 29 +++++++++++++ engine/tests/test_api.py | 65 +++++++++++++++++++++++++++++ engine/tests/test_confidence.py | 73 +++++++++++++++++++++++++++++++++ engine/tests/test_rewards.py | 45 ++++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 engine/pytest.ini create mode 100644 engine/tests/conftest.py create mode 100644 engine/tests/test_api.py create mode 100644 engine/tests/test_confidence.py create mode 100644 engine/tests/test_rewards.py diff --git a/engine/pytest.ini b/engine/pytest.ini new file mode 100644 index 000000000..e69de29bb diff --git a/engine/tests/conftest.py b/engine/tests/conftest.py new file mode 100644 index 000000000..36fdb27ec --- /dev/null +++ b/engine/tests/conftest.py @@ -0,0 +1,29 @@ +""" +Pytest configuration and shared fixtures for engine service tests. +""" + +import sys +import os +import pytest +from httpx import AsyncClient, ASGITransport + +# Make rekall_engine importable from the project root +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) + +# Set required env vars before importing the app +os.environ.setdefault("GROQ_API_KEY", "test-key") +os.environ.setdefault("GO_BACKEND_URL", "http://localhost:8000") + + +@pytest.fixture +def anyio_backend(): + return "asyncio" + + +@pytest.fixture +async def client(): + """Async HTTPX client wired directly to the FastAPI app (no network).""" + from engine.main import app # noqa: PLC0415 + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + yield ac diff --git a/engine/tests/test_api.py b/engine/tests/test_api.py new file mode 100644 index 000000000..a3cbddd44 --- /dev/null +++ b/engine/tests/test_api.py @@ -0,0 +1,65 @@ +""" +Integration tests for engine service API endpoints. +""" + +import pytest +from httpx import AsyncClient + + +pytestmark = pytest.mark.anyio + + +async def test_health(client: AsyncClient): + resp = await client.get("/health") + assert resp.status_code == 200 + data = resp.json() + assert data["ok"] is True + assert data["service"] == "rekall-engine" + + +async def test_run_pipeline_returns_immediately(client: AsyncClient): + """Pipeline should be accepted and run in the background.""" + resp = await client.post("/pipeline/run", json={ + "incident_id": "test-inc-001", + "payload": {"scenario": "postgres_refused", "simulated": True}, + }) + assert resp.status_code == 200 + data = resp.json() + assert data["ok"] is True + assert "started" in data["message"] + + +async def test_run_pipeline_missing_fields(client: AsyncClient): + """Missing required fields should return 422.""" + resp = await client.post("/pipeline/run", json={"incident_id": "x"}) + assert resp.status_code == 422 + + +async def test_learn_success(client: AsyncClient): + """Learn endpoint should accept valid outcomes.""" + resp = await client.post("/pipeline/learn", json={ + "incident_id": "test-inc-001", + "fix_proposal_id": "fp-abc", + "result": "success", + "reviewed_by": "engineer@rekall.io", + }) + assert resp.status_code == 200 + data = resp.json() + assert data["ok"] is True + + +async def test_learn_invalid_method(client: AsyncClient): + resp = await client.get("/pipeline/learn") + assert resp.status_code == 405 + + +async def test_learn_rejected_outcome(client: AsyncClient): + resp = await client.post("/pipeline/learn", json={ + "incident_id": "test-inc-002", + "fix_proposal_id": "fp-xyz", + "result": "rejected", + "reviewed_by": "senior-eng", + "notes": "Fix was too risky for production", + }) + assert resp.status_code == 200 + assert resp.json()["ok"] is True diff --git a/engine/tests/test_confidence.py b/engine/tests/test_confidence.py new file mode 100644 index 000000000..15b9e3f4c --- /dev/null +++ b/engine/tests/test_confidence.py @@ -0,0 +1,73 @@ +""" +Unit tests for the ConfidenceModel (decay and reward application). +""" + +import sys, os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) + +from datetime import datetime, timedelta +import pytest +from rekall_engine.rl.confidence import decay_confidence, apply_reward, MIN_CONFIDENCE, MAX_CONFIDENCE +from rekall_engine.types import VaultEntry + + +def _make_entry(confidence: float, days_old: int = 0) -> VaultEntry: + created = datetime.utcnow() - timedelta(days=days_old) + return VaultEntry( + id="test-id", + failure_signature="sig", + failure_type="infra", + fix_description="fix it", + fix_commands=[], + fix_diff=None, + confidence=confidence, + retrieval_count=0, + success_count=0, + source="human", + created_at=created, + ) + + +class TestDecayConfidence: + def test_fresh_entry_unchanged(self): + entry = _make_entry(confidence=0.9, days_old=0) + result = decay_confidence(entry) + assert result == pytest.approx(0.9, rel=1e-3) + + def test_older_entry_lower_confidence(self): + fresh = decay_confidence(_make_entry(0.9, days_old=0)) + old = decay_confidence(_make_entry(0.9, days_old=365)) + assert old < fresh + + def test_never_below_minimum(self): + entry = _make_entry(confidence=0.1, days_old=10_000) + result = decay_confidence(entry) + assert result >= MIN_CONFIDENCE + + def test_high_confidence_decays_toward_minimum(self): + entry = _make_entry(confidence=1.0, days_old=3000) + result = decay_confidence(entry) + assert result >= MIN_CONFIDENCE + assert result < 1.0 + + +class TestApplyReward: + def test_positive_reward_increases_confidence(self): + new_conf = apply_reward(0.7, 0.1) + assert new_conf > 0.7 + + def test_negative_reward_decreases_confidence(self): + new_conf = apply_reward(0.7, -0.2) + assert new_conf < 0.7 + + def test_clamps_at_maximum(self): + new_conf = apply_reward(0.99, 0.5) + assert new_conf == MAX_CONFIDENCE + + def test_clamps_at_minimum(self): + new_conf = apply_reward(0.15, -1.0) + assert new_conf == MIN_CONFIDENCE + + def test_zero_reward_unchanged(self): + new_conf = apply_reward(0.65, 0.0) + assert new_conf == pytest.approx(0.65) diff --git a/engine/tests/test_rewards.py b/engine/tests/test_rewards.py new file mode 100644 index 000000000..9fe9ce4c5 --- /dev/null +++ b/engine/tests/test_rewards.py @@ -0,0 +1,45 @@ +""" +Unit tests for the RL reward signal computation. +""" + +import sys, os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) + +import pytest +from rekall_engine.rl.rewards import compute_reward, REWARD_MAP + + +class TestComputeReward: + def test_success_tier1_returns_positive(self): + r = compute_reward("success", "T1_human") + assert r > 0, "successful T1 fix should earn positive reward" + + def test_success_tier3_highest_gain(self): + r_t1 = compute_reward("success", "T1_human") + r_t3 = compute_reward("success", "T3_llm") + assert r_t3 > r_t1, "T3 success should earn the most (new knowledge)" + + def test_failure_all_tiers_negative(self): + for tier in ("T1_human", "T2_synthetic", "T3_llm"): + assert compute_reward("failure", tier) < 0, f"failure/{tier} must be negative" + + def test_rejected_all_tiers_negative(self): + for tier in ("T1_human", "T2_synthetic", "T3_llm"): + assert compute_reward("rejected", tier) < 0 + + def test_failure_worse_than_rejected(self): + r_fail = compute_reward("failure", "T1_human") + r_rej = compute_reward("rejected", "T1_human") + assert r_fail < r_rej, "failure penalises more than rejection" + + def test_unknown_pair_returns_zero(self): + assert compute_reward("unknown", "T9_magic") == 0.0 + + def test_all_defined_pairs_are_reachable(self): + for (result, tier), expected in REWARD_MAP.items(): + got = compute_reward(result, tier) + assert got == expected, f"REWARD_MAP mismatch for ({result}, {tier})" + + def test_reward_values_within_sane_range(self): + for val in REWARD_MAP.values(): + assert -1.0 <= val <= 1.0, "rewards should be in [-1, 1]" From 2c1945801f3795765eed8476c2f9dca70e335c00 Mon Sep 17 00:00:00 2001 From: abjt01 Date: Thu, 16 Apr 2026 02:41:03 +0530 Subject: [PATCH 05/48] Initialize backend module with Go setup --- backend/go.mod | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 backend/go.mod diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 000000000..d4258b05f --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,3 @@ +module hacktofuture4-D06/backend + +go 1.25.3 From 2608a8a8d1869837c33da678c6771177df28ce69 Mon Sep 17 00:00:00 2001 From: abjt01 Date: Thu, 16 Apr 2026 10:58:36 +0530 Subject: [PATCH 06/48] Add frontend development configuration and dependencies --- backend/Dockerfile | 23 + backend/Makefile | 47 + backend/cmd/server/main.go | 136 + backend/internal/config/config.go | 82 + frontend/Dockerfile | 18 + .../components/agent-timeline.test.tsx | 59 + .../components/fix-proposal-card.test.tsx | 65 + .../components/incident-card.test.tsx | 71 + .../__tests__/components/risk-gauge.test.tsx | 55 + frontend/__tests__/lib/api-client.test.ts | 76 + frontend/app/(app)/dashboard/page.tsx | 360 + frontend/app/(app)/incidents/[id]/page.tsx | 305 + frontend/app/(app)/layout.tsx | 12 + frontend/app/(app)/rl-metrics/page.tsx | 80 + frontend/app/(app)/vault/page.tsx | 126 + frontend/app/globals.css | 404 + frontend/app/layout.tsx | 27 + frontend/app/page.tsx | 483 + frontend/components/agent-timeline.tsx | 167 + frontend/components/approval-panel.tsx | 163 + frontend/components/fix-proposal-card.tsx | 255 + frontend/components/incident-card.tsx | 138 + frontend/components/risk-gauge.tsx | 207 + frontend/components/rl-dashboard.tsx | 285 + frontend/components/sidebar.tsx | 201 + frontend/components/theme-provider.tsx | 55 + frontend/components/theme-toggle.tsx | 51 + frontend/components/ui/badge.tsx | 37 + frontend/components/ui/skeleton.tsx | 48 + frontend/components/ui/stat-card.tsx | 95 + frontend/components/vault-explorer.tsx | 228 + frontend/e2e/dashboard.spec.ts | 29 + frontend/e2e/rl-metrics.spec.ts | 13 + frontend/e2e/vault.spec.ts | 23 + frontend/jest.config.ts | 22 + frontend/jest.setup.ts | 1 + frontend/lib/api-client.ts | 49 + frontend/lib/hooks/use-agent-stream.ts | 68 + frontend/lib/hooks/use-incidents.ts | 31 + frontend/lib/types.ts | 127 + frontend/lib/utils.ts | 26 + frontend/next-env.d.ts | 6 + frontend/next.config.ts | 15 + frontend/package-lock.json | 8039 +++++++++++++++++ frontend/package.json | 45 + frontend/playwright.config.ts | 36 + frontend/postcss.config.js | 6 + frontend/tailwind.config.ts | 108 + frontend/tsconfig.json | 40 + frontend/tsconfig.tsbuildinfo | 1 + 50 files changed, 13044 insertions(+) create mode 100644 backend/Dockerfile create mode 100644 backend/Makefile create mode 100644 backend/cmd/server/main.go create mode 100644 backend/internal/config/config.go create mode 100644 frontend/Dockerfile create mode 100644 frontend/__tests__/components/agent-timeline.test.tsx create mode 100644 frontend/__tests__/components/fix-proposal-card.test.tsx create mode 100644 frontend/__tests__/components/incident-card.test.tsx create mode 100644 frontend/__tests__/components/risk-gauge.test.tsx create mode 100644 frontend/__tests__/lib/api-client.test.ts create mode 100644 frontend/app/(app)/dashboard/page.tsx create mode 100644 frontend/app/(app)/incidents/[id]/page.tsx create mode 100644 frontend/app/(app)/layout.tsx create mode 100644 frontend/app/(app)/rl-metrics/page.tsx create mode 100644 frontend/app/(app)/vault/page.tsx create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/components/agent-timeline.tsx create mode 100644 frontend/components/approval-panel.tsx create mode 100644 frontend/components/fix-proposal-card.tsx create mode 100644 frontend/components/incident-card.tsx create mode 100644 frontend/components/risk-gauge.tsx create mode 100644 frontend/components/rl-dashboard.tsx create mode 100644 frontend/components/sidebar.tsx create mode 100644 frontend/components/theme-provider.tsx create mode 100644 frontend/components/theme-toggle.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/skeleton.tsx create mode 100644 frontend/components/ui/stat-card.tsx create mode 100644 frontend/components/vault-explorer.tsx create mode 100644 frontend/e2e/dashboard.spec.ts create mode 100644 frontend/e2e/rl-metrics.spec.ts create mode 100644 frontend/e2e/vault.spec.ts create mode 100644 frontend/jest.config.ts create mode 100644 frontend/jest.setup.ts create mode 100644 frontend/lib/api-client.ts create mode 100644 frontend/lib/hooks/use-agent-stream.ts create mode 100644 frontend/lib/hooks/use-incidents.ts create mode 100644 frontend/lib/types.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/postcss.config.js create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.tsbuildinfo diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..0c67c48d7 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,23 @@ +# ── Build stage ────────────────────────────────────────────────────────────── +FROM golang:1.22-alpine AS builder + +RUN apk add --no-cache git ca-certificates + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /rekall-backend ./cmd/server + +# ── Runtime stage ───────────────────────────────────────────────────────────── +FROM alpine:3.20 + +RUN apk add --no-cache ca-certificates wget + +COPY --from=builder /rekall-backend /rekall-backend + +EXPOSE 8000 + +ENTRYPOINT ["/rekall-backend"] diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 000000000..665ff50f6 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,47 @@ +.PHONY: build run test lint clean tidy + +BIN := rekall-backend +CMD := ./cmd/server +GOFLAGS := -ldflags="-s -w" + +## build: compile the binary +build: + go build $(GOFLAGS) -o bin/$(BIN) $(CMD) + +## run: run in development mode (auto-reload not included; use air externally) +run: + GIN_MODE=debug go run $(CMD)/main.go + +## test: run all tests with race detector +test: + go test -race -count=1 ./... + +## test-verbose: run tests with verbose output +test-verbose: + go test -race -v -count=1 ./... + +## test-cover: run tests and produce a coverage report +test-cover: + go test -race -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report: coverage.html" + +## lint: run golangci-lint (must be installed separately) +lint: + golangci-lint run ./... + +## tidy: tidy and verify go modules +tidy: + go mod tidy + go mod verify + +## clean: remove build artifacts +clean: + rm -rf bin/ coverage.out coverage.html + +## docker-build: build the production Docker image +docker-build: + docker build -t rekall-backend:latest . + +help: + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## //' diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 000000000..dac859030 --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,136 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + "github.com/rekall/backend/internal/config" + "github.com/rekall/backend/internal/db" + "github.com/rekall/backend/internal/engine" + "github.com/rekall/backend/internal/handlers" + "github.com/rekall/backend/internal/middleware" + "github.com/rekall/backend/internal/sse" +) + +func main() { + // Load .env from repo root (best-effort; production uses real env vars) + _ = godotenv.Load("../.env") + _ = godotenv.Load(".env") // also try CWD for flexibility + + cfg := config.Load() + gin.SetMode(cfg.GinMode) + + // ── Database ────────────────────────────────────────────────────────── + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := db.Connect(ctx, cfg.DatabaseURL); err != nil { + log.Fatalf("database connect: %v", err) + } + defer db.Close() + log.Printf("[REKALL] connected to database") + + // ── SSE broker ──────────────────────────────────────────────────────── + broker := sse.NewBroker() + + // ── Engine client ───────────────────────────────────────────────────── + eng := engine.NewClient(cfg.EngineURL) + + // ── Handlers ────────────────────────────────────────────────────────── + webhookHandler := handlers.NewWebhookHandler(broker, eng) + approvalHandler := handlers.NewApprovalHandler(eng) + streamHandler := handlers.NewStreamHandler(broker) + callbackHandler := handlers.NewCallbackHandler(broker) + + // ── Router ──────────────────────────────────────────────────────────── + r := gin.New() + r.Use(gin.Recovery()) + r.Use(middleware.Logger()) + r.Use(middleware.CORS(cfg.CORSOrigins)) + + // Health + r.GET("/health", func(c *gin.Context) { + engineUp := eng.Healthy(c.Request.Context()) + c.JSON(http.StatusOK, gin.H{ + "status": "ok", + "service": "rekall-backend", + "engine": engineUp, + }) + }) + + // Webhooks + wh := r.Group("/webhook") + { + wh.POST("/github", webhookHandler.HandleGitHub) + wh.POST("/gitlab", webhookHandler.HandleGitLab) + wh.POST("/simulate", webhookHandler.HandleSimulate) + } + + // Incidents + inc := r.Group("/incidents") + { + inc.GET("", handlers.ListIncidents) + inc.GET("/:id", handlers.GetIncident) + inc.POST("/:id/approve", approvalHandler.Approve) + inc.POST("/:id/reject", approvalHandler.Reject) + } + + // SSE stream + r.GET("/stream/:id", streamHandler.Stream) + + // Vault + v := r.Group("/vault") + { + v.GET("", handlers.ListVault) + v.GET("/stats", handlers.VaultStats) + } + + // Metrics + m := r.Group("/metrics") + { + m.GET("/summary", handlers.Summary) + m.GET("/rl", handlers.RLEpisodes) + } + + // Internal — called by the Python engine service only, not exposed to frontend + internal := r.Group("/internal") + { + internal.POST("/engine-callback", callbackHandler.Handle) + } + + // ── HTTP server with graceful shutdown ─────────────────────────────── + srv := &http.Server{ + Addr: ":" + cfg.Port, + Handler: r, + ReadTimeout: 10 * time.Second, + WriteTimeout: 0, // 0 = no timeout for SSE streams + IdleTimeout: 120 * time.Second, + } + + go func() { + log.Printf("[REKALL] listening on :%s (mode=%s)", cfg.Port, cfg.GinMode) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %v", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("[REKALL] shutting down…") + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Fatalf("shutdown: %v", err) + } + log.Println("[REKALL] stopped") +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 000000000..8580faae3 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,82 @@ +package config + +import ( + "fmt" + "os" + "strconv" +) + +// Config holds all runtime configuration loaded from environment variables. +type Config struct { + // Server + Port string + GinMode string + + // PostgreSQL + DatabaseURL string + + // Python engine service + EngineURL string + + // CORS + CORSOrigins []string + + // ChromaDB (forwarded to engine) + ChromaDBHost string + ChromaDBPort int +} + +// Load reads environment variables and returns a populated Config. +// Missing required variables cause a fatal error at startup. +func Load() *Config { + port := getEnv("PORT", "8000") + dbURL := mustGetEnv("DATABASE_URL") + engineURL := getEnv("ENGINE_URL", "http://localhost:8002") + corsOrigins := getEnvSlice("CORS_ORIGINS", []string{"http://localhost:3000"}) + chromaPort, _ := strconv.Atoi(getEnv("CHROMADB_PORT", "8001")) + + return &Config{ + Port: port, + GinMode: getEnv("GIN_MODE", "debug"), + DatabaseURL: dbURL, + EngineURL: engineURL, + CORSOrigins: corsOrigins, + ChromaDBHost: getEnv("CHROMADB_HOST", "localhost"), + ChromaDBPort: chromaPort, + } +} + +func getEnv(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +func mustGetEnv(key string) string { + v := os.Getenv(key) + if v == "" { + panic(fmt.Sprintf("required environment variable %q is not set", key)) + } + return v +} + +func getEnvSlice(key string, fallback []string) []string { + v := os.Getenv(key) + if v == "" { + return fallback + } + // comma-separated + result := make([]string, 0) + start := 0 + for i := 0; i <= len(v); i++ { + if i == len(v) || v[i] == ',' { + part := v[start:i] + if part != "" { + result = append(result, part) + } + start = i + 1 + } + } + return result +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..1bc2693d5 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json ./ +RUN npm install + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV production +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/frontend/__tests__/components/agent-timeline.test.tsx b/frontend/__tests__/components/agent-timeline.test.tsx new file mode 100644 index 000000000..d01a0a1f0 --- /dev/null +++ b/frontend/__tests__/components/agent-timeline.test.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { AgentTimeline } from "@/components/agent-timeline"; +import type { AgentLog } from "@/lib/types"; + +const makeLog = (step: string, status: "running" | "done" | "error", detail = "detail"): AgentLog => ({ + id: `${step}-${status}`, + incident_id: "inc-001", + step_name: step, + status, + detail, + created_at: new Date().toISOString(), +}); + +describe("AgentTimeline", () => { + it("renders all six pipeline steps", () => { + render(); + expect(screen.getByText(/Monitor/)).toBeInTheDocument(); + expect(screen.getByText(/Diagnostic/)).toBeInTheDocument(); + expect(screen.getByText(/Fix/)).toBeInTheDocument(); + expect(screen.getByText(/Governance/)).toBeInTheDocument(); + expect(screen.getByText(/Execute/)).toBeInTheDocument(); + expect(screen.getByText(/Learning/)).toBeInTheDocument(); + }); + + it("shows done state for completed steps", () => { + const logs = [makeLog("monitor", "done", "Normalised payload")]; + render(); + // Detail shows for done steps + expect(screen.getByText("Normalised payload")).toBeInTheDocument(); + }); + + it("shows pipeline complete when done is true", () => { + render(); + expect(screen.getByText("Pipeline complete")).toBeInTheDocument(); + }); + + it("does not show pipeline complete when done is false", () => { + render(); + expect(screen.queryByText("Pipeline complete")).not.toBeInTheDocument(); + }); + + it("deduplicates logs — uses latest status per step", () => { + const logs = [ + makeLog("monitor", "running", "Starting"), + makeLog("monitor", "done", "Finished"), + ]; + render(); + // Only the last detail should appear + expect(screen.getByText("Finished")).toBeInTheDocument(); + expect(screen.queryByText("Starting")).not.toBeInTheDocument(); + }); + + it("renders error detail for errored step", () => { + const logs = [makeLog("diagnostic", "error", "GitHub API unreachable")]; + render(); + expect(screen.getByText("GitHub API unreachable")).toBeInTheDocument(); + }); +}); diff --git a/frontend/__tests__/components/fix-proposal-card.test.tsx b/frontend/__tests__/components/fix-proposal-card.test.tsx new file mode 100644 index 000000000..adff0351b --- /dev/null +++ b/frontend/__tests__/components/fix-proposal-card.test.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { FixProposalCard } from "@/components/fix-proposal-card"; +import type { FixProposal } from "@/lib/types"; + +const BASE_FIX: FixProposal = { + id: "fp-001", + incident_id: "inc-001", + tier: "T1_human", + vault_entry_id: "vault-abc", + similarity_score: 0.91, + fix_description: "Restore correct Postgres host in database.yml", + fix_commands: ["git checkout -- config/database.yml", "systemctl restart app"], + fix_diff: null, + confidence: 0.92, + created_at: new Date().toISOString(), +}; + +describe("FixProposalCard", () => { + it("renders T1 tier badge", () => { + render(); + expect(screen.getByText(/T1 — Human Vault/)).toBeInTheDocument(); + }); + + it("renders T2 tier badge", () => { + render(); + expect(screen.getByText(/T2 — Synthetic Cache/)).toBeInTheDocument(); + }); + + it("renders T3 tier badge", () => { + render(); + expect(screen.getByText(/T3 — LLM Synthesis/)).toBeInTheDocument(); + }); + + it("renders fix description", () => { + render(); + expect(screen.getByText("Restore correct Postgres host in database.yml")).toBeInTheDocument(); + }); + + it("renders all fix commands", () => { + render(); + expect(screen.getByText("git checkout -- config/database.yml")).toBeInTheDocument(); + expect(screen.getByText("systemctl restart app")).toBeInTheDocument(); + }); + + it("shows confidence percentage", () => { + render(); + expect(screen.getByText("92%")).toBeInTheDocument(); + }); + + it("shows similarity score when present", () => { + render(); + expect(screen.getByText(/91\.0%/)).toBeInTheDocument(); + }); + + it("hides similarity score when null", () => { + render(); + expect(screen.queryByText(/Similarity/)).not.toBeInTheDocument(); + }); + + it("renders diff toggle when diff present", () => { + render(); + expect(screen.getByText("View diff")).toBeInTheDocument(); + }); +}); diff --git a/frontend/__tests__/components/incident-card.test.tsx b/frontend/__tests__/components/incident-card.test.tsx new file mode 100644 index 000000000..0194c10db --- /dev/null +++ b/frontend/__tests__/components/incident-card.test.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { IncidentCard } from "@/components/incident-card"; +import type { Incident } from "@/lib/types"; + +// Next/link needs a router — mock it +jest.mock("next/link", () => { + const MockLink = ({ children, href }: { children: React.ReactNode; href: string }) => ( + {children} + ); + MockLink.displayName = "MockLink"; + return MockLink; +}); + +const BASE_INCIDENT: Incident = { + id: "550e8400-e29b-41d4-a716-446655440000", + source: "simulator", + failure_type: "infra", + raw_payload: { description: "Postgres connection refused" }, + status: "processing", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), +}; + +describe("IncidentCard", () => { + it("renders the failure type badge", () => { + render(); + expect(screen.getByText("INFRA")).toBeInTheDocument(); + }); + + it("renders the source label", () => { + render(); + expect(screen.getByText("simulator")).toBeInTheDocument(); + }); + + it("renders the description from raw_payload", () => { + render(); + expect(screen.getByText("Postgres connection refused")).toBeInTheDocument(); + }); + + it("renders the status label", () => { + render(); + expect(screen.getByText("Processing")).toBeInTheDocument(); + }); + + it("links to the correct incident detail URL", () => { + render(); + const link = screen.getByRole("link"); + expect(link).toHaveAttribute("href", `/incidents/${BASE_INCIDENT.id}`); + }); + + it("renders resolved status correctly", () => { + render(); + expect(screen.getByText("Resolved")).toBeInTheDocument(); + }); + + it("renders failed status correctly", () => { + render(); + expect(screen.getByText("Failed")).toBeInTheDocument(); + }); + + it("renders awaiting_approval status", () => { + render(); + expect(screen.getByText("Awaiting Approval")).toBeInTheDocument(); + }); + + it("falls back to truncated ID when description missing", () => { + render(); + expect(screen.getByText(/Incident 550e8400/)).toBeInTheDocument(); + }); +}); diff --git a/frontend/__tests__/components/risk-gauge.test.tsx b/frontend/__tests__/components/risk-gauge.test.tsx new file mode 100644 index 000000000..3815b8738 --- /dev/null +++ b/frontend/__tests__/components/risk-gauge.test.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { RiskGauge } from "@/components/risk-gauge"; +import type { GovernanceDecision } from "@/lib/types"; + +const makeDecision = ( + risk_score: number, + decision: GovernanceDecision["decision"], + factors: string[] = [], +): GovernanceDecision => ({ + id: "gov-001", + incident_id: "inc-001", + risk_score, + decision, + risk_factors: factors, + created_at: new Date().toISOString(), +}); + +describe("RiskGauge", () => { + it("shows Low Risk for score < 0.3", () => { + render(); + expect(screen.getByText("Low Risk")).toBeInTheDocument(); + }); + + it("shows Medium Risk for score 0.3–0.7", () => { + render(); + expect(screen.getByText("Medium Risk")).toBeInTheDocument(); + }); + + it("shows High Risk for score >= 0.7", () => { + render(); + expect(screen.getByText("High Risk")).toBeInTheDocument(); + }); + + it("shows percentage on gauge", () => { + render(); + expect(screen.getByText("72%")).toBeInTheDocument(); + }); + + it("renders decision label", () => { + render(); + expect(screen.getByText("Auto-Apply")).toBeInTheDocument(); + }); + + it("renders risk factors", () => { + render(); + expect(screen.getByText("touches_secrets")).toBeInTheDocument(); + expect(screen.getByText("llm_generated")).toBeInTheDocument(); + }); + + it("does not render factors section when empty", () => { + render(); + expect(screen.queryByText("Factors")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/__tests__/lib/api-client.test.ts b/frontend/__tests__/lib/api-client.test.ts new file mode 100644 index 000000000..ef276bc0a --- /dev/null +++ b/frontend/__tests__/lib/api-client.test.ts @@ -0,0 +1,76 @@ +/** + * Tests for the API client — uses MSW (mock service worker) patterns + * but kept lightweight here with jest fetch mocks. + */ + +// Polyfill fetch for Node test environment +global.fetch = jest.fn(); + +const mockFetch = global.fetch as jest.Mock; + +// Reset mock between tests +beforeEach(() => { + mockFetch.mockClear(); +}); + +describe("api client URL construction", () => { + it("simulate posts to correct path", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ incident_id: "new-inc-123" }), + }); + + const { api } = await import("@/lib/api-client"); + const result = await api.simulate("postgres_refused"); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const [url] = mockFetch.mock.calls[0]; + expect(url).toContain("/webhook/simulate"); + expect(result).toEqual({ incident_id: "new-inc-123" }); + }); + + it("getIncident calls correct path", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ incident: { id: "abc-123" } }), + }); + + const { api } = await import("@/lib/api-client"); + await api.getIncident("abc-123"); + + const [url] = mockFetch.mock.calls[0]; + expect(url).toContain("/incidents/abc-123"); + }); + + it("streamUrl returns correct SSE URL", async () => { + const { api } = await import("@/lib/api-client"); + const url = api.streamUrl("inc-xyz"); + expect(url).toContain("/stream/inc-xyz"); + }); + + it("throws on non-ok response", async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + json: async () => ({}), + }); + + const { api } = await import("@/lib/api-client"); + await expect(api.getIncident("bad-id")).rejects.toThrow("404"); + }); + + it("approveIncident posts correct body", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ ok: true }), + }); + + const { api } = await import("@/lib/api-client"); + await api.approveIncident("inc-001", "alice", "looks good"); + + const [, init] = mockFetch.mock.calls[0]; + const body = JSON.parse(init.body as string); + expect(body.reviewed_by).toBe("alice"); + expect(body.notes).toBe("looks good"); + }); +}); diff --git a/frontend/app/(app)/dashboard/page.tsx b/frontend/app/(app)/dashboard/page.tsx new file mode 100644 index 000000000..d9cf55055 --- /dev/null +++ b/frontend/app/(app)/dashboard/page.tsx @@ -0,0 +1,360 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { + Loader2, Play, AlertCircle, RefreshCw, + Activity, CheckCircle2, Database, TrendingUp, + Server, Cpu, TestTube, Shield, Container, Radio, + Zap, Terminal, +} from "lucide-react"; +import { useIncidents } from "@/lib/hooks/use-incidents"; +import { IncidentCard } from "@/components/incident-card"; +import { SkeletonCard } from "@/components/ui/skeleton"; +import { StatCard } from "@/components/ui/stat-card"; +import { api } from "@/lib/api-client"; +import { cn } from "@/lib/utils"; + +const SCENARIOS = [ + { + id: "postgres_refused", + label: "Postgres Refused", + type: "infra", + icon: Server, + color: "text-amber-400", + accent: "hsl(38 92% 50%)", + desc: "Connection refused on port 5432", + }, + { + id: "oom_kill", + label: "OOM Kill", + type: "oom", + icon: Cpu, + color: "text-red-400", + accent: "hsl(0 72% 51%)", + desc: "Container killed, exit code 137", + }, + { + id: "test_failure", + label: "Test Failure", + type: "test", + icon: TestTube, + color: "text-blue-400", + accent: "hsl(217 91% 60%)", + desc: "3 assertions failed in test suite", + }, + { + id: "secret_leak", + label: "Secret Leak", + type: "security", + icon: Shield, + color: "text-rose-400", + accent: "hsl(346 77% 50%)", + desc: "API key detected in commit diff", + }, + { + id: "image_pull_backoff", + label: "Image Pull Backoff", + type: "deploy", + icon: Container, + color: "text-sky-400", + accent: "hsl(199 89% 54%)", + desc: "ImagePullBackOff on registry pull", + }, +]; + +export default function DashboardPage() { + const { incidents, loading, error, refetch } = useIncidents(4000); + const [simulating, setSimulating] = useState(null); + const router = useRouter(); + + async function simulate(scenario: string) { + setSimulating(scenario); + try { + const result = await api.simulate(scenario); + refetch(); + router.push(`/incidents/${result.incident_id}`); + } catch { + // silently fail + } finally { + setSimulating(null); + } + } + + const activeCount = incidents.filter(i => i.status === "processing" || i.status === "awaiting_approval").length; + const resolvedCount = incidents.filter(i => i.status === "resolved").length; + const failedCount = incidents.filter(i => i.status === "failed").length; + const successRate = incidents.length > 0 ? Math.round(resolvedCount / incidents.length * 100) : 0; + + return ( +
+ + {/* ── Sticky header ───────────────────────────────────────── */} +
+
+
+
+ +
+
+

+ Live Dashboard +

+

+ Real-time CI/CD failure detection & repair +

+
+
+ +
+
+ +
+ + {/* ── Stats row ───────────────────────────────────────────── */} +
+ } + accent="hsl(217 91% 60%)" + /> + 0 ? "In pipeline…" : "All clear"} + trend={activeCount > 0 ? "up" : "neutral"} + icon={} + accent="hsl(38 92% 50%)" + /> + 0 ? `${successRate}% success rate` : undefined} + trend="up" + icon={} + accent="hsl(142 69% 42%)" + /> + } + accent="hsl(0 72% 51%)" + /> +
+ + {/* ── Failure Simulator ───────────────────────────────────── */} +
+ {/* Header */} +
+
+ +
+
+

Failure Simulator

+

+ Inject a real-looking CI/CD incident to demo the repair pipeline +

+
+
+ + + Demo Mode + +
+
+ +
+
+ {SCENARIOS.map((s) => { + const busy = simulating === s.id; + const Icon = s.icon; + return ( + + ); + })} +
+
+
+ + {/* ── Incidents ───────────────────────────────────────────── */} +
+
+
+

+ Recent Incidents +

+ {incidents.length > 0 && ( + + {incidents.length} + + )} +
+ {loading && incidents.length > 0 && ( + + )} +
+ + {error && ( +
+ + {error} — is the backend running? +
+ )} + + {loading && !incidents.length ? ( +
+ {Array.from({ length: 4 }).map((_, i) => )} +
+ ) : incidents.length === 0 ? ( +
+
+ +
+

No incidents yet

+

+ Fire a scenario above to watch the REKALL pipeline animate in real-time +

+
+ ) : ( +
+ {incidents.map((inc, i) => ( +
+ +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/frontend/app/(app)/incidents/[id]/page.tsx b/frontend/app/(app)/incidents/[id]/page.tsx new file mode 100644 index 000000000..8ef40d25a --- /dev/null +++ b/frontend/app/(app)/incidents/[id]/page.tsx @@ -0,0 +1,305 @@ +"use client"; + +import { use, useEffect, useState, useCallback } from "react"; +import { + ArrowLeft, Loader2, AlertCircle, Clock, + CheckCircle2, XCircle, FileText, GitCommit, Cpu, +} from "lucide-react"; +import Link from "next/link"; +import { api } from "@/lib/api-client"; +import { useAgentStream } from "@/lib/hooks/use-agent-stream"; +import { AgentTimeline } from "@/components/agent-timeline"; +import { FixProposalCard } from "@/components/fix-proposal-card"; +import { RiskGauge } from "@/components/risk-gauge"; +import { ApprovalPanel } from "@/components/approval-panel"; +import { Badge } from "@/components/ui/badge"; +import { SkeletonTimeline } from "@/components/ui/skeleton"; +import { cn, timeAgo } from "@/lib/utils"; +import type { + Incident, DiagnosticBundle, FixProposal, + GovernanceDecision, AgentLog, +} from "@/lib/types"; + +interface Detail { + incident: Incident; + diagnostic_bundle: DiagnosticBundle | null; + fix_proposal: FixProposal | null; + governance_decision: GovernanceDecision | null; + agent_logs: AgentLog[]; +} + +const STATUS_CONFIG = { + processing: { icon: Loader2, variant: "warning" as const, label: "Processing", spin: true, color: "hsl(38 92% 50%)" }, + awaiting_approval: { icon: Clock, variant: "info" as const, label: "Awaiting Approval", spin: false, color: "hsl(199 89% 54%)" }, + resolved: { icon: CheckCircle2, variant: "success" as const, label: "Resolved", spin: false, color: "hsl(142 69% 42%)" }, + failed: { icon: XCircle, variant: "danger" as const, label: "Failed", spin: false, color: "hsl(0 72% 51%)" }, +}; + +function SectionCard({ + title, + icon: Icon, + accent, + children, + className, +}: { + title: string; + icon?: React.ComponentType<{ className?: string }>; + accent?: string; + children: React.ReactNode; + className?: string; +}) { + return ( +
+
+ {Icon && ( + + + + )} +

+ {title} +

+
+ {children} +
+ ); +} + +export default function IncidentDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = use(params); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const { logs, done } = useAgentStream(id); + + const fetchData = useCallback(async () => { + try { + const result = await api.getIncident(id); + setData(result as unknown as Detail); + } finally { + setLoading(false); + } + }, [id]); + + useEffect(() => { fetchData(); }, [fetchData]); + useEffect(() => { if (done) fetchData(); }, [done, fetchData]); + + if (loading) { + return ( +
+
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ +
+ ))} +
+
+ ); + } + + if (!data) { + return ( +
+
+ + Incident not found. +
+
+ ); + } + + const { incident, diagnostic_bundle: bundle, fix_proposal: fix, governance_decision: gov } = data; + const allLogs = logs.length > 0 ? logs : data.agent_logs; + const needsApproval = incident.status === "awaiting_approval" || gov?.decision === "block_await_human"; + const s = STATUS_CONFIG[incident.status] ?? STATUS_CONFIG.processing; + const StatusIcon = s.icon; + + return ( +
+ {/* ── Page header ──────────────────────────────────────────── */} +
+
+ + + + +
+

+ Incident{" "} + {id.slice(0, 8)}… +

+ + + {s.label} + +
+ +

+ {incident.source} · {incident.failure_type} · {timeAgo(incident.created_at)} +

+
+
+ + {/* ── 3-col grid ──────────────────────────────────────────── */} +
+
+ + {/* ── Col 1: Agent timeline ─────────────────────────── */} + +
+ +
+
+ + {/* ── Col 2: Diagnostic + fix ───────────────────────── */} +
+ {/* Log excerpt */} + {bundle?.log_excerpt && ( + +
+                  {bundle.log_excerpt}
+                
+
+ )} + + {/* Git diff */} + {bundle?.git_diff && ( + +
+                  {bundle.git_diff}
+                
+
+ )} + + {/* Context summary */} + {bundle?.context_summary && ( +
+

+ Context Summary +

+

+ {bundle.context_summary} +

+
+ )} + + {/* Fix proposal */} + {fix ? ( + + ) : ( +
+ {incident.status === "processing" ? ( + <> +
+ +
+

Searching memory vault…

+

Running tiered retrieval T1 → T2 → T3

+ + ) : ( +

No fix proposal generated

+ )} +
+ )} +
+ + {/* ── Col 3: Governance + approval ─────────────────── */} +
+ {gov && } + + {needsApproval && ( + + )} + + {incident.status === "resolved" && !needsApproval && ( +
+
+ +
+

Resolved

+

Vault confidence updated

+
+ )} + + {incident.status === "failed" && ( +
+
+ +
+

Failed / Rejected

+

Vault confidence decayed

+
+ )} +
+
+
+
+ ); +} diff --git a/frontend/app/(app)/layout.tsx b/frontend/app/(app)/layout.tsx new file mode 100644 index 000000000..1576d1d86 --- /dev/null +++ b/frontend/app/(app)/layout.tsx @@ -0,0 +1,12 @@ +import { Sidebar } from "@/components/sidebar"; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return ( +
+ +
+ {children} +
+
+ ); +} diff --git a/frontend/app/(app)/rl-metrics/page.tsx b/frontend/app/(app)/rl-metrics/page.tsx new file mode 100644 index 000000000..d61d2d8e5 --- /dev/null +++ b/frontend/app/(app)/rl-metrics/page.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2, BarChart3, TrendingUp } from "lucide-react"; +import { api } from "@/lib/api-client"; +import { RLDashboard } from "@/components/rl-dashboard"; +import { SkeletonStats } from "@/components/ui/skeleton"; +import type { RLEpisode, MetricsSummary } from "@/lib/types"; + +export default function RLMetricsPage() { + const [episodes, setEpisodes] = useState([]); + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + Promise.all([api.rlEpisodes(), api.summary()]) + .then(([ep, sum]) => { + setEpisodes((ep as { episodes: RLEpisode[] }).episodes); + setSummary(sum as unknown as MetricsSummary); + }) + .finally(() => setLoading(false)); + }, []); + + return ( +
+ {/* ── Header ────────────────────────────────────────────── */} +
+
+
+
+ +
+
+

+ RL Metrics +

+

+ Feedback-driven confidence system — vault performance and reward history +

+
+
+ + + Live + +
+
+
+
+ +
+ {loading ? ( +
+ +
+
+
+ ) : ( + + )} +
+
+ ); +} diff --git a/frontend/app/(app)/vault/page.tsx b/frontend/app/(app)/vault/page.tsx new file mode 100644 index 000000000..b2286b1d2 --- /dev/null +++ b/frontend/app/(app)/vault/page.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2, Database, Shield, Cpu, TrendingUp, Lock } from "lucide-react"; +import { api } from "@/lib/api-client"; +import { VaultExplorer } from "@/components/vault-explorer"; +import { StatCard } from "@/components/ui/stat-card"; +import { SkeletonStats } from "@/components/ui/skeleton"; +import type { VaultEntry } from "@/lib/types"; + +interface VaultStats { + total: number; + human_count: number; + synthetic_count: number; + avg_confidence: number | null; +} + +export default function VaultPage() { + const [entries, setEntries] = useState([]); + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + Promise.all([api.listVault(), api.vaultStats()]) + .then(([vault, st]) => { + setEntries((vault as { entries: VaultEntry[] }).entries); + setStats(st as unknown as VaultStats); + }) + .finally(() => setLoading(false)); + }, []); + + return ( +
+ {/* ── Header ──────────────────────────────────────────────── */} +
+
+
+
+ +
+
+

+ Memory Vault +

+

+ Human-approved and AI-validated fixes powering tiered retrieval +

+
+
+ + + RLM Active + +
+
+
+
+ +
+ {/* ── Stats ─────────────────────────────────────────────── */} + {loading ? ( + + ) : stats ? ( +
+ } + accent="hsl(217 91% 60%)" + /> + } + accent="hsl(142 69% 42%)" + /> + } + accent="hsl(199 89% 54%)" + /> + } + accent="hsl(262 83% 65%)" + /> +
+ ) : null} + + {/* ── Explorer ──────────────────────────────────────────── */} + {loading ? ( +
+
+ +

Loading vault entries…

+
+
+ ) : ( + + )} +
+
+ ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 000000000..bd7254756 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,404 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ───────────────────────────────────────────────────────────── + REKALL Design System + Command-room dark aesthetic — deep navy, tight borders, glow accents +───────────────────────────────────────────────────────────── */ + +@layer base { + :root { + /* ── Light mode ──────────────────────────────────── */ + --background: 220 16% 97%; + --background-subtle: 220 16% 93%; + --foreground: 222 24% 8%; + + --card: 0 0% 100%; + --card-hover: 220 16% 98%; + --card-foreground: 222 24% 8%; + + --border: 220 14% 86%; + --border-strong: 220 14% 74%; + --input: 220 14% 92%; + --ring: 221 83% 53%; + + --primary: 221 83% 53%; + --primary-hover: 221 83% 46%; + --primary-foreground: 0 0% 100%; + + --secondary: 220 14% 92%; + --secondary-foreground: 222 20% 30%; + + --muted: 220 14% 92%; + --muted-foreground: 220 10% 46%; + + --accent: 221 83% 53%; + --accent-subtle: 221 83% 96%; + + --popover: 0 0% 100%; + --popover-foreground: 222 24% 8%; + + --destructive: 0 72% 51%; + --success: 142 72% 29%; + --warning: 38 92% 50%; + --info: 199 89% 48%; + + --status-processing: 38 92% 50%; + --status-awaiting: 262 83% 58%; + --status-resolved: 142 72% 29%; + --status-failed: 0 72% 51%; + + --sidebar-width: 240px; + --sidebar-bg: 0 0% 100%; + --sidebar-border: 220 14% 88%; + + --radius: 0.55rem; + } + + .dark { + /* ── Dark mode: command-room navy ───────────────── */ + --background: 224 22% 6%; + --background-subtle: 224 20% 9%; + --foreground: 215 22% 90%; + + --card: 224 20% 9%; + --card-hover: 224 20% 11%; + --card-foreground: 215 22% 90%; + + --border: 224 18% 14%; + --border-strong: 224 18% 22%; + --input: 224 20% 11%; + --ring: 217 91% 60%; + + --primary: 217 91% 60%; + --primary-hover: 217 91% 67%; + --primary-foreground: 224 22% 6%; + + --secondary: 224 20% 13%; + --secondary-foreground: 215 16% 62%; + + --muted: 224 20% 12%; + --muted-foreground: 215 12% 42%; + + --accent: 217 91% 60%; + --accent-subtle: 217 60% 10%; + + --popover: 224 22% 8%; + --popover-foreground: 215 22% 90%; + + --destructive: 0 72% 51%; + --success: 142 69% 42%; + --warning: 38 92% 50%; + --info: 199 89% 54%; + + --status-processing: 38 92% 50%; + --status-awaiting: 262 83% 65%; + --status-resolved: 142 69% 42%; + --status-failed: 0 72% 51%; + + --sidebar-bg: 224 24% 5%; + --sidebar-border: 224 18% 12%; + } +} + +@layer base { + * { + @apply border-border; + box-sizing: border-box; + } + + html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: "cv11", "ss01"; + } + + body { + @apply bg-background text-foreground; + font-family: "Inter", "SF Pro Display", system-ui, -apple-system, sans-serif; + font-size: 0.875rem; + line-height: 1.6; + } + + code, pre, kbd { + font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono", monospace; + } +} + +/* ── Scrollbar ─────────────────────────────────────────────── */ +::-webkit-scrollbar { width: 4px; height: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: hsl(var(--border-strong)); + border-radius: 99px; +} +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground) / 0.5); +} + +/* ── Animations ────────────────────────────────────────────── */ +@keyframes step-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +@keyframes fade-up { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes slide-in-left { + from { opacity: 0; transform: translateX(-12px); } + to { opacity: 1; transform: translateX(0); } +} +@keyframes scale-in { + from { opacity: 0; transform: scale(0.96); } + to { opacity: 1; transform: scale(1); } +} +@keyframes progress-fill { + from { width: 0%; } + to { width: var(--target-width, 100%); } +} + +.step-running { animation: step-pulse 1.4s ease-in-out infinite; } +.fade-up { animation: fade-up 0.35s ease-out both; } +.fade-in { animation: fade-in 0.25s ease-out both; } +.slide-in-left { animation: slide-in-left 0.3s ease-out both; } +.scale-in { animation: scale-in 0.25s ease-out both; } + +/* ── Skeleton shimmer ──────────────────────────────────────── */ +.skeleton { + background: linear-gradient( + 90deg, + hsl(var(--muted)) 25%, + hsl(var(--muted-foreground) / 0.08) 50%, + hsl(var(--muted)) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.8s infinite; + border-radius: var(--radius); +} + +/* ── Sidebar nav active indicator ─────────────────────────── */ +.nav-active { + position: relative; +} +.nav-active::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + height: 60%; + width: 3px; + background: hsl(var(--primary)); + border-radius: 0 99px 99px 0; +} + +/* ── Gradient text ─────────────────────────────────────────── */ +.gradient-text { + background: linear-gradient(135deg, hsl(217 91% 65%), hsl(270 80% 70%)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} +.gradient-text-warm { + background: linear-gradient(135deg, hsl(38 92% 55%), hsl(16 90% 60%)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* ── Card hover lift ───────────────────────────────────────── */ +.card-lift { + transition: transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease, background 140ms ease; +} +.card-lift:hover { + transform: translateY(-2px); + border-color: hsl(var(--border-strong)); + box-shadow: 0 8px 32px -8px hsl(var(--background) / 0.8), 0 2px 8px -2px hsl(0 0% 0% / 0.12); +} + +/* ── Glass panel ───────────────────────────────────────────── */ +.glass { + backdrop-filter: blur(12px) saturate(180%); + background: hsl(var(--card) / 0.7); + border: 1px solid hsl(var(--border)); +} + +/* ── Status dot ────────────────────────────────────────────── */ +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} +.status-dot.live { + background: hsl(var(--success)); + box-shadow: 0 0 0 3px hsl(var(--success) / 0.2); + animation: step-pulse 2s ease-in-out infinite; +} + +/* ── Monospace terminal block ──────────────────────────────── */ +.terminal { + background: hsl(224 30% 5%); + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 12px; + line-height: 1.7; + padding: 1rem; + overflow-x: auto; +} +.dark .terminal { + background: hsl(224 28% 4%); +} + +/* ── Diff colors ───────────────────────────────────────────── */ +.diff-add { color: hsl(142 69% 50%); } +.diff-del { color: hsl(0 72% 55%); } +.diff-meta { color: hsl(var(--muted-foreground)); } + +/* ── Focus ring override ───────────────────────────────────── */ +.focus-ring { + @apply outline-none ring-2 ring-primary ring-offset-2 ring-offset-background; +} +*:focus-visible { + outline: 2px solid hsl(var(--ring)); + outline-offset: 2px; +} + +/* ── Chip / inline tag ─────────────────────────────────────── */ +.chip { + @apply inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium; +} + +/* ── Glow utilities ────────────────────────────────────────── */ +.glow-primary { + box-shadow: 0 0 20px -4px hsl(217 91% 60% / 0.35), 0 0 40px -8px hsl(217 91% 60% / 0.15); +} +.glow-success { + box-shadow: 0 0 20px -4px hsl(142 69% 42% / 0.35), 0 0 40px -8px hsl(142 69% 42% / 0.15); +} +.glow-danger { + box-shadow: 0 0 20px -4px hsl(0 72% 51% / 0.35), 0 0 40px -8px hsl(0 72% 51% / 0.15); +} +.glow-warning { + box-shadow: 0 0 20px -4px hsl(38 92% 50% / 0.30), 0 0 40px -8px hsl(38 92% 50% / 0.12); +} +.glow-purple { + box-shadow: 0 0 20px -4px hsl(262 83% 65% / 0.30), 0 0 40px -8px hsl(262 83% 65% / 0.12); +} + +/* ── Inner border glow ─────────────────────────────────────── */ +.border-glow-primary { + border-color: hsl(217 91% 60% / 0.35); + box-shadow: 0 0 0 1px hsl(217 91% 60% / 0.10) inset; +} +.border-glow-success { + border-color: hsl(142 69% 42% / 0.4); + box-shadow: 0 0 0 1px hsl(142 69% 42% / 0.10) inset; +} + +/* ── Card hover — deeper lift with ambient glow ────────────── */ +.card-lift { + transition: transform 140ms ease, border-color 160ms ease, box-shadow 160ms ease; +} +.card-lift:hover { + transform: translateY(-1px); + border-color: hsl(var(--border-strong)); + box-shadow: 0 4px 24px -6px hsl(0 0% 0% / 0.35), 0 1px 4px -1px hsl(0 0% 0% / 0.2); +} + +/* ── Card hover (flat, no lift) ────────────────────────────── */ +.card-hover { + transition: border-color 140ms ease, background 140ms ease; +} +.card-hover:hover { + border-color: hsl(var(--border-strong)); + background: hsl(var(--card-hover)); +} + +/* ── Section header rule ───────────────────────────────────── */ +.section-rule { + border-bottom: 1px solid hsl(var(--border)); + padding-bottom: 0.75rem; + margin-bottom: 1rem; +} + +/* ── Inset panel (code / log areas) ───────────────────────── */ +.inset-panel { + background: hsl(224 28% 4%); + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); +} +.dark .inset-panel { + background: hsl(224 30% 3%); +} + +/* ── Page header with subtle gradient separator ───────────── */ +.page-header { + border-bottom: 1px solid hsl(var(--border)); + background: linear-gradient( + 180deg, + hsl(var(--background-subtle)) 0%, + hsl(var(--background)) 100% + ); + backdrop-filter: blur(8px); +} + +/* ── Data row highlight on hover ───────────────────────────── */ +.data-row { + transition: background 100ms ease; +} +.data-row:hover { + background: hsl(var(--muted) / 0.6); +} + +/* ── Mono label ────────────────────────────────────────────── */ +.mono-label { + font-family: "JetBrains Mono", "Fira Code", monospace; + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: hsl(var(--muted-foreground)); +} + +/* ── Scan line texture (subtle, dark mode only) ────────────── */ +@media (prefers-color-scheme: dark) { + .scanlines::after { + content: ""; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + hsl(0 0% 0% / 0.03) 2px, + hsl(0 0% 0% / 0.03) 4px + ); + pointer-events: none; + } +} + +/* ── Noise overlay ─────────────────────────────────────────── */ +.noise { + position: relative; +} +.noise::before { + content: ""; + position: absolute; + inset: 0; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.04'/%3E%3C/svg%3E"); + opacity: 0.025; + pointer-events: none; + border-radius: inherit; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 000000000..ee9caa4a1 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { ThemeProvider } from "@/components/theme-provider"; + +export const metadata: Metadata = { + title: { default: "REKALL", template: "%s — REKALL" }, + description: "Memory-driven agentic CI/CD repair — detects failures, retrieves fixes from a learning vault, and applies repairs with human-in-the-loop governance.", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +