diff --git a/admin_server.py b/admin_server.py index b00ce83..e2afba3 100644 --- a/admin_server.py +++ b/admin_server.py @@ -310,8 +310,9 @@ def _seed_if_empty(conn: sqlite3.Connection, table: str, columns: tuple, rows: l def _seed_data(conn: sqlite3.Connection): """Insert default seed data for lookup tables.""" _seed_if_empty(conn, "categories", ("name", "icon"), [ - ("Furniture", "πŸͺ‘"), ("Appliances", "πŸ”Œ"), - ("Utensils & Serveware", "🍽️"), ("Equipment", "βš™οΈ"), ("Other", "πŸ“¦"), + ("Bev", "πŸ₯€"), ("Snack", "🍿"), + ("Food", "πŸ”"), ("Equipment", "πŸ”§"), + ("Appliances", "πŸ”Œ"), ("Other", "πŸ“¦"), ]) _seed_if_empty(conn, "key_names", ("name",), [ ("MK500",), ("Green Dot",), ("Red Key",), ("Blue Key",), @@ -1526,6 +1527,19 @@ def get_stats(): ).fetchall() by_make = {r["make"]: r["cnt"] for r in makes} + # Find asset categories not in the categories lookup table + unmapped = conn.execute( + """SELECT a.category AS name, COUNT(*) AS cnt + FROM assets a LEFT JOIN categories c ON a.category = c.name + WHERE c.id IS NULL AND a.category IS NOT NULL AND a.category != '' + GROUP BY a.category ORDER BY cnt DESC""" + ).fetchall() + category_health = { + "unmapped": [{"name": r["name"], "count": r["cnt"]} for r in unmapped], + "total_categories": len(by_category), + "unmapped_count": len(unmapped), + } + conn.close() return { "total_assets": total_assets, @@ -1538,6 +1552,7 @@ def get_stats(): "top_visited": top_visited_list, "time_on_site": time_on_site, "by_make": by_make, + "category_health": category_health, } diff --git a/static/index.html b/static/index.html index 9796866..5cda65c 100644 --- a/static/index.html +++ b/static/index.html @@ -136,6 +136,7 @@ flex: 1; padding: 24px; max-width: calc(100vw - var(--sidebar-w)); + position: relative; } .page-title { font-size: 22px; @@ -533,6 +534,42 @@ .empty-state { text-align: center; padding: 28px 16px; color: var(--text2); font-size: 13px; } .empty-state .es-icon { font-size: 36px; margin-bottom: 8px; opacity: 0.5; } .loading-row { text-align: center; padding: 20px; color: var(--text2); } + + /* ── Skeleton cards (dashboard loading) ── */ + .skeleton-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px; + margin-bottom: 14px; + min-height: 80px; + } + .skeleton-card.skeleton-stat { padding: 18px; text-align: center; min-height: 100px; } + .skeleton-card .skeleton-bar { + height: 14px; background: var(--card2); border-radius: 8px; margin-bottom: 8px; + animation: skeleton-pulse 1.4s ease-in-out infinite; + } + .skeleton-card .skeleton-bar:last-child { margin-bottom: 0; } + .skeleton-card .skeleton-bar.w60 { width: 60%; } + .skeleton-card .skeleton-bar.w40 { width: 40%; } + .skeleton-card .skeleton-bar.w80 { width: 80%; } + .skeleton-card .skeleton-circle { + width: 48px; height: 48px; border-radius: 50%; background: var(--card2); + margin: 0 auto 10px; animation: skeleton-pulse 1.4s ease-in-out infinite; + } + @keyframes skeleton-pulse { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.7; } + } + @keyframes pulse-border { + 0%, 100% { border-color: var(--accent); } + 50% { border-color: var(--accent2); } + } + td.saving { + opacity: 0.6; + box-shadow: inset 0 0 0 1px var(--accent); + animation: pulse-border 1s infinite; + } .flex-row { display: flex; gap: 8px; align-items: center; } .gap-sm { gap: 6px; } .mt-8 { margin-top: 8px; } @@ -716,6 +753,21 @@ .cs-repl-status.approved { background: rgba(72,199,142,0.15); color: var(--green); } .cs-repl-status.rejected { background: rgba(240,61,62,0.15); color: var(--red); } .cs-repl-resolve-all { margin-top: 8px; } + + /* ── Route Planner iframe ── */ + #pageRoute { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + margin-top: 0; padding: 0; + display: flex; flex-direction: column; + } + #pageRoute.hidden { display: none !important; } + #routeFrame { + width: 100%; + flex: 1; + border: none; + background: #fff; + } @@ -731,7 +783,6 @@ -
Default: admin / changeme
@@ -752,33 +803,39 @@ +
By Make
@@ -996,7 +1057,7 @@
Assets
- +