forked from Rockachopa/Timmy-time-dashboard
ruff (#169)
* polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
708c8a2477
commit
9d78eb31d1
@@ -11,15 +11,13 @@ re-uses the session cookie thereafter.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from config import settings
|
||||
from integrations.paperclip.models import (
|
||||
AddCommentRequest,
|
||||
CreateIssueRequest,
|
||||
PaperclipAgent,
|
||||
PaperclipComment,
|
||||
@@ -40,20 +38,20 @@ class PaperclipClient:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
timeout: int = 30,
|
||||
):
|
||||
self._base_url = (base_url or settings.paperclip_url).rstrip("/")
|
||||
self._api_key = api_key or settings.paperclip_api_key
|
||||
self._timeout = timeout or settings.paperclip_timeout
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
self._client: httpx.AsyncClient | None = None
|
||||
|
||||
# ── lifecycle ────────────────────────────────────────────────────────
|
||||
|
||||
def _get_client(self) -> httpx.AsyncClient:
|
||||
if self._client is None or self._client.is_closed:
|
||||
headers: Dict[str, str] = {"Accept": "application/json"}
|
||||
headers: dict[str, str] = {"Accept": "application/json"}
|
||||
if self._api_key:
|
||||
headers["Authorization"] = f"Bearer {self._api_key}"
|
||||
self._client = httpx.AsyncClient(
|
||||
@@ -69,7 +67,7 @@ class PaperclipClient:
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
async def _get(self, path: str, params: Optional[Dict] = None) -> Optional[Any]:
|
||||
async def _get(self, path: str, params: dict | None = None) -> Any | None:
|
||||
try:
|
||||
resp = await self._get_client().get(path, params=params)
|
||||
resp.raise_for_status()
|
||||
@@ -78,7 +76,7 @@ class PaperclipClient:
|
||||
logger.warning("Paperclip GET %s failed: %s", path, exc)
|
||||
return None
|
||||
|
||||
async def _post(self, path: str, json: Optional[Dict] = None) -> Optional[Any]:
|
||||
async def _post(self, path: str, json: dict | None = None) -> Any | None:
|
||||
try:
|
||||
resp = await self._get_client().post(path, json=json)
|
||||
resp.raise_for_status()
|
||||
@@ -87,7 +85,7 @@ class PaperclipClient:
|
||||
logger.warning("Paperclip POST %s failed: %s", path, exc)
|
||||
return None
|
||||
|
||||
async def _patch(self, path: str, json: Optional[Dict] = None) -> Optional[Any]:
|
||||
async def _patch(self, path: str, json: dict | None = None) -> Any | None:
|
||||
try:
|
||||
resp = await self._get_client().patch(path, json=json)
|
||||
resp.raise_for_status()
|
||||
@@ -114,13 +112,13 @@ class PaperclipClient:
|
||||
|
||||
# ── companies ────────────────────────────────────────────────────────
|
||||
|
||||
async def list_companies(self) -> List[Dict[str, Any]]:
|
||||
async def list_companies(self) -> list[dict[str, Any]]:
|
||||
data = await self._get("/api/companies")
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
# ── agents ───────────────────────────────────────────────────────────
|
||||
|
||||
async def list_agents(self, company_id: Optional[str] = None) -> List[PaperclipAgent]:
|
||||
async def list_agents(self, company_id: str | None = None) -> list[PaperclipAgent]:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
logger.warning("paperclip_company_id not set — cannot list agents")
|
||||
@@ -130,25 +128,25 @@ class PaperclipClient:
|
||||
return []
|
||||
return [PaperclipAgent(**a) for a in data]
|
||||
|
||||
async def get_agent(self, agent_id: str) -> Optional[PaperclipAgent]:
|
||||
async def get_agent(self, agent_id: str) -> PaperclipAgent | None:
|
||||
data = await self._get(f"/api/agents/{agent_id}")
|
||||
return PaperclipAgent(**data) if data else None
|
||||
|
||||
async def wake_agent(
|
||||
self,
|
||||
agent_id: str,
|
||||
issue_id: Optional[str] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
issue_id: str | None = None,
|
||||
message: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Trigger a heartbeat wake for an agent."""
|
||||
body: Dict[str, Any] = {}
|
||||
body: dict[str, Any] = {}
|
||||
if issue_id:
|
||||
body["issueId"] = issue_id
|
||||
if message:
|
||||
body["message"] = message
|
||||
return await self._post(f"/api/agents/{agent_id}/wakeup", json=body)
|
||||
|
||||
async def get_org(self, company_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
async def get_org(self, company_id: str | None = None) -> dict[str, Any] | None:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
return None
|
||||
@@ -158,13 +156,13 @@ class PaperclipClient:
|
||||
|
||||
async def list_issues(
|
||||
self,
|
||||
company_id: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
) -> List[PaperclipIssue]:
|
||||
company_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> list[PaperclipIssue]:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
return []
|
||||
params: Dict[str, str] = {}
|
||||
params: dict[str, str] = {}
|
||||
if status:
|
||||
params["status"] = status
|
||||
data = await self._get(f"/api/companies/{cid}/issues", params=params)
|
||||
@@ -172,15 +170,15 @@ class PaperclipClient:
|
||||
return []
|
||||
return [PaperclipIssue(**i) for i in data]
|
||||
|
||||
async def get_issue(self, issue_id: str) -> Optional[PaperclipIssue]:
|
||||
async def get_issue(self, issue_id: str) -> PaperclipIssue | None:
|
||||
data = await self._get(f"/api/issues/{issue_id}")
|
||||
return PaperclipIssue(**data) if data else None
|
||||
|
||||
async def create_issue(
|
||||
self,
|
||||
req: CreateIssueRequest,
|
||||
company_id: Optional[str] = None,
|
||||
) -> Optional[PaperclipIssue]:
|
||||
company_id: str | None = None,
|
||||
) -> PaperclipIssue | None:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
logger.warning("paperclip_company_id not set — cannot create issue")
|
||||
@@ -195,7 +193,7 @@ class PaperclipClient:
|
||||
self,
|
||||
issue_id: str,
|
||||
req: UpdateIssueRequest,
|
||||
) -> Optional[PaperclipIssue]:
|
||||
) -> PaperclipIssue | None:
|
||||
data = await self._patch(
|
||||
f"/api/issues/{issue_id}",
|
||||
json=req.model_dump(exclude_none=True),
|
||||
@@ -207,7 +205,7 @@ class PaperclipClient:
|
||||
|
||||
# ── issue comments ───────────────────────────────────────────────────
|
||||
|
||||
async def list_comments(self, issue_id: str) -> List[PaperclipComment]:
|
||||
async def list_comments(self, issue_id: str) -> list[PaperclipComment]:
|
||||
data = await self._get(f"/api/issues/{issue_id}/comments")
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
@@ -217,7 +215,7 @@ class PaperclipClient:
|
||||
self,
|
||||
issue_id: str,
|
||||
content: str,
|
||||
) -> Optional[PaperclipComment]:
|
||||
) -> PaperclipComment | None:
|
||||
data = await self._post(
|
||||
f"/api/issues/{issue_id}/comments",
|
||||
json={"content": content},
|
||||
@@ -226,20 +224,20 @@ class PaperclipClient:
|
||||
|
||||
# ── issue workflow ───────────────────────────────────────────────────
|
||||
|
||||
async def checkout_issue(self, issue_id: str) -> Optional[Dict[str, Any]]:
|
||||
async def checkout_issue(self, issue_id: str) -> dict[str, Any] | None:
|
||||
"""Assign an issue to Timmy (checkout)."""
|
||||
body: Dict[str, Any] = {}
|
||||
body: dict[str, Any] = {}
|
||||
if settings.paperclip_agent_id:
|
||||
body["agentId"] = settings.paperclip_agent_id
|
||||
return await self._post(f"/api/issues/{issue_id}/checkout", json=body)
|
||||
|
||||
async def release_issue(self, issue_id: str) -> Optional[Dict[str, Any]]:
|
||||
async def release_issue(self, issue_id: str) -> dict[str, Any] | None:
|
||||
"""Release a checked-out issue."""
|
||||
return await self._post(f"/api/issues/{issue_id}/release")
|
||||
|
||||
# ── goals ────────────────────────────────────────────────────────────
|
||||
|
||||
async def list_goals(self, company_id: Optional[str] = None) -> List[PaperclipGoal]:
|
||||
async def list_goals(self, company_id: str | None = None) -> list[PaperclipGoal]:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
return []
|
||||
@@ -252,8 +250,8 @@ class PaperclipClient:
|
||||
self,
|
||||
title: str,
|
||||
description: str = "",
|
||||
company_id: Optional[str] = None,
|
||||
) -> Optional[PaperclipGoal]:
|
||||
company_id: str | None = None,
|
||||
) -> PaperclipGoal | None:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
return None
|
||||
@@ -267,38 +265,38 @@ class PaperclipClient:
|
||||
|
||||
async def list_heartbeat_runs(
|
||||
self,
|
||||
company_id: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
company_id: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
return []
|
||||
data = await self._get(f"/api/companies/{cid}/heartbeat-runs")
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
async def get_run_events(self, run_id: str) -> List[Dict[str, Any]]:
|
||||
async def get_run_events(self, run_id: str) -> list[dict[str, Any]]:
|
||||
data = await self._get(f"/api/heartbeat-runs/{run_id}/events")
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
async def cancel_run(self, run_id: str) -> Optional[Dict[str, Any]]:
|
||||
async def cancel_run(self, run_id: str) -> dict[str, Any] | None:
|
||||
return await self._post(f"/api/heartbeat-runs/{run_id}/cancel")
|
||||
|
||||
# ── approvals ────────────────────────────────────────────────────────
|
||||
|
||||
async def list_approvals(self, company_id: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
async def list_approvals(self, company_id: str | None = None) -> list[dict[str, Any]]:
|
||||
cid = company_id or settings.paperclip_company_id
|
||||
if not cid:
|
||||
return []
|
||||
data = await self._get(f"/api/companies/{cid}/approvals")
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
async def approve(self, approval_id: str, comment: str = "") -> Optional[Dict[str, Any]]:
|
||||
body: Dict[str, Any] = {}
|
||||
async def approve(self, approval_id: str, comment: str = "") -> dict[str, Any] | None:
|
||||
body: dict[str, Any] = {}
|
||||
if comment:
|
||||
body["comment"] = comment
|
||||
return await self._post(f"/api/approvals/{approval_id}/approve", json=body)
|
||||
|
||||
async def reject(self, approval_id: str, comment: str = "") -> Optional[Dict[str, Any]]:
|
||||
body: Dict[str, Any] = {}
|
||||
async def reject(self, approval_id: str, comment: str = "") -> dict[str, Any] | None:
|
||||
body: dict[str, Any] = {}
|
||||
if comment:
|
||||
body["comment"] = comment
|
||||
return await self._post(f"/api/approvals/{approval_id}/reject", json=body)
|
||||
|
||||
Reference in New Issue
Block a user