You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by gs...@apache.org on 2026/08/12 23:55:49 UTC

(tooling-llmao) branch main updated: Show GET page errors as flashes via page_errors.

This is an automated email from the ASF dual-hosted git repository.

gstein pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-llmao.git


The following commit(s) were added to refs/heads/main by this push:
     new 29a76e2  Show GET page errors as flashes via page_errors.
29a76e2 is described below

commit 29a76e2ea16d15fcdca9f5817013790c9a41fd86
Author: Greg Stein <gs...@gmail.com>
AuthorDate: Wed Aug 12 18:55:44 2026 -0500

    Show GET page errors as flashes via page_errors.
    
    basic_info no longer drains Quart flashes up front, so a flash() after
    that call (including from the decorator) is still visible: page_errors
    catches AuthzError/BackendUnavailable, flashes, then attaches messages
    immediately before use_template renders. Drop result.error and the
    per-template error alerts; policy denials raise AuthzError.
    
    Co-authored-by: Grok <gr...@x.ai>
---
 pages.py                       | 142 +++++++++++++++++++++++------------------
 templates/key_create.ezt       |   4 --
 templates/key_create_other.ezt |   4 --
 templates/keys.ezt             |   6 --
 templates/keys_other.ezt       |   6 --
 templates/models.ezt           |   6 --
 templates/project.ezt          |   4 --
 templates/projects.ezt         |   6 --
 8 files changed, 81 insertions(+), 97 deletions(-)

diff --git a/pages.py b/pages.py
index f61c56d..909c708 100644
--- a/pages.py
+++ b/pages.py
@@ -54,12 +54,51 @@ def _render(template_name: str, data) -> str:
     return asfquart.utils.render(APP.load_template(TEMPLATES / template_name), data)
 
 
+def _flash_rows():
+    """Drain Quart flashes for EZT. Call immediately before use_template render."""
+    try:
+        msgs = quart.get_flashed_messages(with_categories=True)
+    except Exception:
+        return []
+    return [edict(category=c, message=m) for c, m in msgs]
+
+
+def page_errors(*extra_exc, category: str = "warning", title: str | None = None):
+    """Catch page failures, flash them, attach flashes after the view returns.
+
+    Innermost under ``@APP.use_template`` so ``flash()`` happens before EZT.
+    ``basic_info()`` does not drain flashes — this decorator does, last.
+    """
+    types = (AuthzError, BackendUnavailable) + extra_exc
+
+    def deco(fn):
+        @functools.wraps(fn)
+        async def wrapper(*args, **kwargs):
+            try:
+                data = await fn(*args, **kwargs)
+            except types as e:
+                await quart.flash(str(e), category)
+                data = await basic_info()
+                if title:
+                    data.title = title
+                if "project" in kwargs:
+                    data.project = kwargs["project"]
+                    if not title:
+                        data.title = kwargs["project"]
+            if data is not None and hasattr(data, "update"):
+                data.flashes = _flash_rows()
+            return data
+
+        return wrapper
+
+    return deco
+
+
 async def basic_info() -> edict:
     """Base-level EZT template data shared by HTML pages."""
     basic = edict()
     basic.title = "llmao"
     basic.flashes = []
-    basic.error = None
 
     # Form defaults for create pages
     basic.form_project = ""
@@ -67,12 +106,6 @@ async def basic_info() -> edict:
     basic.keys_back = "/keys"
     basic.keys_create_another = "/keys/new"
 
-    try:
-        msgs = quart.get_flashed_messages(with_categories=True)
-        basic.flashes = [edict({"category": c, "message": m}) for c, m in msgs]
-    except Exception:
-        basic.flashes = []
-
     client_session = await asfquart.session.read()
     if client_session is not None and getattr(client_session, "uid", None):
         basic.uid = client_session.uid
@@ -141,6 +174,7 @@ def _key_rows(keys: list[KeyInfo], *, after_path: str = "/keys") -> list:
 
 @APP.get("/")
 @APP.use_template(TEMPLATES / "home.ezt")
+@page_errors()
 async def home_page():
     result = await basic_info()
     result.title = "Home"
@@ -150,18 +184,15 @@ async def home_page():
 @APP.get("/models")
 @asfquart.auth.require
 @APP.use_template(TEMPLATES / "models.ezt")
+@page_errors(FileNotFoundError, ValueError, OSError, title="Models")
 async def models_page():
     """Gateway model inventory (public fields; supply path for site admins)."""
     result = await basic_info()
     result.title = "Models"
     result.models = []
-    result.error = None
     result.reveal_supply = bool(result.is_site_admin)
-    try:
-        raw = ux_models(cfg=APP.cfg, reveal_supply=result.reveal_supply)
-        result.models = [edict(m) for m in raw]
-    except (FileNotFoundError, ValueError, OSError) as e:
-        result.error = str(e)
+    raw = ux_models(cfg=APP.cfg, reveal_supply=result.reveal_supply)
+    result.models = [edict(m) for m in raw]
     return result
 
 
@@ -193,40 +224,34 @@ def _project_list_rows(rows) -> list:
 @APP.get("/projects")
 @asfquart.auth.require
 @APP.use_template(TEMPLATES / "projects.ezt")
+@page_errors(title="Projects")
 async def projects_list():
     """Projects you belong to, with project-budget summary."""
     result = await basic_info()
     result.title = "Projects"
     result.project_rows = []
-    result.error = None
-    try:
-        ident = await current_identity(APP.cfg)
-        assert ident is not None
-        seam = APP.config["LLMAO_SEAM"]
-        result.project_rows = _project_list_rows(
-            await seam.list_projects_for(ident)
-        )
-    except (AuthzError, BackendUnavailable) as e:
-        result.error = str(e)
+    ident = await current_identity(APP.cfg)
+    assert ident is not None
+    seam = APP.config["LLMAO_SEAM"]
+    result.project_rows = _project_list_rows(
+        await seam.list_projects_for(ident)
+    )
     return result
 
 
 @APP.get("/projects/<project>")
 @asfquart.auth.require
 @APP.use_template(TEMPLATES / "project.ezt")
+@page_errors()
 async def project_stub(project: str):
     """Member-gated stub until P0.3 overview."""
     result = await basic_info()
     result.title = project
     result.project = project
-    result.error = None
-    try:
-        ident = await current_identity(APP.cfg)
-        assert ident is not None
-        seam = APP.config["LLMAO_SEAM"]
-        await seam.team_status(ident, project)
-    except (AuthzError, BackendUnavailable) as e:
-        result.error = str(e)
+    ident = await current_identity(APP.cfg)
+    assert ident is not None
+    seam = APP.config["LLMAO_SEAM"]
+    await seam.team_status(ident, project)
     return result
 
 
