Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
832 changes: 832 additions & 0 deletions docs/superpowers/plans/2026-04-15-dashboard-improvements.md

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions docs/superpowers/specs/2026-04-15-dashboard-improvements-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Dashboard Improvements: Real Stats + System Info

**Date:** 2026-04-15
**Status:** Draft
**Module:** `modules/dashboard`

## Goal

Replace the current placeholder dashboard with a data-rich overview showing real user/product counts and system information. Activity logging and audit trails are out of scope — a separate audit log module will handle that.

## Current State

The dashboard (`modules/dashboard/dashboard/pages/Home.tsx`) renders:

- 3 stat cards with hardcoded/placeholder values (Products: "-", Users: "-", Modules: "3")
- A static welcome card
- In-memory product event counters (not persisted, lost on restart)

The backend view endpoint passes only a `welcome` string to the frontend via Inertia.

## Design

### Section 1: Stat Cards (replace placeholders with real data)

Four stat cards in a responsive grid:

| Card | Source | Query |
|------|--------|-------|
| **Total Users** | `users_user` table | `SELECT COUNT(*) FROM users_user` |
| **Active Users** (7d) | `users_user.last_login_at` | `WHERE last_login_at >= NOW() - INTERVAL '7 days'` |
| **Products** | `products_product` table | `SELECT COUNT(*) FROM products_product` |
| **Modules** | `discover_modules()` | `len(discover_modules())` |

The stat card grid changes from 3 to 4 columns: `grid-cols-1 sm:grid-cols-2 md:grid-cols-4`.

Active Users gets a new accent color (amber) to differentiate from Total Users (emerald stays for total).

### Section 2: System Info Panel

A card displayed below the stat cards showing:

- **Loaded modules** — name and status badge (green dot) for each discovered module
- **Python version** — from `sys.version`
- **Health checks** — results from `app.state.health_registry` with green/yellow/red status indicators

### Section 3: Dashboard Stats API (updated)

Expand `GET /api/dashboard/stats` to return all data the frontend needs:

```python
@router.get("/stats")
async def dashboard_stats(request: Request, db: AsyncSession = Depends(get_db)) -> dict:
return {
"total_users": <count from users_user>,
"active_users_7d": <count where last_login_at >= 7 days ago>,
"total_products": <count from products_product>,
"module_count": <len(discover_modules())>,
"system_info": {
"modules": [
{"name": "Auth", "status": "loaded"},
{"name": "Users", "status": "loaded"},
{"name": "Products", "status": "loaded"},
{"name": "Dashboard", "status": "loaded"},
],
"python_version": "3.12.x",
"health_checks": [
{"name": "database", "status": "healthy"},
],
},
}
```

Cross-module DB queries: The stats endpoint queries `users_user` and `products_product` tables directly (same database). This is a read-only cross-module query, acceptable for an aggregation endpoint.

### Section 4: Dashboard View Endpoint (updated)

The Inertia view endpoint (`GET /dashboard`) passes all stats as page props:

```python
@router.get("/")
async def dashboard(inertia: InertiaDep, t: TranslatorDep, db: ...) -> InertiaResponse:
stats = await fetch_dashboard_stats(db, request)
return await inertia.render("Dashboard/Home", {
"welcome": t.t("dashboard.home.welcome_message"),
**stats,
})
```

### Section 5: Frontend — Home.tsx (rewritten)

The page receives all stats as Inertia page props and renders:

1. **Stat Cards Row** — 4-column responsive grid with real values
2. **System Info Panel** — card listing modules, Python version, health checks

**System Info Panel:**
- Card listing loaded modules with status badges (green dot for loaded)
- Python version display
- Health check results with status indicator (green/yellow/red dot)
- Wrapped in a `Card` with title "System"

### Section 6: i18n Updates

Add new translation keys to `modules/dashboard/dashboard/locales/en.json` (and `es.json`):

- `stats.active_users`: "Active Users (7d)"
- `system_info_title`: "System"
- `system_info.modules`: "Modules"
- `system_info.python_version`: "Python Version"
- `system_info.health_checks`: "Health Checks"

### Section 7: Cleanup

Remove the in-memory product event counters from `handlers.py` since they serve no purpose without the activity log. The dashboard module still depends on Products (for the product count query) but no longer subscribes to product events.

## Files to Modify

| File | Changes |
|------|---------|
| `modules/dashboard/dashboard/handlers.py` | Remove in-memory counters (file may become empty/deleted) |
| `modules/dashboard/dashboard/module.py` | Remove event handler subscriptions, add `depends_on=["Users"]` |
| `modules/dashboard/dashboard/endpoints/api.py` | Expand `/stats` with real counts + system info |
| `modules/dashboard/dashboard/endpoints/views.py` | Pass full stats as Inertia props |
| `modules/dashboard/dashboard/pages/Home.tsx` | Rewrite with real stat cards + system info panel |
| `modules/dashboard/dashboard/locales/en.json` | Add new translation keys |
| `modules/dashboard/dashboard/locales/es.json` | Add new translation keys |
| `modules/dashboard/pyproject.toml` | Add dependency on `users` |
| `modules/dashboard/tests/test_dashboard.py` | Rewrite tests for new stats endpoint |

## Out of Scope

- Activity/audit logging (separate module)
- Event handler persistence
- Recent activity feed
- Activity charts
- Real-time updates
- Dashboard customization
1 change: 1 addition & 0 deletions framework/hosting/simple_module_hosting/app_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
lifespan=lifespan,
)

