Summary
Reorganize the audera/server/ package into audera/ui/, extract shared NiceGUI components, and redesign the CLI from a verb-first (run streamer-server) to a subject-first (streamer start) interface. This is a breaking-change refactor with no behavior changes.
Why
The server/ name is a misnomer. The package contains NiceGUI web UIs — not network servers, RPC listeners, or daemons. Callers and contributors have to inspect the code to understand what lives there. The real servers (Snapcast, CamillaDSP, PlexAmp) are external processes; what Audera ships is a UI layer on top of them.
The CLI surface is backwards. audera run streamer-server puts the verb first and buries the subject in a compound noun. Users think in subjects: "I want to do something with the streamer" — not "I want to run something called a streamer-server". The proposed audera streamer start follows the same convention used by docker container start, systemctl start, kubectl pod get, and virtually every modern CLI.
There is no shared UI layer. server/setup.py and server/streamer/app.py both use NiceGUI but define their own header, card layouts, and color choices independently. Any future UI would continue this pattern. Common structure should live in one place.
Architecture
Current layout
audera/
├── server/
│ ├── __init__.py
│ ├── setup.py # Wi-Fi onboarding wizard (3-page NiceGUI app)
│ └── streamer/
│ ├── __init__.py
│ └── app.py # Streamer control UI (Players / Services / Settings tabs)
└── cli/
├── audera.py # argparse definition
└── commands.py # run(), conf() handlers
Proposed layout
audera/
├── ui/ # renamed from server/
│ ├── __init__.py
│ ├── components/ # NEW — shared NiceGUI primitives
│ │ ├── __init__.py
│ │ ├── theme.py # color tokens, spacing, typography helpers
│ │ └── header.py # shared page header component
│ ├── setup/ # was server/setup.py — promoted to package
│ │ ├── __init__.py # run(role) entry point (unchanged public API)
│ │ └── pages.py # welcome(), connect(), finish() page factories
│ └── streamer/
│ ├── __init__.py
│ └── app.py # unchanged content; updated imports + shared components
└── cli/
├── __init__.py
├── audera.py # restructured subparser hierarchy
└── commands.py # handlers reorganized by subject
Design choices
1 — ui/ not app/ or web/
app/ conflicts with the common pattern of an app object (Flask, FastAPI, NiceGUI). web/ implies HTTP assets only. ui/ is precise: it holds the user-facing interface layer, whether that interface is served over HTTP or rendered locally.
2 — ui/setup/ as a package, not a module
The setup wizard already has three logical pages (welcome, connect, finish) plus a stateful Page class. Splitting it into __init__.py (entry point) and pages.py (page implementations) separates the public surface from the internals without requiring callers to change.
3 — Shared components extracted, not imposed
ui/components/ is a library of helpers, not a framework. Each UI imports what it needs; nothing is wired automatically. This prevents the components from becoming a hidden dependency on import order or global state.
4 — CLI: {subject} {verb}, not {verb} {noun}
# Before
audera run streamer-server
audera run player-setup
audera conf streamer snapserver.conf
audera conf player camilladsp.yml
# After
audera streamer start
audera streamer conf snapserver.conf
audera player setup
audera player conf camilladsp.yml
Each subject (streamer, player) becomes a subcommand group. Verbs live inside groups. This is consistent with git remote add, docker image pull, and kubectl pod describe.
Implementation details
ui/components/theme.py
PRIMARY = '#1a1a2e'
SECONDARY = '#16213e'
ACCENT = '#0f3460'
TEXT = '#e0e0e0'
MUTED = '#888888'
def apply_defaults() -> None:
from nicegui import ui
ui.colors(primary=PRIMARY, secondary=SECONDARY, accent=ACCENT)
ui/components/header.py
from nicegui import ui
def render(title: str, subtitle: str) -> None:
with ui.header().classes('items-center justify-between'):
ui.label('audera').classes('text-h6 text-weight-bold')
ui.label(subtitle).classes('text-caption text-grey')
ui/setup/__init__.py (public surface unchanged)
from audera.ui.setup.pages import welcome, connect, finish
from audera.ui import components
def run(role: str) -> None:
components.theme.apply_defaults()
# ... existing AccessPoint setup, ui.run() call
ui/setup/pages.py (extracted from current setup.py)
# welcome(), connect(), finish() extracted from the Page class methods,
# registered as @ui.page('/'), @ui.page('/connect'), @ui.page('/finish').
cli/audera.py — new subparser structure
import argparse
from audera.cli import commands
def main() -> None:
parser = argparse.ArgumentParser(prog='audera')
subs = parser.add_subparsers(dest='subject', required=True)
# audera streamer ...
streamer = subs.add_parser('streamer', help='Manage the audera streamer')
streamer_subs = streamer.add_subparsers(dest='verb', required=True)
s_start = streamer_subs.add_parser('start', help='Start the streamer web UI')
s_start.set_defaults(func=commands.streamer_start)
s_conf = streamer_subs.add_parser('conf', help='Print a bundled streamer config')
s_conf.add_argument('filename')
s_conf.set_defaults(func=commands.streamer_conf)
# audera player ...
player = subs.add_parser('player', help='Manage an audera player')
player_subs = player.add_subparsers(dest='verb', required=True)
p_setup = player_subs.add_parser('setup', help='Run the device Wi-Fi setup wizard')
p_setup.set_defaults(func=commands.player_setup)
p_conf = player_subs.add_parser('conf', help='Print a bundled player config')
p_conf.add_argument('filename')
p_conf.set_defaults(func=commands.player_conf)
args = parser.parse_args()
args.func(**vars(args))
cli/commands.py — renamed handlers (logic unchanged)
# Before: run(type, **_) and conf(type, filename, **_) dispatching on a type string
# After: one function per command; no dispatch table
def streamer_start(**_) -> None:
from audera import netifaces
from audera.ui import streamer, setup
if not netifaces.connected():
setup.run(role='streamer')
streamer.run()
def streamer_conf(filename: str, **_) -> None:
# unchanged body; was commands.conf(type='streamer', filename=filename)
...
def player_setup(**_) -> None:
from audera.ui import setup
setup.run(role='player')
def player_conf(filename: str, **_) -> None:
# unchanged body; was commands.conf(type='player', filename=filename)
...
Workstreams
WS1 is a prerequisite for WS2 and WS3, which are otherwise independent.
WS1 — Directory restructure and import surgery (~400–500 lines)
Scope: Rename server/ → ui/, split setup.py into a package, update all internal imports. No logic changes.
Files changed:
audera/server/ → audera/ui/ (move entire tree)
audera/server/setup.py → audera/ui/setup/__init__.py + audera/ui/setup/pages.py
audera/server/streamer/app.py → audera/ui/streamer/app.py (import paths only)
audera/__init__.py — update any server re-exports
audera/cli/commands.py — update from audera.server.* → from audera.ui.*
tests/ — update any test imports
Acceptance criteria:
uv run ruff check --fix && uv run ruff format && uv run ty check passes with zero errors
audera run streamer-server still works (old CLI preserved during transition)
audera run player-setup still works
WS2 — Shared UI components (~300–400 lines)
Prerequisite: WS1
Scope: Create ui/components/theme.py and ui/components/header.py. Refactor ui/setup/ and ui/streamer/app.py to use them. Remove all inline color/style duplication.
Files changed:
audera/ui/components/__init__.py — new
audera/ui/components/theme.py — new; color tokens + apply_defaults()
audera/ui/components/header.py — new; render(title, subtitle) function
audera/ui/setup/__init__.py — call theme.apply_defaults(); replace inline header with header.render()
audera/ui/streamer/app.py — same replacements
Acceptance criteria:
- Both UIs render the same header markup (verified by inspection)
uv run ty check passes
- No inline hex color literals remain in
ui/setup/ or ui/streamer/
WS3 — CLI redesign (~300–400 lines)
Prerequisite: WS1
Scope: Replace the {verb} {noun} argparse layout with {subject} {verb} subparser groups. Remove the old run and conf top-level commands. This is a breaking change — document in CHANGELOG and bump the minor version.
Files changed:
audera/cli/audera.py — new subparser structure (see code example above)
audera/cli/commands.py — rename run() → streamer_start(), player_setup(); rename conf() → streamer_conf(), player_conf()
README.md — update all CLI usage examples
CHANGELOG.md — document breaking change
tests/ — update or add CLI dispatch tests
Acceptance criteria:
audera streamer start invokes the streamer UI
audera player setup invokes the Wi-Fi wizard
audera streamer conf snapserver.conf prints the config to stdout
- Old commands (
audera run streamer-server) raise a clear argparse error
uv run ty check passes
Summary
Reorganize the
audera/server/package intoaudera/ui/, extract shared NiceGUI components, and redesign the CLI from a verb-first (run streamer-server) to a subject-first (streamer start) interface. This is a breaking-change refactor with no behavior changes.Why
The
server/name is a misnomer. The package contains NiceGUI web UIs — not network servers, RPC listeners, or daemons. Callers and contributors have to inspect the code to understand what lives there. The real servers (Snapcast, CamillaDSP, PlexAmp) are external processes; what Audera ships is a UI layer on top of them.The CLI surface is backwards.
audera run streamer-serverputs the verb first and buries the subject in a compound noun. Users think in subjects: "I want to do something with the streamer" — not "I want to run something called a streamer-server". The proposedaudera streamer startfollows the same convention used bydocker container start,systemctl start,kubectl pod get, and virtually every modern CLI.There is no shared UI layer.
server/setup.pyandserver/streamer/app.pyboth use NiceGUI but define their own header, card layouts, and color choices independently. Any future UI would continue this pattern. Common structure should live in one place.Architecture
Current layout
Proposed layout
Design choices
1 —
ui/notapp/orweb/app/conflicts with the common pattern of anappobject (Flask, FastAPI, NiceGUI).web/implies HTTP assets only.ui/is precise: it holds the user-facing interface layer, whether that interface is served over HTTP or rendered locally.2 —
ui/setup/as a package, not a moduleThe setup wizard already has three logical pages (
welcome,connect,finish) plus a statefulPageclass. Splitting it into__init__.py(entry point) andpages.py(page implementations) separates the public surface from the internals without requiring callers to change.3 — Shared components extracted, not imposed
ui/components/is a library of helpers, not a framework. Each UI imports what it needs; nothing is wired automatically. This prevents the components from becoming a hidden dependency on import order or global state.4 — CLI:
{subject} {verb}, not{verb} {noun}Each subject (
streamer,player) becomes a subcommand group. Verbs live inside groups. This is consistent withgit remote add,docker image pull, andkubectl pod describe.Implementation details
ui/components/theme.pyui/components/header.pyui/setup/__init__.py(public surface unchanged)ui/setup/pages.py(extracted from currentsetup.py)cli/audera.py— new subparser structurecli/commands.py— renamed handlers (logic unchanged)Workstreams
WS1 is a prerequisite for WS2 and WS3, which are otherwise independent.
WS1 — Directory restructure and import surgery (~400–500 lines)
Scope: Rename
server/→ui/, splitsetup.pyinto a package, update all internal imports. No logic changes.Files changed:
audera/server/→audera/ui/(move entire tree)audera/server/setup.py→audera/ui/setup/__init__.py+audera/ui/setup/pages.pyaudera/server/streamer/app.py→audera/ui/streamer/app.py(import paths only)audera/__init__.py— update anyserverre-exportsaudera/cli/commands.py— updatefrom audera.server.*→from audera.ui.*tests/— update any test importsAcceptance criteria:
uv run ruff check --fix && uv run ruff format && uv run ty checkpasses with zero errorsaudera run streamer-serverstill works (old CLI preserved during transition)audera run player-setupstill worksWS2 — Shared UI components (~300–400 lines)
Prerequisite: WS1
Scope: Create
ui/components/theme.pyandui/components/header.py. Refactorui/setup/andui/streamer/app.pyto use them. Remove all inline color/style duplication.Files changed:
audera/ui/components/__init__.py— newaudera/ui/components/theme.py— new; color tokens +apply_defaults()audera/ui/components/header.py— new;render(title, subtitle)functionaudera/ui/setup/__init__.py— calltheme.apply_defaults(); replace inline header withheader.render()audera/ui/streamer/app.py— same replacementsAcceptance criteria:
uv run ty checkpassesui/setup/orui/streamer/WS3 — CLI redesign (~300–400 lines)
Prerequisite: WS1
Scope: Replace the
{verb} {noun}argparse layout with{subject} {verb}subparser groups. Remove the oldrunandconftop-level commands. This is a breaking change — document inCHANGELOGand bump the minor version.Files changed:
audera/cli/audera.py— new subparser structure (see code example above)audera/cli/commands.py— renamerun()→streamer_start(),player_setup(); renameconf()→streamer_conf(),player_conf()README.md— update all CLI usage examplesCHANGELOG.md— document breaking changetests/— update or add CLI dispatch testsAcceptance criteria:
audera streamer startinvokes the streamer UIaudera player setupinvokes the Wi-Fi wizardaudera streamer conf snapserver.confprints the config to stdoutaudera run streamer-server) raise a clearargparseerroruv run ty checkpasses