Skip to content

Commit c60aa8b

Browse files
committed
Dashboard app: Modern as the new default theme
1 parent 26f389b commit c60aa8b

28 files changed

Lines changed: 4606 additions & 390 deletions

apps/_dashboard/DASHBOARD_GUIDE.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,15 @@ export PY4WEB_PASSWORD_FILE=password.txt
149149

150150
## Theming
151151

152-
The dashboard supports multiple built-in themes (AlienDark, AlienLight, Classic) with a fully extensible theming system. Users can switch themes dynamically without page reload, with preferences persisted to the backend.
152+
The dashboard supports multiple built-in themes (AlienDark, AlienLight, Classic, Modern) with a fully extensible theming system. Users can switch themes dynamically without page reload, with preferences persisted to the backend.
153153

154154
**For comprehensive theming documentation**, see [THEMES_GUIDE.md](THEMES_GUIDE.md).
155155

156156
### Quick Overview
157157

158158
**Currently Available Themes:**
159-
- **AlienDark** - Modern dark theme with cyan accents (default)
159+
- **Modern** - Light activity-bar layout with green and orange accents (default)
160+
- **AlienDark** - Modern dark theme with cyan accents
160161
- **AlienLight** - Professional light theme with blue accents
161162
- **Classic** - Legacy-inspired dashboard appearance
162163

@@ -221,14 +222,14 @@ static/themes/{ThemeName}/
221222
4. **Optional - Add assets:** `favicon.ico`, `widget.gif`, `templates/partials/` hook overrides
222223
5. **Optional - Add behavior:** `theme.js` for theme-specific JavaScript
223224

224-
**That's it!** The theme automatically appears in the dropdown.
225+
**That's it!** The theme automatically appears in the settings dropdown.
225226

226227
### Theme Selector UI
227228

228-
Dropdowns appear on all dashboard pages:
229+
Theme selector dropdown is rendered in the settings page (`templates/settings.html`) and synchronized through `data-theme-selector`:
229230

230231
```html
231-
<select id="dashboard-theme-select" data-theme-selector onchange="setDashboardTheme(this.value)">
232+
<select id="theme-select" data-theme-selector onchange="setDashboardTheme(this.value)">
232233
[[for theme in themes:]]
233234
<option value="[[=theme]]">[[=theme]]</option>
234235
[[pass]]
@@ -277,7 +278,7 @@ Theme selection is saved in two places:
277278

278279
1. **Backend** - `apps/_dashboard/user_settings.toml`
279280
```toml
280-
selected_theme = "AlienDark"
281+
selected_theme = "Modern"
281282
```
282283
Survives browser cache clear / private browsing
283284

@@ -287,7 +288,9 @@ Theme selection is saved in two places:
287288
**Selection Priority:**
288289
1. Backend setting (from `user_settings.toml`)
289290
2. Browser storage (from localStorage)
290-
3. Default theme (AlienDark if available, else first alphabetically)
291+
3. Default theme (Modern if available, else first alphabetically)
292+
293+
Backend persistence is attempted only for authenticated sessions (`USER_ID` available). Without login, selection remains in localStorage only.
291294

292295
Themes should not ship full-page replacements for the dashboard main pages; keep the main `index.html` in `apps/_dashboard/templates/` and use `templates/partials/` hooks for small structural overrides.
293296

apps/_dashboard/THEMES_GUIDE.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ The `_dashboard` app supports dynamic theme switching with persisted user prefer
66
Themes are discovered from `static/themes/` and can be switched without a page reload.
77

88
Current built-in themes:
9-
- **AlienDark** (default dark UI)
9+
- **Modern** (default light activity-bar UI with green primary and orange highlight accents)
10+
- **AlienDark** (dark UI)
1011
- **AlienLight** (light UI)
1112
- **Classic** (legacy-inspired dashboard style)
1213

@@ -110,9 +111,14 @@ Key responsibilities:
110111
- load optional `theme.js`
111112
- persist selection via `POST ../save_theme`
112113
- keep all selectors (`[data-theme-selector]`) synchronized
114+
- expose shared helpers through `window.DashboardThemeUtils`
113115

114116
Public API:
115117
- `window.setDashboardTheme(theme)`
118+
- `window.DashboardThemeUtils.getDashboardBaseUrl()`
119+
- `window.DashboardThemeUtils.getDashboardRelativeBase()`
120+
- `window.DashboardThemeUtils.getDashboardViewUrl(appName, view)`
121+
- `window.DashboardThemeUtils.getActiveThemeName()`
116122

117123
---
118124

@@ -130,9 +136,9 @@ Key functions:
130136

131137
Backend normalization guarantees that stale values (for example removed themes) do not break rendering.
132138
If a stored theme is unavailable:
133-
1. use `AlienDark` if present
134-
2. otherwise use the first available theme
135-
3. otherwise fall back to `AlienDark`
139+
1. use `Modern` if present
140+
2. otherwise use the first theme alphabetically
141+
3. otherwise fall back to `Modern`
136142

137143
`save_theme` also validates that the requested theme exists.
138144

@@ -167,12 +173,20 @@ In dashboard templates, keep these elements:
167173

168174
```html
169175
<script>
170-
var SELECTED_THEME = '[[= selected_theme or "AlienDark" ]]';
176+
var SELECTED_THEME = '[[= selected_theme or "Modern" ]]';
171177
</script>
172178