app.state.modules = modules
app.state.menu_registry = menu_registry
app.state.perm_registry = perm_registry
app.state.ff_registry = ff_registry
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""add indexes for dashboard queries

Revision ID: a01185374312
Revises: e3ce9754e6dc
Create Date: 2026-04-15 23:53:08.569085
"""

from collections.abc import Sequence

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "a01185374312"
down_revision: str | None = "e3ce9754e6dc"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(
op.f("ix_products_product_is_active"), "products_product", ["is_active"], unique=False
)
op.create_index(
op.f("ix_users_user_last_login_at"), "users_user", ["last_login_at"], unique=False
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_users_user_last_login_at"), table_name="users_user")
op.drop_index(op.f("ix_products_product_is_active"), table_name="products_product")
# ### end Alembic commands ###
14 changes: 7 additions & 7 deletions modules/dashboard/dashboard/endpoints/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

from __future__ import annotations

from fastapi import APIRouter
from fastapi import APIRouter, Depends, Request
from simple_module_db.deps import get_db
from sqlalchemy.ext.asyncio import AsyncSession

from dashboard.handlers import get_product_event_counts
from dashboard.stats import fetch_dashboard_stats

router = APIRouter()


@router.get("/stats")
async def dashboard_stats() -> dict:
"""Return dashboard statistics including product event counts."""
return {
"product_events": get_product_event_counts(),
}
async def dashboard_stats(request: Request, db: AsyncSession = Depends(get_db)) -> dict:
"""Return dashboard statistics including user counts and system info."""
return await fetch_dashboard_stats(db, request.app)
15 changes: 13 additions & 2 deletions modules/dashboard/dashboard/endpoints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,31 @@

from __future__ import annotations

from fastapi import APIRouter
from fastapi import APIRouter, Depends, Request
from inertia import InertiaResponse
from simple_module_db.deps import get_db
from simple_module_hosting.i18n_deps import TranslatorDep
from simple_module_hosting.inertia_deps import InertiaDep
from sqlalchemy.ext.asyncio import AsyncSession

from dashboard.stats import fetch_dashboard_stats

router = APIRouter()


@router.get("/", response_model=None)
async def dashboard(inertia: InertiaDep, t: TranslatorDep) -> InertiaResponse:
async def dashboard(
request: Request,
inertia: InertiaDep,
t: TranslatorDep,
db: AsyncSession = Depends(get_db),
) -> InertiaResponse:
"""Authenticated dashboard — requires login (enforced by AuthMiddleware)."""
stats = await fetch_dashboard_stats(db, request.app)
return await inertia.render(
"Dashboard/Home",
{
"welcome": t.t("dashboard.home.welcome_message"),
**stats,
},
)
46 changes: 0 additions & 46 deletions modules/dashboard/dashboard/handlers.py

This file was deleted.

9 changes: 8 additions & 1 deletion modules/dashboard/dashboard/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
"title": "Dashboard",
"description": "Overview of your application",
"stats": {
"total_users": "Total Users",
"active_users": "Active Users (7d)",
"products": "Products",
"users": "Users",
"modules": "Modules"
},
"system_info_title": "System",
"system_info": {
"modules": "Modules",
"python_version": "Python Version",
"health_checks": "Health Checks"
},
"welcome_card_title": "Welcome",
"welcome_message": "Welcome to SimpleModule",
"description_body": "This is a modular monolith built with FastAPI, Inertia.js, and React. Each module provides its own pages, API endpoints, and database schema."
Expand Down
9 changes: 8 additions & 1 deletion modules/dashboard/dashboard/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
"title": "Panel",
"description": "Resumen de tu aplicación",
"stats": {
"total_users": "Usuarios Totales",
"active_users": "Usuarios Activos (7d)",
"products": "Productos",
"users": "Usuarios",
"modules": "Módulos"
},
"system_info_title": "Sistema",
"system_info": {
"modules": "Módulos",
"python_version": "Versión de Python",
"health_checks": "Verificaciones de Salud"
},
"welcome_card_title": "Bienvenido",
"welcome_message": "Bienvenido a SimpleModule",
"description_body": "Este es un monolito modular construido con FastAPI, Inertia.js y React. Cada módulo proporciona sus propias páginas, endpoints de API y esquema de base de datos."
Expand Down
11 changes: 1 addition & 10 deletions modules/dashboard/dashboard/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,16 @@
from pathlib import Path

from fastapi import APIRouter
from products.contracts.events import ProductCreated, ProductDeleted, ProductUpdated
from simple_module_core.events import EventBus
from simple_module_core.menu import MenuItem, MenuRegistry, MenuSection
from simple_module_core.module import ModuleBase, ModuleMeta

from dashboard.handlers import on_product_created, on_product_deleted, on_product_updated


class DashboardModule(ModuleBase):
meta = ModuleMeta(
name="Dashboard",
route_prefix="/api/dashboard",
view_prefix="/dashboard",
depends_on=["Products"],
depends_on=["Products", "Users"],
)

def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None:
Expand All @@ -40,10 +36,5 @@ def register_menu_items(self, registry: MenuRegistry) -> None:
)
)

def register_event_handlers(self, bus: EventBus) -> None:
bus.subscribe(ProductCreated, on_product_created)
bus.subscribe(ProductUpdated, on_product_updated)
bus.subscribe(ProductDeleted, on_product_deleted)

def locale_dirs(self) -> dict[str, Path]:
return {"dashboard": Path(str(importlib.resources.files(__package__) / "locales"))}
Loading