diff --git a/.env.example b/.env.example index 1fe6671..1723c12 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,23 @@ DATA_DIR=./data # IP this machine advertises to the coordinator (must be reachable from coordinator host) # CF_ORCH_ADVERTISE_HOST=10.1.10.71 +# CF-core hosted coordinator (managed cloud GPU inference — Paid+ tier) +# Set CF_ORCH_URL to use a hosted cf-orch coordinator instead of self-hosting. +# CF_LICENSE_KEY is read automatically by CFOrchClient for bearer auth. +# CF_ORCH_URL=https://orch.circuitforge.tech +# CF_LICENSE_KEY=CFG-KIWI-xxxx-xxxx-xxxx + +# LLM backend — env-var auto-config (no llm.yaml needed for bare-metal users) +# LLMRouter checks these in priority order: +# 1. Anthropic cloud — set ANTHROPIC_API_KEY +# 2. OpenAI cloud — set OPENAI_API_KEY +# 3. Local Ollama — set OLLAMA_HOST (+ optionally OLLAMA_MODEL) +# All three are optional; leave unset to rely on a local llm.yaml instead. +# ANTHROPIC_API_KEY=sk-ant-... +# OPENAI_API_KEY=sk-... +# OLLAMA_HOST=http://localhost:11434 +# OLLAMA_MODEL=llama3.2 + # Processing USE_GPU=true GPU_MEMORY_LIMIT=6144 @@ -53,3 +70,8 @@ DEMO_MODE=false # Directus JWT (must match cf-directus SECRET env var) # DIRECTUS_JWT_SECRET= + +# In-app feedback → Forgejo issue creation +# FORGEJO_API_TOKEN= +# FORGEJO_REPO=Circuit-Forge/kiwi +# FORGEJO_API_URL=https://git.opensourcesolarpunk.com/api/v1 diff --git a/.gitleaksignore b/.gitleaksignore new file mode 100644 index 0000000..19e8b63 --- /dev/null +++ b/.gitleaksignore @@ -0,0 +1,7 @@ +# Findings suppressed here are historical false positives or already-rotated secrets. +# .env was accidentally included in the initial commit; it is now gitignored. +# Rotate DIRECTUS_JWT_SECRET if it has not been changed since 2026-03-30. + +# c166e5216 (chore: initial commit) — .env included by mistake +c166e5216af532a08112ef87e8542cd51c184115:.env:generic-api-key:25 +c166e5216af532a08112ef87e8542cd51c184115:.env:cf-generic-env-token:25 diff --git a/app/api/endpoints/feedback.py b/app/api/endpoints/feedback.py new file mode 100644 index 0000000..8609073 --- /dev/null +++ b/app/api/endpoints/feedback.py @@ -0,0 +1,169 @@ +""" +Feedback endpoint — creates Forgejo issues from in-app feedback. +Ported from peregrine/scripts/feedback_api.py; adapted for Kiwi context. +""" +from __future__ import annotations + +import os +import platform +import subprocess +from datetime import datetime, timezone +from pathlib import Path +from typing import Literal + +import requests +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from app.core.config import settings + +router = APIRouter() + +_ROOT = Path(__file__).resolve().parents[3] + +# ── Forgejo helpers ──────────────────────────────────────────────────────────── + +_LABEL_COLORS = { + "beta-feedback": "#0075ca", + "needs-triage": "#e4e669", + "bug": "#d73a4a", + "feature-request": "#a2eeef", + "question": "#d876e3", +} + + +def _forgejo_headers() -> dict: + token = os.environ.get("FORGEJO_API_TOKEN", "") + return {"Authorization": f"token {token}", "Content-Type": "application/json"} + + +def _ensure_labels(label_names: list[str]) -> list[int]: + base = os.environ.get("FORGEJO_API_URL", "https://git.opensourcesolarpunk.com/api/v1") + repo = os.environ.get("FORGEJO_REPO", "Circuit-Forge/kiwi") + headers = _forgejo_headers() + resp = requests.get(f"{base}/repos/{repo}/labels", headers=headers, timeout=10) + existing = {lb["name"]: lb["id"] for lb in resp.json()} if resp.ok else {} + ids: list[int] = [] + for name in label_names: + if name in existing: + ids.append(existing[name]) + else: + r = requests.post( + f"{base}/repos/{repo}/labels", + headers=headers, + json={"name": name, "color": _LABEL_COLORS.get(name, "#ededed")}, + timeout=10, + ) + if r.ok: + ids.append(r.json()["id"]) + return ids + + +def _collect_context(tab: str) -> dict: + """Collect lightweight app context: tab, version, platform, timestamp.""" + try: + version = subprocess.check_output( + ["git", "describe", "--tags", "--always"], + cwd=_ROOT, text=True, timeout=5, + ).strip() + except Exception: + version = "dev" + + return { + "tab": tab, + "version": version, + "demo_mode": settings.DEMO_MODE, + "cloud_mode": settings.CLOUD_MODE, + "platform": platform.platform(), + "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + } + + +def _build_issue_body(form: dict, context: dict) -> str: + _TYPE_LABELS = {"bug": "🐛 Bug", "feature": "✨ Feature Request", "other": "💬 Other"} + lines: list[str] = [ + f"## {_TYPE_LABELS.get(form.get('type', 'other'), '💬 Other')}", + "", + form.get("description", ""), + "", + ] + if form.get("type") == "bug" and form.get("repro"): + lines += ["### Reproduction Steps", "", form["repro"], ""] + + lines += ["### Context", ""] + for k, v in context.items(): + lines.append(f"- **{k}:** {v}") + lines.append("") + + if form.get("submitter"): + lines += ["---", f"*Submitted by: {form['submitter']}*"] + + return "\n".join(lines) + + +# ── Schemas ──────────────────────────────────────────────────────────────────── + +class FeedbackRequest(BaseModel): + title: str + description: str + type: Literal["bug", "feature", "other"] = "other" + repro: str = "" + tab: str = "unknown" + submitter: str = "" # optional "Name " attribution + + +class FeedbackResponse(BaseModel): + issue_number: int + issue_url: str + + +# ── Routes ───────────────────────────────────────────────────────────────────── + +@router.get("/status") +def feedback_status() -> dict: + """Return whether feedback submission is configured on this instance.""" + return {"enabled": bool(os.environ.get("FORGEJO_API_TOKEN")) and not settings.DEMO_MODE} + + +@router.post("", response_model=FeedbackResponse) +def submit_feedback(payload: FeedbackRequest) -> FeedbackResponse: + """ + File a Forgejo issue from in-app feedback. + Silently disabled when FORGEJO_API_TOKEN is not set (demo/offline mode). + """ + token = os.environ.get("FORGEJO_API_TOKEN", "") + if not token: + raise HTTPException( + status_code=503, + detail="Feedback disabled: FORGEJO_API_TOKEN not configured.", + ) + if settings.DEMO_MODE: + raise HTTPException(status_code=403, detail="Feedback disabled in demo mode.") + + context = _collect_context(payload.tab) + form = { + "type": payload.type, + "description": payload.description, + "repro": payload.repro, + "submitter": payload.submitter, + } + body = _build_issue_body(form, context) + labels = ["beta-feedback", "needs-triage"] + labels.append({"bug": "bug", "feature": "feature-request"}.get(payload.type, "question")) + + base = os.environ.get("FORGEJO_API_URL", "https://git.opensourcesolarpunk.com/api/v1") + repo = os.environ.get("FORGEJO_REPO", "Circuit-Forge/kiwi") + headers = _forgejo_headers() + + label_ids = _ensure_labels(labels) + resp = requests.post( + f"{base}/repos/{repo}/issues", + headers=headers, + json={"title": payload.title, "body": body, "labels": label_ids}, + timeout=15, + ) + if not resp.ok: + raise HTTPException(status_code=502, detail=f"Forgejo error: {resp.text[:200]}") + + data = resp.json() + return FeedbackResponse(issue_number=data["number"], issue_url=data["html_url"]) diff --git a/app/api/routes.py b/app/api/routes.py index fd642c7..79395a2 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples +from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples, feedback api_router = APIRouter() @@ -10,4 +10,5 @@ api_router.include_router(export.router, tags=["export" api_router.include_router(inventory.router, prefix="/inventory", tags=["inventory"]) api_router.include_router(recipes.router, prefix="/recipes", tags=["recipes"]) api_router.include_router(settings.router, prefix="/settings", tags=["settings"]) -api_router.include_router(staples.router, prefix="/staples", tags=["staples"]) \ No newline at end of file +api_router.include_router(staples.router, prefix="/staples", tags=["staples"]) +api_router.include_router(feedback.router, prefix="/feedback", tags=["feedback"]) \ No newline at end of file diff --git a/app/core/config.py b/app/core/config.py index 0b06934..091b574 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -46,6 +46,10 @@ class Settings: # CF-core resource coordinator (VRAM lease management) COORDINATOR_URL: str = os.environ.get("COORDINATOR_URL", "http://localhost:7700") + # Hosted cf-orch coordinator — bearer token for managed cloud GPU inference (Paid+) + # CFOrchClient reads CF_LICENSE_KEY automatically; exposed here for startup validation. + CF_LICENSE_KEY: str | None = os.environ.get("CF_LICENSE_KEY") + # Feature flags ENABLE_OCR: bool = os.environ.get("ENABLE_OCR", "false").lower() in ("1", "true", "yes") diff --git a/frontend/index.html b/frontend/index.html index f95f61d..d6947bc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - + Kiwi — Pantry Tracker @@ -11,6 +11,18 @@ href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&display=swap" rel="stylesheet" /> + +
diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 406471c..fe179f8 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -113,6 +113,9 @@ + + +