feat: add environments — parametric ticket ID prefixes, env selector UI, MCP tools
- Environment model (prefix, name, description) with CRUD API - Ticket.human_id now computed from environment prefix (e.g. PROJ-001) - Migration 0002: environments table + backfill existing tickets to HOME env - Router: list/create/get/patch/delete environments; list_tickets + create_ticket accept environment_id - MCP: list_environments, create_environment tools; environment_id added to list_tickets + create_ticket - UI: env pill selector with dropdown, new-environment modal, filter tickets by env Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2a7686f398
commit
d5f3ce0667
5 changed files with 442 additions and 40 deletions
|
|
@ -91,21 +91,53 @@ def _check(r: httpx.Response) -> dict | list:
|
|||
return r.json()
|
||||
|
||||
|
||||
# ── Environment tools ─────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def list_environments() -> list[dict]:
|
||||
"""
|
||||
List all ticket environments (prefix + name). Each environment scopes a
|
||||
set of tickets with its own ID prefix (e.g. HOME-001, PROJ-001).
|
||||
"""
|
||||
with _client() as c:
|
||||
return _check(c.get("/api/environments"))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_environment(prefix: str, name: str, description: str = "") -> dict:
|
||||
"""
|
||||
Create a new ticket environment.
|
||||
|
||||
Args:
|
||||
prefix: Short uppercase identifier, e.g. PROJ or LAB (max 16 chars).
|
||||
name: Human-readable name, e.g. "My Project".
|
||||
description: Optional longer description.
|
||||
"""
|
||||
with _client() as c:
|
||||
return _check(c.post("/api/environments", json={
|
||||
"prefix": prefix.upper(),
|
||||
"name": name,
|
||||
"description": description or None,
|
||||
}))
|
||||
|
||||
|
||||
# ── Ticket tools ──────────────────────────────────────────────────────────────
|
||||
|
||||
@mcp.tool()
|
||||
def list_tickets(status: str = "", type: str = "", q: str = "") -> list[dict]:
|
||||
def list_tickets(status: str = "", type: str = "", q: str = "",
|
||||
environment_id: int = 0) -> list[dict]:
|
||||
"""
|
||||
List homelab tickets. Results include attachment_count so you know
|
||||
which tickets have supporting documents without fetching them.
|
||||
|
||||
Args:
|
||||
status: Filter by status — open, ongoing, completed, abandoned.
|
||||
Leave empty to list all statuses.
|
||||
type: Filter by type — feature, bug, chore, project.
|
||||
Leave empty to list all types.
|
||||
q: Full-text search query matched against ticket title and
|
||||
description (case-insensitive). Leave empty to skip.
|
||||
status: Filter by status — open, ongoing, completed, abandoned.
|
||||
Leave empty to list all statuses.
|
||||
type: Filter by type — feature, bug, chore, project.
|
||||
Leave empty to list all types.
|
||||
q: Full-text search query matched against ticket title and
|
||||
description (case-insensitive). Leave empty to skip.
|
||||
environment_id: Filter by environment ID. Pass 0 (default) for all environments.
|
||||
"""
|
||||
with _client() as c:
|
||||
params = {}
|
||||
|
|
@ -115,6 +147,8 @@ def list_tickets(status: str = "", type: str = "", q: str = "") -> list[dict]:
|
|||
params["type"] = type
|
||||
if q:
|
||||
params["q"] = q
|
||||
if environment_id:
|
||||
params["environment_id"] = environment_id
|
||||
return _check(c.get("/api/tickets", params=params))
|
||||
|
||||
|
||||
|
|
@ -133,13 +167,14 @@ def get_ticket(ticket_id: int) -> dict:
|
|||
|
||||
@mcp.tool()
|
||||
def create_ticket(
|
||||
title: str,
|
||||
description: str = "",
|
||||
status: str = "open",
|
||||
type: str = "feature",
|
||||
user: str = "claude",
|
||||
parent_id: int = 0,
|
||||
effort: int = 0,
|
||||
title: str,
|
||||
description: str = "",
|
||||
status: str = "open",
|
||||
type: str = "feature",
|
||||
user: str = "claude",
|
||||
parent_id: int = 0,
|
||||
effort: int = 0,
|
||||
environment_id: int = 0,
|
||||
) -> dict:
|
||||
"""
|
||||
Create a new homelab ticket. For longer supporting documents
|
||||
|
|
@ -147,13 +182,16 @@ def create_ticket(
|
|||
everything into description.
|
||||
|
||||
Args:
|
||||
title: Short summary of the work item.
|
||||
description: Markdown body with context, links, acceptance criteria.
|
||||
status: open (default), ongoing, completed, abandoned.
|
||||
type: feature (default), bug, chore, project.
|
||||
user: Who is submitting — kevin or claude (default: claude).
|
||||
parent_id: ID of the parent ticket (omit or pass 0 for no parent).
|
||||
effort: Estimated effort rating 1–10 (omit or pass 0 to leave unset).
|
||||
title: Short summary of the work item.
|
||||
description: Markdown body with context, links, acceptance criteria.
|
||||
status: open (default), ongoing, completed, abandoned.
|
||||
type: feature (default), bug, chore, project.
|
||||
user: Who is submitting — kevin or claude (default: claude).
|
||||
parent_id: ID of the parent ticket (omit or pass 0 for no parent).
|
||||
effort: Estimated effort rating 1–10 (omit or pass 0 to leave unset).
|
||||
environment_id: Environment to file the ticket in. Pass 0 to use the
|
||||
default environment (HOME). Use list_environments() to
|
||||
find available environments.
|
||||
"""
|
||||
body: dict = {
|
||||
"title": title,
|
||||
|
|
@ -166,6 +204,8 @@ def create_ticket(
|
|||
body["parent_id"] = parent_id
|
||||
if effort:
|
||||
body["effort"] = effort
|
||||
if environment_id:
|
||||
body["environment_id"] = environment_id
|
||||
with _client() as c:
|
||||
ticket = _check(c.post("/api/tickets", json=body))
|
||||
script = _generate_startup_script(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue