Skip to content

Commit c22b79e

Browse files
olivermeyerclaude
andauthored
feat(gui): registry-based page registration with frame injection (#39)
* feat(gui): registry-based page registration with frame injection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(gui): async page function invoked via registry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(gui): extract _actualize_via_register_pages helper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4ed5c46 commit c22b79e

6 files changed

Lines changed: 590 additions & 143 deletions

File tree

ATTRIBUTIONS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ SOFTWARE.
360360

361361
```
362362

363-
## aignostics-foundry-core (0.8.1) - MIT License
363+
## aignostics-foundry-core (0.8.2) - MIT License
364364

365365
🏭 Foundational infrastructure for Foundry components.
366366

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# 5. GUI page registration
2+
3+
Date: 2026-04-08
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
Library consumers need to register NiceGUI pages with a frame that displays a consistent header,
12+
navigation sidebar, and health status bar. The frame function is inherently tied to the service,
13+
meaning it cannot be defined in a generic library like foundry-core.
14+
15+
The previous implementation relied on library consumers creating `gui = GUINamespace(frame_func=frame)` in
16+
their service. Since this library replaces the foundational components for services, there is no natural
17+
place to do this. In Bridge, this `GUINamespace` would be created in the system module and imported
18+
into other modules, violating domain boundaries.
19+
20+
### Alternatives considered
21+
22+
**Option A — Deferred singleton in foundry-core**: Add `gui = GUINamespace()` to
23+
`aignostics_foundry_core.gui` and a `configure(frame_func=...)` method. Feature modules import
24+
`gui` from foundry-core; services call `gui.configure(frame_func=frame)` at startup.
25+
Rejected: pollutes a generic library with a stateful singleton that must be mutated by the
26+
application layer. Complicates testing (global state to reset). Philosophically wrong: the
27+
singleton is specific to the service and should not live in a general-purpose library.
28+
29+
**Option B — New `gui` module in services**: Create a new module in each service as a neutral singleton
30+
home. Feature modules import from that module.
31+
Rejected: adds a thin wrapper module whose only content is a singleton. The module boundary
32+
problem is merely moved, not eliminated. No architectural gain justifies the additional module.
33+
34+
**Option C (chosen) — Registry-based page decorators**: The standalone `page_*` decorators in
35+
foundry-core (`page_authenticated`, `page_public`, etc.) write to a module-level `_registry`
36+
instead of calling `@ui.page()` immediately. `gui_register_pages(frame_func=frame)` processes
37+
the registry after all `BasePageBuilder.register_pages()` calls, actualizing each entry with
38+
the correct `frame_func`. Feature modules import only from `aignostics_foundry_core.gui`.
39+
Library consumers provide the `frame_func` to `gui_run()`, which flows it down to
40+
`gui_register_pages`. The `gui` singleton in library consumers is deleted entirely.
41+
42+
## Decision
43+
44+
Implement Option C. The standalone `page_*` decorators become pure registration decorators
45+
that record intent (path, title, access level, page function) to a module-level list. The
46+
`gui_register_pages(frame_func)` function actualizes all entries using the private
47+
`_actualize_*` functions. `GUINamespace` methods continue to call `_actualize_*` directly,
48+
bypassing the registry (preserving the existing opt-in, frame-at-construction-time API for
49+
any future consumers that prefer it).
50+
51+
`gui_run()` gains a `frame_func` parameter that is forwarded to `gui_register_pages`.
52+
Library consumers passe `frame_func=frame` when calling `gui_run()`.
53+
54+
## Consequences
55+
56+
**Easier**:
57+
- Feature modules have zero dependency on a `gui` module for page registration.
58+
- Dependency graph is clean: feature modules → `aignostics_foundry_core.gui`; library consumers
59+
orchestrate and provide the frame.
60+
- Adding a new page requires only `from aignostics_foundry_core.gui import page_authenticated`
61+
— no reference to any singleton or other service module.
62+
63+
**Harder / risks**:
64+
- Page registration is now a two-phase process (write to registry, then actualize). Code that
65+
calls `page_authenticated(path)(func)` and expects the route to be live immediately (without
66+
subsequently calling `gui_register_pages`) will silently not register the route.
67+
- `_registry` is module-level mutable state. Tests must call `clear_page_registry()` in
68+
teardown to avoid cross-test contamination.
69+
- `GUINamespace` now calls a different set of internal functions (`_actualize_*`) than the
70+
public `page_*` API, which is a maintenance surface to keep in sync.

src/aignostics_foundry_core/gui/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"""
99

1010
from .auth import (
11+
AccessLevel,
1112
GUINamespace,
13+
clear_page_registry,
1214
get_gui_user,
1315
gui,
1416
page_admin,
@@ -37,11 +39,13 @@
3739
"BROWSER_RECONNECT_TIMEOUT",
3840
"RESPONSE_TIMEOUT",
3941
"WINDOW_SIZE",
42+
"AccessLevel",
4043
"BaseNavBuilder",
4144
"BasePageBuilder",
4245
"GUINamespace",
4346
"NavGroup",
4447
"NavItem",
48+
"clear_page_registry",
4549
"get_gui_user",
4650
"gui",
4751
"gui_get_nav_groups",

0 commit comments

Comments
 (0)