@@ -261,58 +286,52 @@ async def _flash_key_created(created, *, kind_label: str, keys_back: str, keys_c
 @APP.get("/keys")
 @asfquart.auth.require
 @APP.use_template(TEMPLATES / "keys.ezt")
+@page_errors(title="My Keys")
 async def keys_list():
     """My Keys — personal PATs only (one list_keys call)."""
     result = await basic_info()
     result.title = "My Keys"
     result.keys = []
-    result.error = None
-    try:
-        ident = await current_identity(APP.cfg)
-        assert ident is not None
-        seam = APP.config["LLMAO_SEAM"]
-        my_keys = await seam.list_my_keys(ident)
-        result.keys = _key_rows(my_keys, after_path="/keys")
-    except (AuthzError, BackendUnavailable) as e:
-        result.error = str(e)
+    ident = await current_identity(APP.cfg)
+    assert ident is not None
+    seam = APP.config["LLMAO_SEAM"]
+    my_keys = await seam.list_my_keys(ident)
+    result.keys = _key_rows(my_keys, after_path="/keys")
     return result
 
 
 @APP.get("/keys/other")
 @asfquart.auth.require
 @APP.use_template(TEMPLATES / "keys_other.ezt")
+@page_errors(title="Other Keys")
 async def keys_other_list():
     """Other Keys — automation / team-scoped (PMC / site admin)."""
     result = await basic_info()
     result.title = "Other Keys"
     result.keys = []
-    result.error = None
     if not result.can_create_automation:
-        result.error = "Other Keys is limited to PMC members and site admins."
-        return result
-    try:
-        ident = await current_identity(APP.cfg)
-        assert ident is not None
-        seam = APP.config["LLMAO_SEAM"]
-        admin_projects = (
-            ident.all_projects() if ident.is_site_admin else list(ident.committees)
-        )
-        by_id: dict[str, KeyInfo] = {}
-        for p in admin_projects:
-            try:
-                for k in await seam.list_automation_keys(ident, p):
-                    by_id.setdefault(k.token_id, k)
-            except (AuthzError, BackendUnavailable):
-                continue
-        result.keys = _key_rows(list(by_id.values()), after_path="/keys/other")
-    except (AuthzError, BackendUnavailable) as e:
-        result.error = str(e)
+        raise AuthzError("Other Keys is limited to PMC members and site admins.")
+    ident = await current_identity(APP.cfg)
+    assert ident is not None
+    seam = APP.config["LLMAO_SEAM"]
+    admin_projects = (
+        ident.all_projects() if ident.is_site_admin else list(ident.committees)
+    )
+    by_id: dict[str, KeyInfo] = {}
+    for p in admin_projects:
+        try:
+            for k in await seam.list_automation_keys(ident, p):
+                by_id.setdefault(k.token_id, k)
+        except (AuthzError, BackendUnavailable):
+            continue
+    result.keys = _key_rows(list(by_id.values()), after_path="/keys/other")
     return result
 
 
 @APP.get("/keys/new")
 @asfquart.auth.require
 @APP.use_template(TEMPLATES / "key_create.ezt")
+@page_errors(title="Create personal key")
 async def keys_new_form():
     result = await basic_info()
     result.title = "Create personal key"
@@ -345,11 +364,12 @@ async def do_create_key():
 @APP.get("/keys/other/new")
 @asfquart.auth.require
 @APP.use_template(TEMPLATES / "key_create_other.ezt")
+@page_errors(title="Create automation key")
 async def keys_other_new_form():
     result = await basic_info()
     result.title = "Create automation key"
     if not result.can_create_automation:
-        result.error = "Only PMC members and site admins may create automation keys."
+        raise AuthzError("Only PMC members and site admins may create automation keys.")
     return result
 
 
diff --git a/templates/key_create.ezt b/templates/key_create.ezt
index 2acb81b..539836f 100644
--- a/templates/key_create.ezt
+++ b/templates/key_create.ezt
@@ -8,10 +8,6 @@
     password manager or secrets system.
   </p>
 
-  [if-any error]
-  <div class="alert alert-danger">[error]</div>
-  [end]
-
   <form method="post" action="/do-create-key" class="vstack gap-3">
     <div>
       <label class="form-label" for="project">Project</label>
diff --git a/templates/key_create_other.ezt b/templates/key_create_other.ezt
index 2e85fb6..d04ba76 100644
--- a/templates/key_create_other.ezt
+++ b/templates/key_create_other.ezt
@@ -9,10 +9,6 @@
     you will be recorded as <code>created_by</code>.
   </p>
 
-  [if-any error]
-  <div class="alert alert-danger">[error]</div>
-  [end]
-
   [if-any can_create_automation]
   <form method="post" action="/do-create-other-key" class="vstack gap-3">
     <div>
diff --git a/templates/keys.ezt b/templates/keys.ezt
index 3faf01e..e66db29 100644
--- a/templates/keys.ezt
+++ b/templates/keys.ezt
@@ -14,10 +14,6 @@
     attributed to you. The secret is shown once when you create the key.
   </p>
 
-  [if-any error]
-  <div class="alert alert-warning">[error]</div>
-  [end]
-
   [if-any keys]
   <div class="table-responsive">
     <table class="table table-sm table-hover align-middle">
@@ -55,10 +51,8 @@
     </table>
   </div>
   [else]
-  [if-any error][else]
   <p class="text-muted">No personal keys yet. <a href="/keys/new">Create one</a>.</p>
   [end]
-  [end]
 </div>
 
 <div class="modal fade" id="revokeModal" tabindex="-1" aria-labelledby="revokeModalLabel" aria-hidden="true">
diff --git a/templates/keys_other.ezt b/templates/keys_other.ezt
index 283b82b..28dab25 100644
--- a/templates/keys_other.ezt
+++ b/templates/keys_other.ezt
@@ -18,10 +18,6 @@
     <strong>Created by</strong> is who saw it and who teammates should ask.
   </p>
 
-  [if-any error]
-  <div class="alert alert-warning">[error]</div>
-  [end]
-
   [if-any keys]
   <div class="table-responsive">
     <table class="table table-sm table-hover align-middle">
@@ -61,14 +57,12 @@
     </table>
   </div>
   [else]
-  [if-any error][else]
   <p class="text-muted">No automation keys for your admin projects yet.
     [if-any can_create_automation]
     <a href="/keys/other/new">Create one</a>.
     [end]
   </p>
   [end]
-  [end]
 </div>
 
 <div class="modal fade" id="revokeModal" tabindex="-1" aria-labelledby="revokeModalLabel" aria-hidden="true">
diff --git a/templates/models.ezt b/templates/models.ezt
index 0d37140..9502dc0 100644
--- a/templates/models.ezt
+++ b/templates/models.ezt
@@ -10,10 +10,6 @@
     can call when configured.
   </p>
 
-  [if-any error]
-  <div class="alert alert-warning">[error]</div>
-  [end]
-
   [if-any models]
   <div class="table-responsive">
     <table class="table table-sm table-hover align-middle">
@@ -76,10 +72,8 @@
     </table>
   </div>
   [else]
-  [if-any error][else]
   <p class="text-muted">No models in the inventory.</p>
   [end]
-  [end]
 </div>
 
 <div class="modal fade" id="modelDetailModal" tabindex="-1"
diff --git a/templates/project.ezt b/templates/project.ezt
index a2c037d..a6f7304 100644
--- a/templates/project.ezt
+++ b/templates/project.ezt
@@ -7,12 +7,8 @@
   </p>
   <h1 class="h3 mb-3">[project]</h1>
 
-  [if-any error]
-  <div class="alert alert-warning">[error]</div>
-  [else]
   <p class="text-muted">
     Project overview (budget meters, by-person usage) is next.
   </p>
-  [end]
 </div>
 [include "footer.ezt"]
diff --git a/templates/projects.ezt b/templates/projects.ezt
index 81baff9..2f90714 100644
--- a/templates/projects.ezt
+++ b/templates/projects.ezt
@@ -8,10 +8,6 @@
     <strong>stewards</strong> for that project.
   </p>
 
-  [if-any error]
-  <div class="alert alert-warning">[error]</div>
-  [end]
-
   [if-any project_rows]
   <div class="table-responsive">
     <table class="table table-sm table-hover align-middle">
@@ -52,9 +48,7 @@
     </table>
   </div>
   [else]
-  [if-any error][else]
   <p class="text-muted">You're not listed on any ASF project.</p>
   [end]
-  [end]
 </div>
 [include "footer.ezt"]


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org