fixed restart target

This commit is contained in:
2026-05-02 16:19:19 +02:00
parent 0505e8352c
commit 14fce4f4d6
3 changed files with 63 additions and 12 deletions

View File

@@ -2,6 +2,7 @@ PORT := 8002
PIDFILE := var/api.pid
LOGFILE := var/api-8002.log
UVICORN := .venv/bin/uvicorn
HEALTH_URL := http://127.0.0.1:$(PORT)/health
.DEFAULT_GOAL := help
.PHONY: help start stop restart status showlogs
@@ -15,10 +16,23 @@ start: ## Start the API server in the background on port 8002
@if [ -f $(PIDFILE) ] && kill -0 $$(cat $(PIDFILE)) 2>/dev/null; then \
echo "Already running (PID $$(cat $(PIDFILE)))"; \
else \
nohup $(UVICORN) repo_registry.web_api.app:app --port $(PORT) \
setsid sh -c 'echo $$$$ > $(PIDFILE); exec $(UVICORN) repo_registry.web_api.app:app --host 127.0.0.1 --port $(PORT)' \
>> $(LOGFILE) 2>&1 & \
echo $$! > $(PIDFILE); \
echo "Started (PID $$!) — http://127.0.0.1:$(PORT)/ui"; \
PID=""; \
for i in $$(seq 1 50); do \
if [ -f $(PIDFILE) ]; then PID=$$(cat $(PIDFILE)); fi; \
if [ -n "$$PID" ] && ! kill -0 $$PID 2>/dev/null; then break; fi; \
if curl -fsS $(HEALTH_URL) >/dev/null 2>&1; then \
echo "Started (PID $$PID) — http://127.0.0.1:$(PORT)/ui"; \
exit 0; \
fi; \
sleep 0.1; \
done; \
rm -f $(PIDFILE); \
echo "Failed to start API server on http://127.0.0.1:$(PORT)/ui"; \
echo "Recent log output:"; \
tail -40 $(LOGFILE) 2>/dev/null || true; \
exit 1; \
fi
stop: ## Stop the API server

View File

