peregrine/dev-api.py
pyr0ball 1f5ab2df37 chore(vue-spa): dev API + Vite proxy for live data during development
- dev-api.py: minimal FastAPI on :8601 reading /devl/job-seeker/staging.db
  Endpoints: GET /api/jobs, /api/jobs/counts, POST /api/jobs/{id}/approve|reject|revert,
  GET /api/system/status, /api/config/user
- vite.config.ts: server.proxy /api/* → localhost:8601; host: 0.0.0.0 for LAN access
2026-03-17 22:36:45 -07:00

139 lines
4.7 KiB
Python

"""
Minimal dev-only FastAPI server for the Vue SPA.
Reads directly from /devl/job-seeker/staging.db.
Run with:
conda run -n job-seeker uvicorn dev-api:app --port 8600 --reload
"""
import sqlite3
import os
import json
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Optional
DB_PATH = os.environ.get("STAGING_DB", "/devl/job-seeker/staging.db")
app = FastAPI(title="Peregrine Dev API")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://10.1.10.71:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
def _get_db():
db = sqlite3.connect(DB_PATH)
db.row_factory = sqlite3.Row
return db
def _row_to_job(row) -> dict:
d = dict(row)
d["is_remote"] = bool(d.get("is_remote", 0))
return d
# ── GET /api/jobs ─────────────────────────────────────────────────────────────
@app.get("/api/jobs")
def list_jobs(status: str = "pending", limit: int = 50):
db = _get_db()
rows = db.execute(
"SELECT id, title, company, url, source, location, is_remote, salary, "
"description, match_score, keyword_gaps, date_found, status "
"FROM jobs WHERE status = ? ORDER BY match_score DESC NULLS LAST LIMIT ?",
(status, limit),
).fetchall()
db.close()
return [_row_to_job(r) for r in rows]
# ── GET /api/jobs/counts ──────────────────────────────────────────────────────
@app.get("/api/jobs/counts")
def job_counts():
db = _get_db()
rows = db.execute("SELECT status, count(*) as n FROM jobs GROUP BY status").fetchall()
db.close()
counts = {r["status"]: r["n"] for r in rows}
return {
"pending": counts.get("pending", 0),
"approved": counts.get("approved", 0),
"applied": counts.get("applied", 0),
"synced": counts.get("synced", 0),
"rejected": counts.get("rejected", 0),
"total": sum(counts.values()),
}
# ── POST /api/jobs/{id}/approve ───────────────────────────────────────────────
@app.post("/api/jobs/{job_id}/approve")
def approve_job(job_id: int):
db = _get_db()
db.execute("UPDATE jobs SET status = 'approved' WHERE id = ?", (job_id,))
db.commit()
db.close()
return {"ok": True}
# ── POST /api/jobs/{id}/reject ────────────────────────────────────────────────
@app.post("/api/jobs/{job_id}/reject")
def reject_job(job_id: int):
db = _get_db()
db.execute("UPDATE jobs SET status = 'rejected' WHERE id = ?", (job_id,))
db.commit()
db.close()
return {"ok": True}
# ── POST /api/jobs/{id}/revert ────────────────────────────────────────────────
class RevertBody(BaseModel):
status: str
@app.post("/api/jobs/{job_id}/revert")
def revert_job(job_id: int, body: RevertBody):
allowed = {"pending", "approved", "rejected", "applied", "synced"}
if body.status not in allowed:
raise HTTPException(400, f"Invalid status: {body.status}")
db = _get_db()
db.execute("UPDATE jobs SET status = ? WHERE id = ?", (body.status, job_id))
db.commit()
db.close()
return {"ok": True}
# ── GET /api/system/status ────────────────────────────────────────────────────
@app.get("/api/system/status")
def system_status():
return {
"enrichment_enabled": False,
"enrichment_last_run": None,
"enrichment_next_run": None,
"tasks_running": 0,
"integration_name": "Notion",
"integration_unsynced": 0,
}
# ── GET /api/config/user ──────────────────────────────────────────────────────
@app.get("/api/config/user")
def config_user():
# Try to read name from user.yaml if present
try:
import yaml
cfg_path = os.path.join(os.path.dirname(DB_PATH), "config", "user.yaml")
if not os.path.exists(cfg_path):
cfg_path = "/devl/job-seeker/config/user.yaml"
with open(cfg_path) as f:
cfg = yaml.safe_load(f)
return {"name": cfg.get("name", "")}
except Exception:
return {"name": ""}