173-
<link id="dashboard-theme" rel="stylesheet" href="themes/[[= selected_theme or 'AlienDark' ]]/theme.css">
179+
<link id="dashboard-theme" rel="stylesheet" href="themes/[[= selected_theme or 'Modern' ]]/theme.css">
180+
```
181+
182+
Theme selector dropdowns are optional and are currently rendered in:
183+
184+
- `apps/_dashboard/templates/settings.html`
174185

175-
<select id="dashboard-theme-select" data-theme-selector onchange="setDashboardTheme(this.value)">
186+
Selector example used in settings:
187+
188+
```html
189+
<select id="theme-select" name="theme" data-theme-selector onchange="setDashboardTheme(this.value)">
176190
[[for theme in themes:]]
177191
<option value="[[=theme]]" [[='selected' if theme == selected_theme else '']]>[[=theme]]</option>
178192
[[pass]]
@@ -211,6 +225,8 @@ No backend code changes are required.
211225
- verify `POST ../save_theme` succeeds
212226
- check `apps/_dashboard/user_settings.toml` is writable
213227

228+
If the user is not logged in, theme changes still apply in the browser and are stored in localStorage, but backend persistence is skipped.
229+
214230
### Theme script not running
215231
- verify `theme.js` exists and has valid JavaScript
216232
- check browser console for syntax/runtime errors
@@ -221,3 +237,4 @@ No backend code changes are required.
221237

222238
- The dashboard uses canonical page templates plus optional hook partials.
223239
- Theme-specific customization should stay primarily in `theme.css` and optional `theme.js`; use hook partials only for small structural differences.
240+
- Keep shared behavior helpers in `apps/_dashboard/static/js/theme-selector.js` and shared dashboard JS files, not duplicated in individual themes.

apps/_dashboard/__init__.py

Lines changed: 98 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@ def change_password_handler(old_password, new_password):
115115
def load_user_settings():
116116
"""Load user settings from user_settings.toml"""
117117
settings_file = os.path.join(settings.APP_FOLDER, "user_settings.toml")
118-
default_settings = {"selected_theme": "AlienDark"}
118+
default_settings = {"selected_theme": "Modern"}
119119

120120
if not os.path.exists(settings_file):
121121
# Create default settings file
122122
try:
123123
with open(settings_file, "w") as fp:
124-
fp.write('selected_theme = "AlienDark"\n')
124+
fp.write('selected_theme = "Modern"\n')
125125
return default_settings
126126
except Exception:
127127
return default_settings
@@ -179,11 +179,11 @@ def normalize_selected_theme(selected_theme, available_themes=None):
179179
themes = available_themes if available_themes is not None else get_available_themes()
180180
if selected_theme and selected_theme in themes:
181181
return selected_theme
182-
if "AlienDark" in themes:
183-
return "AlienDark"
182+
if "Modern" in themes:
183+
return "Modern"
184184
if themes:
185185
return themes[0]
186-
return "AlienDark"
186+
return "Modern"
187187

188188

189189
def _safe_name(value):
@@ -233,6 +233,26 @@ def build_theme_partials(selected_theme, partial_names):
233233
}
234234