@@ -17,7 +17,12 @@ router = APIRouter(include_in_schema=False)
APP_NAME = "Repository Scoping"
def page(title: str, body: str) -> HTMLResponse:
def page(title: str, body: str, selected_repository: str | None = None) -> HTMLResponse:
repository_context = (
f'<span class="header-context">{escape(selected_repository)}</span>'
if selected_repository
else ""
)
return HTMLResponse(
f"""
<!doctype html>
@@ -166,6 +171,9 @@ def page(title: str, body: str) -> HTMLResponse:
font: 13px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace;
}}
.actions {{ display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }}
.header-brand {{ display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }}
.header-context {{ color: #b6c8d6; font-weight: 650; }}
.header-context::before {{ content: "/"; color: #6b7d8f; margin-right: 10px; }}
[data-pending] {{ display: none; color: var(--muted); }}
form.is-submitting [data-pending] {{ display: inline; }}
form.is-submitting button[type="submit"] {{ opacity: .7; cursor: wait; }}
@@ -182,7 +190,10 @@ def page(title: str, body: str) -> HTMLResponse:
</head>
<body>
<header>
<a href="/ui"><strong>{APP_NAME}</strong></a>
<div class="header-brand">
<a href="/ui"><strong>{APP_NAME}</strong></a>
{repository_context}
</div>
<nav class="actions">
<a href="/ui/search">Search</a>
<a href="/ui/discovery">Discovery</a>
@@ -279,6 +290,7 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
@router.get("/ui/scope")
def scope_document() -> HTMLResponse:
repository_name = Path.cwd().name
scope_path = Path("SCOPE.md")
if scope_path.exists():
content = scope_path.read_text(encoding="utf-8")
@@ -288,7 +300,7 @@ def scope_document() -> HTMLResponse:
body = f"""
<h1>SCOPE.md</h1>
<section class="panel stack">
<p class="muted">Canonical scope summary for this repository.</p>
<p class="muted">Canonical scope summary for the {escape(repository_name)} repository.</p>
{rendered}
</section>
"""
@@ -828,7 +840,7 @@ def repository_detail(
</form>
</section>
"""
return page(repository.name, body)
return page(repository.name, body, selected_repository=repository.name)
@router.get("/ui/repos/{repository_id}/export")
@@ -1280,7 +1292,11 @@ def analysis_run_detail(
{render_expectation_gaps(expectation_gaps)}
</section>
"""
return page(f"{repository.name} Run {analysis_run_id}", body)
return page(
f"{repository.name} Run {analysis_run_id}",
body,
selected_repository=repository.name,
)
@router.post("/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/expectation-gaps")
@@ -1446,7 +1462,7 @@ def repository_element_listing(
</table>
</section>
"""
return page(title, body)
return page(title, body, selected_repository=repository.name)
@router.get("/ui/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}")
@@ -1499,7 +1515,11 @@ def analysis_run_diff_detail(
</section>
</div>
"""
return page(f"{diff.repository.name} Change Review", body)
return page(
f"{diff.repository.name} Change Review",
body,
selected_repository=diff.repository.name,
)
@router.post("/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve")
@@ -2654,11 +2674,17 @@ def render_element_row(
repository_id: int,
analysis_run_id: int | None,
) -> str:
name = str(row["name"])
rendered_name = (
f'<a href="/ui/repos/{repository_id}">{escape(name)}</a>'
if row.get("item_kind") == "scope"
else escape(name)
)
return f"""
<tr>
<td>{render_entry_badge(row)}</td>
<td><span class="pill">{escape(str(row["primary_class"]))}</span>{render_attribute_pills(row)}</td>
<td>{escape(str(row["name"]))}{render_drilldown_links(row, repository_id, analysis_run_id)}</td>
<td>{rendered_name}{render_drilldown_links(row, repository_id, analysis_run_id)}</td>
<td>{escape(str(row["parent"]))}</td>
<td>{render_element_source_detail(row)}</td>
<td>{render_element_actions(row, repository_id, analysis_run_id)}</td>
@@ -3587,7 +3613,7 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
<div class="tree">
<ul>
<li id="scope-{scope['id']}">
<strong>{escape(scope['name'])}</strong>
<strong><a href="/ui/repos/{repository_id}">{escape(scope['name'])}</a></strong>
<span class="pill">scope</span>
<span class="pill">{scope['confidence']:.2f} {escape(scope['confidence_label'])}</span>
<p class="muted">{escape(scope_description)}</p>

View File

@@ -330,6 +330,7 @@ def test_ui_scope_page_presents_scope_md():
assert response.status_code == 200
assert "SCOPE.md" in response.text
assert "Canonical scope summary for the repo-scoping repository." in response.text
assert "scope.generate" in response.text
assert "repo-scoping" in response.text
@@ -1358,8 +1359,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
approved_detail = client.get(approve_response.headers["location"])
assert approved_detail.status_code == 200
assert '<span class="header-context">UI Repo Edited</span>' in approved_detail.text
assert "Approved Characteristics" in approved_detail.text
assert "Approved Characteristic Tree" in approved_detail.text
assert '<strong><a href="/ui/repos/1">UI Repo</a></strong>' in approved_detail.text
assert "scope" in approved_detail.text
assert "Evidence supporting this capability" in approved_detail.text
assert "1 scope" in approved_detail.text
@@ -1378,6 +1381,14 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "Export" in approved_detail.text
assert "Elements" in approved_detail.text
assert "q=Report+Service+Status" in approved_detail.text
scope_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={"scope": "all", "type": "scopes", "entry_filter": "approved"},
)
assert scope_listing.status_code == 200
assert '<span class="header-context">UI Repo Edited</span>' in scope_listing.text
assert f'<a href="/ui/repos/{repository_id}">UI Repo</a>' in scope_listing.text
assert (
f"/ui/repos/{repository_id}/elements?scope=all&amp;entry_filter=approved&amp;type=abilities"
in approved_detail.text