generated from coulomb/repo-seed
fixed restart target
This commit is contained in:
20
Makefile
20
Makefile
@@ -2,6 +2,7 @@ PORT := 8002
|
|||||||
PIDFILE := var/api.pid
|
PIDFILE := var/api.pid
|
||||||
LOGFILE := var/api-8002.log
|
LOGFILE := var/api-8002.log
|
||||||
UVICORN := .venv/bin/uvicorn
|
UVICORN := .venv/bin/uvicorn
|
||||||
|
HEALTH_URL := http://127.0.0.1:$(PORT)/health
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
.PHONY: help start stop restart status showlogs
|
.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 \
|
@if [ -f $(PIDFILE) ] && kill -0 $$(cat $(PIDFILE)) 2>/dev/null; then \
|
||||||
echo "Already running (PID $$(cat $(PIDFILE)))"; \
|
echo "Already running (PID $$(cat $(PIDFILE)))"; \
|
||||||
else \
|
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 & \
|
>> $(LOGFILE) 2>&1 & \
|
||||||
echo $$! > $(PIDFILE); \
|
PID=""; \
|
||||||
echo "Started (PID $$!) — http://127.0.0.1:$(PORT)/ui"; \
|
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
|
fi
|
||||||
|
|
||||||
stop: ## Stop the API server
|
stop: ## Stop the API server
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ router = APIRouter(include_in_schema=False)
|
|||||||
APP_NAME = "Repository Scoping"
|
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(
|
return HTMLResponse(
|
||||||
f"""
|
f"""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -166,6 +171,9 @@ def page(title: str, body: str) -> HTMLResponse:
|
|||||||
font: 13px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace;
|
font: 13px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||||
}}
|
}}
|
||||||
.actions {{ display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }}
|
.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); }}
|
[data-pending] {{ display: none; color: var(--muted); }}
|
||||||
form.is-submitting [data-pending] {{ display: inline; }}
|
form.is-submitting [data-pending] {{ display: inline; }}
|
||||||
form.is-submitting button[type="submit"] {{ opacity: .7; cursor: wait; }}
|
form.is-submitting button[type="submit"] {{ opacity: .7; cursor: wait; }}
|
||||||
@@ -182,7 +190,10 @@ def page(title: str, body: str) -> HTMLResponse:
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<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">
|
<nav class="actions">
|
||||||
<a href="/ui/search">Search</a>
|
<a href="/ui/search">Search</a>
|
||||||
<a href="/ui/discovery">Discovery</a>
|
<a href="/ui/discovery">Discovery</a>
|
||||||
@@ -279,6 +290,7 @@ def repository_index(service: RegistryService = Depends(get_service)) -> HTMLRes
|
|||||||
|
|
||||||
@router.get("/ui/scope")
|
@router.get("/ui/scope")
|
||||||
def scope_document() -> HTMLResponse:
|
def scope_document() -> HTMLResponse:
|
||||||
|
repository_name = Path.cwd().name
|
||||||
scope_path = Path("SCOPE.md")
|
scope_path = Path("SCOPE.md")
|
||||||
if scope_path.exists():
|
if scope_path.exists():
|
||||||
content = scope_path.read_text(encoding="utf-8")
|
content = scope_path.read_text(encoding="utf-8")
|
||||||
@@ -288,7 +300,7 @@ def scope_document() -> HTMLResponse:
|
|||||||
body = f"""
|
body = f"""
|
||||||
<h1>SCOPE.md</h1>
|
<h1>SCOPE.md</h1>
|
||||||
<section class="panel stack">
|
<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}
|
{rendered}
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
@@ -828,7 +840,7 @@ def repository_detail(
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
"""
|
"""
|
||||||
return page(repository.name, body)
|
return page(repository.name, body, selected_repository=repository.name)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ui/repos/{repository_id}/export")
|
@router.get("/ui/repos/{repository_id}/export")
|
||||||
@@ -1280,7 +1292,11 @@ def analysis_run_detail(
|
|||||||
{render_expectation_gaps(expectation_gaps)}
|
{render_expectation_gaps(expectation_gaps)}
|
||||||
</section>
|
</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")
|
@router.post("/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}/expectation-gaps")
|
||||||
@@ -1446,7 +1462,7 @@ def repository_element_listing(
|
|||||||
</table>
|
</table>
|
||||||
</section>
|
</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}")
|
@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>
|
</section>
|
||||||
</div>
|
</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")
|
@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,
|
repository_id: int,
|
||||||
analysis_run_id: int | None,
|
analysis_run_id: int | None,
|
||||||
) -> str:
|
) -> 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"""
|
return f"""
|
||||||
<tr>
|
<tr>
|
||||||
<td>{render_entry_badge(row)}</td>
|
<td>{render_entry_badge(row)}</td>
|
||||||
<td><span class="pill">{escape(str(row["primary_class"]))}</span>{render_attribute_pills(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>{escape(str(row["parent"]))}</td>
|
||||||
<td>{render_element_source_detail(row)}</td>
|
<td>{render_element_source_detail(row)}</td>
|
||||||
<td>{render_element_actions(row, repository_id, analysis_run_id)}</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">
|
<div class="tree">
|
||||||
<ul>
|
<ul>
|
||||||
<li id="scope-{scope['id']}">
|
<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</span>
|
||||||
<span class="pill">{scope['confidence']:.2f} {escape(scope['confidence_label'])}</span>
|
<span class="pill">{scope['confidence']:.2f} {escape(scope['confidence_label'])}</span>
|
||||||
<p class="muted">{escape(scope_description)}</p>
|
<p class="muted">{escape(scope_description)}</p>
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ def test_ui_scope_page_presents_scope_md():
|
|||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "SCOPE.md" in response.text
|
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 "scope.generate" in response.text
|
||||||
assert "repo-scoping" 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"])
|
approved_detail = client.get(approve_response.headers["location"])
|
||||||
assert approved_detail.status_code == 200
|
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 Characteristics" in approved_detail.text
|
||||||
assert "Approved Characteristic Tree" 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 "scope" in approved_detail.text
|
||||||
assert "Evidence supporting this capability" in approved_detail.text
|
assert "Evidence supporting this capability" in approved_detail.text
|
||||||
assert "1 scope" 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 "Export" in approved_detail.text
|
||||||
assert "Elements" in approved_detail.text
|
assert "Elements" in approved_detail.text
|
||||||
assert "q=Report+Service+Status" 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 (
|
assert (
|
||||||
f"/ui/repos/{repository_id}/elements?scope=all&entry_filter=approved&type=abilities"
|
f"/ui/repos/{repository_id}/elements?scope=all&entry_filter=approved&type=abilities"
|
||||||
in approved_detail.text
|
in approved_detail.text
|
||||||
|
|||||||
Reference in New Issue
Block a user