235235

236+
def get_system_info_payload():
237+
"""Collect system and loaded module version information."""
238+
os_info = f"{platform.system()} {platform.release()}"
239+
items = [
240+
{"name": "os", "version": os_info},
241+
{"name": "py4web", "version": __version__},
242+
{"name": "python", "version": sys.version},
243+
]
244+
for module in sorted(sys.modules):
245+
if "." in module:
246+
continue
247+
try:
248+
imported = __import__(module)
249+
if "__version__" in dir(imported):
250+
items.append({"name": module, "version": imported.__version__})
251+
except ImportError:
252+
pass
253+
return items
254+
255+
236256
session = Session()
237257
T = Translator(settings.T_FOLDER)
238258
authenticated = ActionFactory(Logged(session))
@@ -252,7 +272,7 @@ def index():
252272
themes = get_available_themes()
253273
user_settings = load_user_settings()
254274
selected_theme = normalize_selected_theme(
255-
user_settings.get("selected_theme", "AlienDark"), themes
275+
user_settings.get("selected_theme", "Modern"), themes
256276
)
257277
theme_partials = build_theme_partials(selected_theme, ["index_header_actions"])
258278

@@ -315,26 +335,46 @@ def save_theme():
315335
user_settings["selected_theme"] = theme
316336
return save_user_settings(user_settings)
317337

338+
@action("settings")
339+
@action.uses(Logged(session), "settings.html")
340+
@catch_errors
341+
def settings_page():
342+
themes = get_available_themes()
343+
user_settings = load_user_settings()
344+
selected_theme = normalize_selected_theme(
345+
user_settings.get("selected_theme", "Modern"), themes
346+
)
347+
return dict(
348+
themes=themes,
349+
selected_theme=selected_theme,
350+
message="",
351+
message_type="",
352+
)
353+
318354
@action("tickets/search")
319355
@action.uses(Logged(session), "dbadmin.html")
320356
def dbadmin():
321357
db = error_logger.database_logger.db
322358
themes = get_available_themes()
323359
user_settings = load_user_settings()
324360
selected_theme = normalize_selected_theme(
325-
user_settings.get("selected_theme", "AlienDark"), themes
361+
user_settings.get("selected_theme", "Modern"), themes
326362
)
327363
theme_partials = build_theme_partials(
328364
selected_theme, ["dbadmin_nav", "dbadmin_footer_back"]
329365
)
366+
filter_app = request.query.get("app_name", "").strip()
330367

