From 9ed98c7058342bba0b6b0deb53bd1fb27826ed2a Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 26 Apr 2026 02:30:18 +0200 Subject: [PATCH] browser-side edit/delete controls --- src/repo_registry/web_ui/views.py | 191 +++++++++++++++++++++++++++++- tests/test_web_api.py | 73 ++++++++++++ 2 files changed, 258 insertions(+), 6 deletions(-) diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py index 6f94e9c..060ed4f 100644 --- a/src/repo_registry/web_ui/views.py +++ b/src/repo_registry/web_ui/views.py @@ -305,7 +305,7 @@ def repository_detail(

Approved Ability Map

- {render_ability_map(asdict(ability_map))} + {render_ability_map(asdict(ability_map), repository_id)}
@@ -435,6 +435,128 @@ def create_evidence_from_form( return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) +@router.post("/ui/repos/{repository_id}/abilities/{ability_id}/edit") +def edit_ability_from_form( + repository_id: int, + ability_id: int, + name: str = Form(...), + description: str = Form(""), + confidence: float = Form(1.0), + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.update_ability( + repository_id, + ability_id, + name=name, + description=description, + confidence=confidence, + ) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + +@router.post("/ui/repos/{repository_id}/abilities/{ability_id}/delete") +def delete_ability_from_form( + repository_id: int, + ability_id: int, + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.delete_ability(repository_id, ability_id) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + +@router.post("/ui/repos/{repository_id}/capabilities/{capability_id}/edit") +def edit_capability_from_form( + repository_id: int, + capability_id: int, + name: str = Form(...), + description: str = Form(""), + inputs: str = Form(""), + outputs: str = Form(""), + confidence: float = Form(1.0), + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.update_capability( + repository_id, + capability_id, + name=name, + description=description, + inputs=split_csv(inputs), + outputs=split_csv(outputs), + confidence=confidence, + ) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + +@router.post("/ui/repos/{repository_id}/capabilities/{capability_id}/delete") +def delete_capability_from_form( + repository_id: int, + capability_id: int, + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.delete_capability(repository_id, capability_id) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + +@router.post("/ui/repos/{repository_id}/features/{feature_id}/edit") +def edit_feature_from_form( + repository_id: int, + feature_id: int, + name: str = Form(...), + type: str = Form(...), + location: str = Form(""), + confidence: float = Form(1.0), + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.update_feature( + repository_id, + feature_id, + name=name, + type=type, + location=location, + confidence=confidence, + ) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + +@router.post("/ui/repos/{repository_id}/features/{feature_id}/delete") +def delete_feature_from_form( + repository_id: int, + feature_id: int, + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.delete_feature(repository_id, feature_id) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + +@router.post("/ui/repos/{repository_id}/evidence/{evidence_id}/edit") +def edit_evidence_from_form( + repository_id: int, + evidence_id: int, + type: str = Form(...), + reference: str = Form(...), + strength: str = Form("medium"), + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.update_evidence( + repository_id, + evidence_id, + type=type, + reference=reference, + strength=strength, + ) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + +@router.post("/ui/repos/{repository_id}/evidence/{evidence_id}/delete") +def delete_evidence_from_form( + repository_id: int, + evidence_id: int, + service: RegistryService = Depends(get_service), +) -> RedirectResponse: + service.delete_evidence(repository_id, evidence_id) + return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) + + @router.post("/ui/repos/{repository_id}/analysis-runs") def create_analysis_run_from_form( repository_id: int, @@ -1070,7 +1192,7 @@ def render_candidate_merge_form( """ -def render_ability_map(ability_map: dict) -> str: +def render_ability_map(ability_map: dict, repository_id: int) -> str: abilities = ability_map.get("abilities", []) if not abilities: return '

No approved entries yet.

' @@ -1079,11 +1201,11 @@ def render_ability_map(ability_map: dict) -> str: capabilities = [] for capability in ability["capabilities"]: features = "".join( - render_approved_feature(feature) + render_approved_feature(feature, repository_id) for feature in capability["features"] ) evidence = "".join( - render_approved_evidence(item) + render_approved_evidence(item, repository_id) for item in capability["evidence"] ) capabilities.append( @@ -1092,6 +1214,7 @@ def render_ability_map(ability_map: dict) -> str: {escape(capability['name'])} ID {capability['id']}

{escape(capability['description'])}

+ {render_approved_capability_forms(capability, repository_id)} """ @@ -1102,6 +1225,7 @@ def render_ability_map(ability_map: dict) -> str: {escape(ability['name'])} ID {ability['id']}

{escape(ability['description'])}

+ {render_approved_ability_forms(ability, repository_id)} """ @@ -1109,24 +1233,79 @@ def render_ability_map(ability_map: dict) -> str: return f'
    {"".join(items)}
' -def render_approved_feature(feature: dict) -> str: +def render_approved_ability_forms(ability: dict, repository_id: int) -> str: + return f""" +
+ + + +
+ +
+
+
+ +
+ """ + + +def render_approved_capability_forms(capability: dict, repository_id: int) -> str: + inputs = ", ".join(capability["inputs"]) + outputs = ", ".join(capability["outputs"]) + return f""" +
+ + + + + +
+ +
+
+
+ +
+ """ + + +def render_approved_feature(feature: dict, repository_id: int) -> str: return f"""
  • {escape(feature["name"])} {escape(feature["type"])} {escape(feature["location"])} {render_sources(feature.get("source_refs", []))} +
    + + + + + +
    +
    + +
  • """ -def render_approved_evidence(evidence: dict) -> str: +def render_approved_evidence(evidence: dict, repository_id: int) -> str: return f"""
  • {escape(evidence["type"])} {escape(evidence["strength"])} {escape(evidence["reference"])} {render_sources(evidence.get("source_refs", []))} +
    + + + + +
    +
    + +
  • """ diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 806e2c4..2e731ba 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -506,6 +506,79 @@ def test_ui_manual_registry_entry_loop(tmp_path): assert "Manual API" in detail_response.text assert "README.md" in detail_response.text assert "ID " in detail_response.text + assert "Save Ability" in detail_response.text + + edit_ability_response = client.post( + f"{repository_path}/abilities/{ability_id}/edit", + data={ + "name": "Edited Manual Ability", + "description": "Edited by hand.", + "confidence": "0.8", + }, + follow_redirects=False, + ) + assert edit_ability_response.status_code == 303 + + edit_capability_response = client.post( + f"{repository_path}/capabilities/{capability_id}/edit", + data={ + "name": "Edited Manual Capability", + "description": "Edited capability.", + "inputs": "ticket", + "outputs": "decision", + "confidence": "0.75", + }, + follow_redirects=False, + ) + assert edit_capability_response.status_code == 303 + + ability_map = client.get(f"/repos/{repository_id}/ability-map").json() + feature_id = ability_map["abilities"][0]["capabilities"][0]["features"][0]["id"] + evidence_id = ability_map["abilities"][0]["capabilities"][0]["evidence"][0]["id"] + + edit_feature_response = client.post( + f"{repository_path}/features/{feature_id}/edit", + data={ + "name": "Edited Manual API", + "type": "HTTP endpoint", + "location": "src/edited.py", + "confidence": "0.7", + }, + follow_redirects=False, + ) + assert edit_feature_response.status_code == 303 + + edit_evidence_response = client.post( + f"{repository_path}/evidence/{evidence_id}/edit", + data={ + "type": "test", + "reference": "tests/test_manual.py", + "strength": "strong", + }, + follow_redirects=False, + ) + assert edit_evidence_response.status_code == 303 + + detail_response = client.get(repository_path) + assert "Edited Manual Ability" in detail_response.text + assert "Edited Manual Capability" in detail_response.text + assert "Edited Manual API" in detail_response.text + assert "tests/test_manual.py" in detail_response.text + + delete_feature_response = client.post( + f"{repository_path}/features/{feature_id}/delete", + follow_redirects=False, + ) + assert delete_feature_response.status_code == 303 + delete_evidence_response = client.post( + f"{repository_path}/evidence/{evidence_id}/delete", + follow_redirects=False, + ) + assert delete_evidence_response.status_code == 303 + + detail_response = client.get(repository_path) + assert "Edited Manual API" not in detail_response.text + assert "tests/test_manual.py" not in detail_response.text finally: app.dependency_overrides.clear()