Layout optimization initial page

This commit is contained in:
2026-05-02 22:19:54 +02:00
parent bf2dc4ae98
commit 2ef9086c75
2 changed files with 92 additions and 32 deletions

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import asdict from dataclasses import asdict
from html import escape from html import escape
from pathlib import Path from pathlib import Path
from urllib.parse import quote_plus from urllib.parse import quote_plus, urlparse
from fastapi import APIRouter, Depends, Form, HTTPException, Query from fastapi import APIRouter, Depends, Form, HTTPException, Query
from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse
@@ -17,6 +17,25 @@ router = APIRouter(include_in_schema=False)
APP_NAME = "Repository Scoping" APP_NAME = "Repository Scoping"
def repository_directory_name(url: str, fallback: str) -> str:
parsed = urlparse(url)
path = parsed.path if parsed.scheme or parsed.netloc else url
normalized = path.replace("\\", "/").rstrip("/")
name = normalized.rsplit("/", 1)[-1] if normalized else ""
if name.endswith(".git"):
name = name[:-4]
return name or fallback
def repository_display_name(repository) -> str:
return repository_directory_name(repository.url, repository.name)
def repository_dict_display_name(repository: dict) -> str:
fallback = str(repository.get("name") or repository.get("repository_name") or "")
return repository_directory_name(str(repository.get("url") or ""), fallback)
def page( def page(
title: str, title: str,
body: str, body: str,
@@ -238,7 +257,7 @@ def render_repository_index(
rows = "\n".join( rows = "\n".join(
f""" f"""
<tr> <tr>
<td><a href="/ui/repos/{repo.id}">{escape(repo.name)}</a></td> <td><a href="/ui/repos/{repo.id}">{escape(repository_display_name(repo))}</a></td>
<td><span class="pill">{escape(repo.status)}</span></td> <td><span class="pill">{escape(repo.status)}</span></td>
<td class="source">{escape(repo.branch)}</td> <td class="source">{escape(repo.branch)}</td>
<td class="source">{escape(repo.url)}</td> <td class="source">{escape(repo.url)}</td>
@@ -260,6 +279,15 @@ def render_repository_index(
<h1>Repositories</h1> <h1>Repositories</h1>
{error} {error}
<div class="grid"> <div class="grid">
<section class="panel">
<div class="actions">
<h2 style="margin-right:auto">Registry</h2>
</div>
<table>
<thead><tr><th>Name</th><th>Status</th><th>Branch</th><th>Source</th></tr></thead>
<tbody>{rows or '<tr><td colspan="4" class="muted">No repositories yet.</td></tr>'}</tbody>
</table>
</section>
<section class="panel"> <section class="panel">
<h2>Register Repository</h2> <h2>Register Repository</h2>
<form class="stack" method="post" action="/ui/repos"> <form class="stack" method="post" action="/ui/repos">
@@ -276,16 +304,6 @@ def render_repository_index(
</div> </div>
</form> </form>
</section> </section>
<section class="panel">
<div class="actions">
<h2 style="margin-right:auto">Registry</h2>
<a class="button secondary" href="/ui/discovery">Discovery</a>
</div>
<table>
<thead><tr><th>Name</th><th>Status</th><th>Branch</th><th>Source</th></tr></thead>
<tbody>{rows or '<tr><td colspan="4" class="muted">No repositories yet.</td></tr>'}</tbody>
</table>
</section>
</div> </div>
""" """
response = page("Repositories", body) response = page("Repositories", body)
@@ -323,6 +341,7 @@ def repository_scope_document(
service: RegistryService = Depends(get_service), service: RegistryService = Depends(get_service),
) -> HTMLResponse: ) -> HTMLResponse:
repository = service.get_repository(repository_id) repository = service.get_repository(repository_id)
display_name = repository_display_name(repository)
checkout = service.ingestion.cached_checkout(repository.url) checkout = service.ingestion.cached_checkout(repository.url)
if checkout is None: if checkout is None:
rendered = ( rendered = (
@@ -342,14 +361,14 @@ def repository_scope_document(
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 the {escape(repository.name)} repository.</p> <p class="muted">Canonical scope summary for the {escape(display_name)} repository.</p>
{rendered} {rendered}
</section> </section>
""" """
return page( return page(
"SCOPE.md", "SCOPE.md",
body, body,
selected_repository=repository.name, selected_repository=display_name,
selected_repository_id=repository.id, selected_repository_id=repository.id,
) )
@@ -721,6 +740,7 @@ def repository_detail(
repository = service.get_repository(repository_id) repository = service.get_repository(repository_id)
except NotFoundError as exc: except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc raise HTTPException(status_code=404, detail=str(exc)) from exc
display_name = repository_display_name(repository)
runs = service.list_analysis_runs(repository_id) runs = service.list_analysis_runs(repository_id)
ability_map = service.ability_map(repository_id) ability_map = service.ability_map(repository_id)
latest_candidate = latest_completed_candidate_graph( latest_candidate = latest_completed_candidate_graph(
@@ -746,7 +766,7 @@ def repository_detail(
) )
body = f""" body = f"""
<div class="actions"> <div class="actions">
<h1 style="margin-right:auto">{escape(repository.name)}</h1> <h1 style="margin-right:auto">{escape(display_name)}</h1>
<a class="button secondary" href="/ui/repos/{repository_id}/export">Export</a> <a class="button secondary" href="/ui/repos/{repository_id}/export">Export</a>
<a class="button secondary" href="/ui/repos/{repository_id}/scope">SCOPE</a> <a class="button secondary" href="/ui/repos/{repository_id}/scope">SCOPE</a>
<a class="button secondary" href="/ui">Back</a> <a class="button secondary" href="/ui">Back</a>
@@ -889,9 +909,9 @@ def repository_detail(
</section> </section>
""" """
return page( return page(
repository.name, display_name,
body, body,
selected_repository=repository.name, selected_repository=display_name,
selected_repository_id=repository.id, selected_repository_id=repository.id,
) )
@@ -1254,6 +1274,7 @@ def analysis_run_detail(
service: RegistryService = Depends(get_service), service: RegistryService = Depends(get_service),
) -> HTMLResponse: ) -> HTMLResponse:
repository = service.get_repository(repository_id) repository = service.get_repository(repository_id)
display_name = repository_display_name(repository)
candidate_graph = service.candidate_graph(repository_id, analysis_run_id) candidate_graph = service.candidate_graph(repository_id, analysis_run_id)
candidate_graph_data = asdict(candidate_graph) candidate_graph_data = asdict(candidate_graph)
facts = service.list_observed_facts(repository_id, analysis_run_id) facts = service.list_observed_facts(repository_id, analysis_run_id)
@@ -1273,7 +1294,7 @@ def analysis_run_detail(
) )
body = f""" body = f"""
<div class="actions"> <div class="actions">
<h1 style="margin-right:auto">{escape(repository.name)} · Run #{analysis_run_id}</h1> <h1 style="margin-right:auto">{escape(display_name)} · Run #{analysis_run_id}</h1>
{render_run_detail_compare_link(repository_id, analysis_run_id, service.list_analysis_runs(repository_id))} {render_run_detail_compare_link(repository_id, analysis_run_id, service.list_analysis_runs(repository_id))}
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a> <a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
</div> </div>
@@ -1346,9 +1367,9 @@ def analysis_run_detail(
</section> </section>
""" """
return page( return page(
f"{repository.name} Run {analysis_run_id}", f"{display_name} Run {analysis_run_id}",
body, body,
selected_repository=repository.name, selected_repository=display_name,
selected_repository_id=repository.id, selected_repository_id=repository.id,
) )
@@ -1411,9 +1432,10 @@ def repository_element_listing(
service: RegistryService = Depends(get_service), service: RegistryService = Depends(get_service),
) -> HTMLResponse: ) -> HTMLResponse:
repository = service.get_repository(repository_id) repository = service.get_repository(repository_id)
display_name = repository_display_name(repository)
if scope in {"approved", "candidate"} and not entry_filter: if scope in {"approved", "candidate"} and not entry_filter:
entry_filter = scope entry_filter = scope
title = element_listing_title(repository.name, scope, type) title = element_listing_title(display_name, scope, type)
if scope in {"all", "approved", "candidate"}: if scope in {"all", "approved", "candidate"}:
elements = graph_element_rows( elements = graph_element_rows(
asdict(service.ability_map(repository_id)), asdict(service.ability_map(repository_id)),
@@ -1519,7 +1541,7 @@ def repository_element_listing(
return page( return page(
title, title,
body, body,
selected_repository=repository.name, selected_repository=display_name,
selected_repository_id=repository.id, selected_repository_id=repository.id,
) )
@@ -1539,9 +1561,10 @@ def analysis_run_diff_detail(
) )
except NotFoundError as exc: except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc raise HTTPException(status_code=404, detail=str(exc)) from exc
display_name = repository_display_name(diff.repository)
body = f""" body = f"""
<div class="actions"> <div class="actions">
<h1 style="margin-right:auto">{escape(diff.repository.name)} · Change Review</h1> <h1 style="margin-right:auto">{escape(display_name)} · Change Review</h1>
<a class="button secondary" href="/ui/repos/{repository_id}/analysis-runs/{target_analysis_run_id}">Target Run</a> <a class="button secondary" href="/ui/repos/{repository_id}/analysis-runs/{target_analysis_run_id}">Target Run</a>
<a class="button secondary" href="/ui/repos/{repository_id}">Repository</a> <a class="button secondary" href="/ui/repos/{repository_id}">Repository</a>
</div> </div>
@@ -1575,9 +1598,9 @@ def analysis_run_diff_detail(
</div> </div>
""" """
return page( return page(
f"{diff.repository.name} Change Review", f"{display_name} Change Review",
body, body,
selected_repository=diff.repository.name, selected_repository=display_name,
selected_repository_id=diff.repository.id, selected_repository_id=diff.repository.id,
) )
@@ -2147,7 +2170,7 @@ def render_repository_checkbox_list(
<label> <label>
<span> <span>
<input style="width:auto" type="checkbox" name="repository_ids" value="{repository.id}"{disabled}> <input style="width:auto" type="checkbox" name="repository_ids" value="{repository.id}"{disabled}>
{escape(repository.name)} {escape(repository_display_name(repository))}
</span> </span>
<span class="muted">{escape(status)}</span> <span class="muted">{escape(status)}</span>
</label> </label>
@@ -2162,7 +2185,7 @@ def render_compared_repositories(repositories: list[dict]) -> str:
rows = "\n".join( rows = "\n".join(
f""" f"""
<tr> <tr>
<td><a href="/ui/repos/{repository['id']}">{escape(repository['name'])}</a></td> <td><a href="/ui/repos/{repository['id']}">{escape(repository_dict_display_name(repository))}</a></td>
<td><span class="pill">{escape(repository['status'])}</span></td> <td><span class="pill">{escape(repository['status'])}</span></td>
<td class="source">{escape(repository['branch'])}</td> <td class="source">{escape(repository['branch'])}</td>
</tr> </tr>

View File

@@ -323,6 +323,43 @@ def test_ui_uses_repository_scoping_brand():
assert "Repository Ability Registry" not in response.text assert "Repository Ability Registry" not in response.text
def test_ui_homepage_registry_panel_uses_directory_identifier_and_left_column(tmp_path):
source = tmp_path / "short-dir"
source.mkdir()
(source / "README.md").write_text("# Short Dir\n", encoding="utf-8")
def override_settings():
return Settings(
database_path=str(tmp_path / "ui-home.sqlite3"),
checkout_root=str(tmp_path / "ui-home-checkouts"),
)
app.dependency_overrides[get_settings] = override_settings
client = TestClient(app)
try:
response = client.post(
"/repos",
json={
"name": "Very Long Marketing Project Name",
"url": str(source),
},
)
assert response.status_code == 201
homepage = client.get("/ui")
assert homepage.status_code == 200
assert homepage.text.index(">Registry<") < homepage.text.index(">Register Repository<")
registry_panel = homepage.text[
homepage.text.index(">Registry<") : homepage.text.index(">Register Repository<")
]
assert "short-dir" in registry_panel
assert "Very Long Marketing Project Name" not in registry_panel
assert "Discovery" not in registry_panel
finally:
app.dependency_overrides.clear()
def test_ui_scope_page_presents_scope_md(): def test_ui_scope_page_presents_scope_md():
client = TestClient(app) client = TestClient(app)
@@ -1281,10 +1318,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
repo_scope_response = client.get(f"/ui/repos/{repository_id}/scope") repo_scope_response = client.get(f"/ui/repos/{repository_id}/scope")
assert repo_scope_response.status_code == 200 assert repo_scope_response.status_code == 200
assert ( assert (
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo</a>' f'<a class="header-context" href="/ui/repos/{repository_id}">repo</a>'
in repo_scope_response.text in repo_scope_response.text
) )
assert "Canonical scope summary for the UI Repo repository." in repo_scope_response.text assert "Canonical scope summary for the repo repository." in repo_scope_response.text
assert "UI Repo owns the status reporting scope." in repo_scope_response.text assert "UI Repo owns the status reporting scope." in repo_scope_response.text
edit_repository_response = client.post( edit_repository_response = client.post(
@@ -1378,7 +1415,7 @@ 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 ( assert (
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo Edited</a>' f'<a class="header-context" href="/ui/repos/{repository_id}">repo</a>'
in approved_detail.text in approved_detail.text
) )
assert "Approved Characteristics" in approved_detail.text assert "Approved Characteristics" in approved_detail.text
@@ -1409,7 +1446,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
) )
assert scope_listing.status_code == 200 assert scope_listing.status_code == 200
assert ( assert (
f'<a class="header-context" href="/ui/repos/{repository_id}">UI Repo Edited</a>' f'<a class="header-context" href="/ui/repos/{repository_id}">repo</a>'
in scope_listing.text in scope_listing.text
) )
assert f'<a href="/ui/repos/{repository_id}">UI Repo</a>' in scope_listing.text assert f'<a href="/ui/repos/{repository_id}">UI Repo</a>' in scope_listing.text
@@ -2362,7 +2399,7 @@ def test_ui_discovery_compare_gap_and_export(tmp_path):
assert discovery.status_code == 200 assert discovery.status_code == 200
assert "Compare Repositories" in discovery.text assert "Compare Repositories" in discovery.text
assert "Capability Gap Report" in discovery.text assert "Capability Gap Report" in discovery.text
assert "EmptyProfile" in discovery.text assert "empty-profile-ui" in discovery.text
assert "No approved profile" in discovery.text assert "No approved profile" in discovery.text
comparison = client.get( comparison = client.get(