331368
def make_grid():
332369
make_safe(db)
333370
table = db.py4web_error
371+
query = (table.app_name == filter_app) if filter_app else table
334372
columns = [field for field in table if not field.name == "snapshot"]
335373
return Grid(
336-
table,
374+
query,
337375
columns=columns,
376+
search_queries=None,
377+
create=False,
338378
details=False,
339379
editable=False,
340380
pre_action_buttons=[
@@ -348,7 +388,7 @@ def make_grid():
348388

349389
grid = action.uses(db)(make_grid)()
350390
return dict(
351-
app_name="",
391+
app_name=filter_app,
352392
table_name="py4web_error",
353393
grid=grid,
354394
themes=themes,
@@ -362,7 +402,7 @@ def dbadmin(app_name, db_name, table_name):
362402
themes = get_available_themes()
363403
user_settings = load_user_settings()
364404
selected_theme = normalize_selected_theme(
365-
user_settings.get("selected_theme", "AlienDark"), themes
405+
user_settings.get("selected_theme", "Modern"), themes
366406
)
367407
theme_partials = build_theme_partials(
368408
selected_theme, ["dbadmin_nav", "dbadmin_footer_back"]
@@ -415,22 +455,21 @@ def make_grid():
415455
@session_secured
416456
@catch_errors
417457
def info():
418-
# Start with OS information
419-
os_info = f"{platform.system()} {platform.release()}"
420-
vars = [
421-
{"name": "os", "version": os_info},
422-
{"name": "py4web", "version": __version__},
423-
{"name": "python", "version": sys.version}
424-
]
425-
for module in sorted(sys.modules):
426-
if not "." in module:
427-
try:
428-
m = __import__(module)
429-
if "__version__" in dir(m):
430-
vars.append({"name": module, "version": m.__version__})
431-
except ImportError:
432-
pass
433-
return {"status": "success", "payload": vars}
458+
return {"status": "success", "payload": get_system_info_payload()}
459+
460+
@action("system_info")
461+
@action.uses(Logged(session), "system_info.html")
462+
def system_info_page():
463+
themes = get_available_themes()
464+
user_settings = load_user_settings()
465+
selected_theme = normalize_selected_theme(
466+
user_settings.get("selected_theme", "Modern"), themes
467+
)
468+
return dict(
469+
info_items=get_system_info_payload(),
470+
themes=themes,
471+
selected_theme=selected_theme,
472+
)
434473

435474
@action("routes")
436475
@session_secured
@@ -703,7 +742,21 @@ def packed(path):
703742
def tickets():
704743
"""Returns most recent tickets grouped by path+error"""
705744
tickets = safely(error_logger.database_logger.get) if MODE != "DEMO" else None
706-
return {"payload": tickets or [], "status": "success"}
745+
total_count = 0
746+
if MODE != "DEMO":
747+
total_count = safely(
748+
lambda: error_logger.database_logger.db(
749+
error_logger.database_logger.db.py4web_error
750+
).count()
751+
) or 0
752+
return {"payload": tickets or [], "total_count": total_count, "status": "success"}
753+
754+
@action("tickets/delete_all", method=["POST"])
755+
@action.uses(Logged(session))
756+
def delete_all_tickets():
757+
if MODE != "demo":
758+
safely(error_logger.database_logger.clear)
759+
redirect(URL("tickets/search"))
707760

708761
@action("clear")
709762
@session_secured
@@ -718,17 +771,21 @@ def error_ticket(ticket_uuid):
718771
themes = get_available_themes()
719772
user_settings = load_user_settings()
720773
selected_theme = normalize_selected_theme(
721-
user_settings.get("selected_theme", "AlienDark"), themes
774+
user_settings.get("selected_theme", "Modern"), themes
775+
)
776+
theme_partials = build_theme_partials(
777+
selected_theme, ["dbadmin_nav", "dbadmin_footer_back"]
722778
)
723779
if MODE != "demo":
724780
return dict(
725781
ticket=safely(
726782
lambda: error_logger.database_logger.get(ticket_uuid=ticket_uuid)
727783
),
728-
selected_theme=selected_theme
784+
selected_theme=selected_theme,
785+
theme_partials=theme_partials,
729786
)
730787
else:
731-
return dict(ticket=None, selected_theme=selected_theme)
788+
return dict(ticket=None, selected_theme=selected_theme, theme_partials=theme_partials)
732789

733790
@action("rest/<path:path>", method=["GET", "POST", "PUT", "DELETE"])
734791
@session_secured
@@ -921,8 +978,16 @@ def new_app():
921978
def gitlog(project):
922979
themes = get_available_themes()
923980
user_settings = load_user_settings()
981+
selected_theme = normalize_selected_theme(
982+
user_settings.get("selected_theme", "Modern"), themes
983+
)
924984
if not is_git_repo(os.path.join(FOLDER, project)):
925-
return "Project is not a GIT repo"
985+
return dict(
986+
status="error",
987+
error="Project is not a GIT repo",
988+
project=project,
989+
selected_theme=selected_theme,
990+
)
926991
branches = get_branches(cwd=os.path.join(FOLDER, project))
927992
commits = get_commits(cwd=os.path.join(FOLDER, project))
928993
return dict(
@@ -931,9 +996,7 @@ def gitlog(project):
931996
checkout=checkout,
932997
project=project,
933998
branches=branches,
934-
selected_theme=normalize_selected_theme(
935-
user_settings.get("selected_theme", "AlienDark"), themes
936-
),
999+
selected_theme=selected_theme,
9371000
)
9381001

9391002
@authenticated.callback()
@@ -987,7 +1050,7 @@ def translations(name):
9871050
return dict(
9881051
languages=t.languages,
9891052
selected_theme=normalize_selected_theme(
990-
user_settings.get("selected_theme", "AlienDark"), themes
1053+
user_settings.get("selected_theme", "Modern"), themes
9911054
),
9921055
)
9931056

0 commit comments

Comments
 (0)