simple-module-dashboard 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dashboard/__init__.py +1 -0
- dashboard/endpoints/__init__.py +0 -0
- dashboard/endpoints/api.py +17 -0
- dashboard/endpoints/views.py +36 -0
- dashboard/locales/en.json +21 -0
- dashboard/locales/es.json +21 -0
- dashboard/module.py +45 -0
- dashboard/package.json +16 -0
- dashboard/pages/Home.tsx +186 -0
- dashboard/py.typed +0 -0
- dashboard/stats.py +114 -0
- simple_module_dashboard-0.0.1.dist-info/METADATA +75 -0
- simple_module_dashboard-0.0.1.dist-info/RECORD +16 -0
- simple_module_dashboard-0.0.1.dist-info/WHEEL +4 -0
- simple_module_dashboard-0.0.1.dist-info/entry_points.txt +2 -0
- simple_module_dashboard-0.0.1.dist-info/licenses/LICENSE +21 -0
dashboard/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dashboard module."""
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""REST API endpoints for the Dashboard module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, Request
|
|
6
|
+
from simple_module_db.deps import get_db
|
|
7
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
|
+
|
|
9
|
+
from dashboard.stats import fetch_dashboard_stats
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("/stats")
|
|
15
|
+
async def dashboard_stats(request: Request, db: AsyncSession = Depends(get_db)) -> dict:
|
|
16
|
+
"""Return dashboard statistics including user counts and system info."""
|
|
17
|
+
return await fetch_dashboard_stats(db, request.app)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Inertia view endpoints for the Dashboard.
|
|
2
|
+
|
|
3
|
+
Mounted under ``/dashboard`` via :attr:`DashboardModule.meta.view_prefix`.
|
|
4
|
+
The public landing page at ``/`` is owned by the host, not this module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Depends, Request
|
|
10
|
+
from inertia import InertiaResponse
|
|
11
|
+
from simple_module_db.deps import get_db
|
|
12
|
+
from simple_module_hosting.i18n_deps import TranslatorDep
|
|
13
|
+
from simple_module_hosting.inertia_deps import InertiaDep
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
|
|
16
|
+
from dashboard.stats import fetch_dashboard_stats
|
|
17
|
+
|
|
18
|
+
router = APIRouter()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/", response_model=None)
|
|
22
|
+
async def dashboard(
|
|
23
|
+
request: Request,
|
|
24
|
+
inertia: InertiaDep,
|
|
25
|
+
t: TranslatorDep,
|
|
26
|
+
db: AsyncSession = Depends(get_db),
|
|
27
|
+
) -> InertiaResponse:
|
|
28
|
+
"""Authenticated dashboard — requires login (enforced by AuthMiddleware)."""
|
|
29
|
+
stats = await fetch_dashboard_stats(db, request.app)
|
|
30
|
+
return await inertia.render(
|
|
31
|
+
"Dashboard/Home",
|
|
32
|
+
{
|
|
33
|
+
"welcome": t.t("dashboard.home.welcome_message"),
|
|
34
|
+
**stats,
|
|
35
|
+
},
|
|
36
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"home": {
|
|
3
|
+
"title": "Dashboard",
|
|
4
|
+
"description": "Overview of your application",
|
|
5
|
+
"stats": {
|
|
6
|
+
"total_users": "Total Users",
|
|
7
|
+
"active_users": "Active Users (7d)",
|
|
8
|
+
"products": "Products",
|
|
9
|
+
"modules": "Modules"
|
|
10
|
+
},
|
|
11
|
+
"system_info_title": "System",
|
|
12
|
+
"system_info": {
|
|
13
|
+
"modules": "Modules",
|
|
14
|
+
"python_version": "Python Version",
|
|
15
|
+
"health_checks": "Health Checks"
|
|
16
|
+
},
|
|
17
|
+
"welcome_card_title": "Welcome",
|
|
18
|
+
"welcome_message": "Welcome to SimpleModule",
|
|
19
|
+
"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."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"home": {
|
|
3
|
+
"title": "Panel",
|
|
4
|
+
"description": "Resumen de tu aplicación",
|
|
5
|
+
"stats": {
|
|
6
|
+
"total_users": "Usuarios Totales",
|
|
7
|
+
"active_users": "Usuarios Activos (7d)",
|
|
8
|
+
"products": "Productos",
|
|
9
|
+
"modules": "Módulos"
|
|
10
|
+
},
|
|
11
|
+
"system_info_title": "Sistema",
|
|
12
|
+
"system_info": {
|
|
13
|
+
"modules": "Módulos",
|
|
14
|
+
"python_version": "Versión de Python",
|
|
15
|
+
"health_checks": "Verificaciones de Salud"
|
|
16
|
+
},
|
|
17
|
+
"welcome_card_title": "Bienvenido",
|
|
18
|
+
"welcome_message": "Bienvenido a SimpleModule",
|
|
19
|
+
"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."
|
|
20
|
+
}
|
|
21
|
+
}
|
dashboard/module.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Dashboard module definition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.resources
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastapi import APIRouter
|
|
9
|
+
from simple_module_core.menu import MenuItem, MenuRegistry, MenuSection
|
|
10
|
+
from simple_module_core.module import ModuleBase, ModuleMeta
|
|
11
|
+
|
|
12
|
+
_MODULE_PRODUCTS = "Products"
|
|
13
|
+
_MODULE_USERS = "Users"
|
|
14
|
+
_URL_DASHBOARD = "/dashboard/"
|
|
15
|
+
_ICON_DASHBOARD = "home"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DashboardModule(ModuleBase):
|
|
19
|
+
meta = ModuleMeta(
|
|
20
|
+
name="Dashboard",
|
|
21
|
+
route_prefix="/api/dashboard",
|
|
22
|
+
view_prefix="/dashboard",
|
|
23
|
+
depends_on=[_MODULE_PRODUCTS, _MODULE_USERS],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def register_routes(self, api_router: APIRouter, view_router: APIRouter) -> None:
|
|
27
|
+
from dashboard.endpoints.api import router as api
|
|
28
|
+
from dashboard.endpoints.views import router as views
|
|
29
|
+
|
|
30
|
+
api_router.include_router(api)
|
|
31
|
+
view_router.include_router(views)
|
|
32
|
+
|
|
33
|
+
def register_menu_items(self, registry: MenuRegistry) -> None:
|
|
34
|
+
registry.add(
|
|
35
|
+
MenuItem(
|
|
36
|
+
label="Dashboard",
|
|
37
|
+
url=_URL_DASHBOARD,
|
|
38
|
+
icon=_ICON_DASHBOARD,
|
|
39
|
+
order=1,
|
|
40
|
+
section=MenuSection.SIDEBAR,
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def locale_dirs(self) -> dict[str, Path]:
|
|
45
|
+
return {"dashboard": Path(str(importlib.resources.files(__package__) / "locales"))}
|
dashboard/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simple-module-py/dashboard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Frontend assets for the Dashboard module",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"react": "^19.0.0",
|
|
8
|
+
"react-dom": "^19.0.0",
|
|
9
|
+
"@inertiajs/react": "^2.0.0",
|
|
10
|
+
"@simple-module-py/ui": "*"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@simple-module-py/tsconfig": "*"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {}
|
|
16
|
+
}
|
dashboard/pages/Home.tsx
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { usePage } from '@inertiajs/react';
|
|
2
|
+
import { keys, useT } from '@simple-module-py/i18n';
|
|
3
|
+
import { PageShell } from '@simple-module-py/ui/components/PageShell';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@simple-module-py/ui/components/ui/card';
|
|
5
|
+
import { Table, TableBody, TableCell, TableRow } from '@simple-module-py/ui/components/ui/table';
|
|
6
|
+
import { AuthenticatedLayout } from '@simple-module-py/ui/layouts/AuthenticatedLayout';
|
|
7
|
+
import { Activity, Box, Heart, Package, Server, Users } from 'lucide-react';
|
|
8
|
+
|
|
9
|
+
type Accent = 'primary' | 'emerald' | 'violet' | 'amber';
|
|
10
|
+
|
|
11
|
+
const HEALTH_STATUS_COLOR: Record<string, string> = {
|
|
12
|
+
healthy: 'bg-emerald-500',
|
|
13
|
+
degraded: 'bg-amber-500',
|
|
14
|
+
unhealthy: 'bg-red-500',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
interface SystemModule {
|
|
18
|
+
name: string;
|
|
19
|
+
status: 'loaded';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface HealthCheck {
|
|
23
|
+
name: string;
|
|
24
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SystemInfo {
|
|
28
|
+
modules: SystemModule[];
|
|
29
|
+
python_version: string;
|
|
30
|
+
health_checks: HealthCheck[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
total_users: number;
|
|
35
|
+
active_users_7d: number;
|
|
36
|
+
total_products: number;
|
|
37
|
+
module_count: number;
|
|
38
|
+
system_info: SystemInfo;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Home() {
|
|
42
|
+
const props = usePage<{ props: Props }>().props as unknown as Props;
|
|
43
|
+
const { t } = useT();
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<PageShell
|
|
47
|
+
title={t(keys.dashboard.home.title)}
|
|
48
|
+
description={t(keys.dashboard.home.description)}
|
|
49
|
+
>
|
|
50
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-5 mb-8">
|
|
51
|
+
<StatCard
|
|
52
|
+
title={t(keys.dashboard.home.stats.total_users)}
|
|
53
|
+
value={String(props.total_users)}
|
|
54
|
+
icon={<Users className="size-4" />}
|
|
55
|
+
accent="emerald"
|
|
56
|
+
/>
|
|
57
|
+
<StatCard
|
|
58
|
+
title={t(keys.dashboard.home.stats.active_users)}
|
|
59
|
+
value={String(props.active_users_7d)}
|
|
60
|
+
icon={<Activity className="size-4" />}
|
|
61
|
+
accent="amber"
|
|
62
|
+
/>
|
|
63
|
+
<StatCard
|
|
64
|
+
title={t(keys.dashboard.home.stats.products)}
|
|
65
|
+
value={String(props.total_products)}
|
|
66
|
+
icon={<Package className="size-4" />}
|
|
67
|
+
accent="primary"
|
|
68
|
+
/>
|
|
69
|
+
<StatCard
|
|
70
|
+
title={t(keys.dashboard.home.stats.modules)}
|
|
71
|
+
value={String(props.module_count)}
|
|
72
|
+
icon={<Box className="size-4" />}
|
|
73
|
+
accent="violet"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<Card>
|
|
78
|
+
<CardHeader>
|
|
79
|
+
<CardTitle className="flex items-center gap-2 font-[var(--font-display)]">
|
|
80
|
+
<Server className="size-4" />
|
|
81
|
+
{t(keys.dashboard.home.system_info_title)}
|
|
82
|
+
</CardTitle>
|
|
83
|
+
</CardHeader>
|
|
84
|
+
<CardContent className="space-y-6">
|
|
85
|
+
<div>
|
|
86
|
+
<h4 className="text-sm font-medium text-muted-foreground mb-2">
|
|
87
|
+
{t(keys.dashboard.home.system_info.modules)}
|
|
88
|
+
</h4>
|
|
89
|
+
<div className="flex flex-wrap gap-2">
|
|
90
|
+
{props.system_info.modules.map((mod) => (
|
|
91
|
+
<span
|
|
92
|
+
key={mod.name}
|
|
93
|
+
className="inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium"
|
|
94
|
+
>
|
|
95
|
+
<span className="size-1.5 rounded-full bg-emerald-500" />
|
|
96
|
+
{mod.name}
|
|
97
|
+
</span>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<Table>
|
|
103
|
+
<TableBody>
|
|
104
|
+
<TableRow>
|
|
105
|
+
<TableCell className="font-medium text-muted-foreground">
|
|
106
|
+
{t(keys.dashboard.home.system_info.python_version)}
|
|
107
|
+
</TableCell>
|
|
108
|
+
<TableCell>{props.system_info.python_version}</TableCell>
|
|
109
|
+
</TableRow>
|
|
110
|
+
{props.system_info.health_checks.map((check) => (
|
|
111
|
+
<TableRow key={check.name}>
|
|
112
|
+
<TableCell className="font-medium text-muted-foreground flex items-center gap-2">
|
|
113
|
+
<Heart className="size-3" />
|
|
114
|
+
{check.name}
|
|
115
|
+
</TableCell>
|
|
116
|
+
<TableCell>
|
|
117
|
+
<span className="inline-flex items-center gap-1.5">
|
|
118
|
+
<span
|
|
119
|
+
className={`size-2 rounded-full ${HEALTH_STATUS_COLOR[check.status] ?? 'bg-red-500'}`}
|
|
120
|
+
/>
|
|
121
|
+
{check.status}
|
|
122
|
+
</span>
|
|
123
|
+
</TableCell>
|
|
124
|
+
</TableRow>
|
|
125
|
+
))}
|
|
126
|
+
</TableBody>
|
|
127
|
+
</Table>
|
|
128
|
+
</CardContent>
|
|
129
|
+
</Card>
|
|
130
|
+
</PageShell>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function StatCard({
|
|
135
|
+
title,
|
|
136
|
+
value,
|
|
137
|
+
icon,
|
|
138
|
+
accent,
|
|
139
|
+
}: {
|
|
140
|
+
title: string;
|
|
141
|
+
value: string;
|
|
142
|
+
icon: React.ReactNode;
|
|
143
|
+
accent: Accent;
|
|
144
|
+
}) {
|
|
145
|
+
const styles: Record<Accent, { card: string; icon: string; value: string }> = {
|
|
146
|
+
primary: {
|
|
147
|
+
card: 'border-primary-200 bg-gradient-to-br from-primary-50 to-card',
|
|
148
|
+
icon: 'text-primary-500 bg-primary-100',
|
|
149
|
+
value: 'text-primary-900',
|
|
150
|
+
},
|
|
151
|
+
emerald: {
|
|
152
|
+
card: 'border-emerald-border bg-gradient-to-br from-emerald-bg to-card',
|
|
153
|
+
icon: 'text-emerald-icon-fg bg-emerald-icon-bg',
|
|
154
|
+
value: 'text-emerald-value',
|
|
155
|
+
},
|
|
156
|
+
violet: {
|
|
157
|
+
card: 'border-violet-border bg-gradient-to-br from-violet-bg to-card',
|
|
158
|
+
icon: 'text-violet-icon-fg bg-violet-icon-bg',
|
|
159
|
+
value: 'text-violet-value',
|
|
160
|
+
},
|
|
161
|
+
amber: {
|
|
162
|
+
card: 'border-amber-200 bg-gradient-to-br from-amber-50 to-card',
|
|
163
|
+
icon: 'text-amber-600 bg-amber-100',
|
|
164
|
+
value: 'text-amber-900',
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const s = styles[accent];
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Card className={s.card}>
|
|
172
|
+
<CardContent className="pt-5">
|
|
173
|
+
<div className="flex items-center justify-between mb-3">
|
|
174
|
+
<span className="text-sm font-medium text-muted-foreground">{title}</span>
|
|
175
|
+
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${s.icon}`}>
|
|
176
|
+
{icon}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<p className={`text-3xl font-bold font-[var(--font-display)] ${s.value}`}>{value}</p>
|
|
180
|
+
</CardContent>
|
|
181
|
+
</Card>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
Home.layout = (page: React.ReactNode) => <AuthenticatedLayout>{page}</AuthenticatedLayout>;
|
|
186
|
+
export default Home;
|
dashboard/py.typed
ADDED
|
File without changes
|
dashboard/stats.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Dashboard statistics queries with TTL-based caching."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from datetime import UTC, datetime, timedelta
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from products.models import Product
|
|
12
|
+
from simple_module_core.health import HealthCheck, HealthStatus
|
|
13
|
+
from sqlalchemy import func, select
|
|
14
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
15
|
+
from users.models import User
|
|
16
|
+
|
|
17
|
+
_CACHE_TTL_SECONDS = 30
|
|
18
|
+
_cache: dict | None = None
|
|
19
|
+
_cache_ts: float = 0.0
|
|
20
|
+
_cache_lock = asyncio.Lock()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _cache_hit() -> dict | None:
|
|
24
|
+
if _cache is not None and (time.monotonic() - _cache_ts) < _CACHE_TTL_SECONDS:
|
|
25
|
+
return _cache.copy()
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def fetch_dashboard_stats(db: AsyncSession, app: FastAPI) -> dict:
|
|
30
|
+
"""Gather all dashboard statistics, cached for 30 seconds."""
|
|
31
|
+
global _cache, _cache_ts
|
|
32
|
+
|
|
33
|
+
hit = _cache_hit()
|
|
34
|
+
if hit is not None:
|
|
35
|
+
return hit
|
|
36
|
+
|
|
37
|
+
async with _cache_lock:
|
|
38
|
+
# Re-check after acquiring lock — another coroutine may have refreshed.
|
|
39
|
+
hit = _cache_hit()
|
|
40
|
+
if hit is not None:
|
|
41
|
+
return hit
|
|
42
|
+
|
|
43
|
+
total_users = await _count_users(db)
|
|
44
|
+
active_users_7d = await _count_active_users(db, days=7)
|
|
45
|
+
total_products = await _count_products(db)
|
|
46
|
+
modules_list = _get_module_info(app)
|
|
47
|
+
health_checks = await _run_health_checks(app)
|
|
48
|
+
|
|
49
|
+
result = {
|
|
50
|
+
"total_users": total_users,
|
|
51
|
+
"active_users_7d": active_users_7d,
|
|
52
|
+
"total_products": total_products,
|
|
53
|
+
"module_count": len(modules_list),
|
|
54
|
+
"system_info": {
|
|
55
|
+
"modules": modules_list,
|
|
56
|
+
"python_version": (
|
|
57
|
+
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
58
|
+
),
|
|
59
|
+
"health_checks": health_checks,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_cache = result
|
|
64
|
+
_cache_ts = time.monotonic()
|
|
65
|
+
return result.copy()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def invalidate_stats_cache() -> None:
|
|
69
|
+
"""Clear the stats cache — useful for testing or after data mutations."""
|
|
70
|
+
global _cache, _cache_ts
|
|
71
|
+
_cache = None
|
|
72
|
+
_cache_ts = 0.0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _count_users(db: AsyncSession) -> int:
|
|
76
|
+
result = await db.execute(select(func.count()).select_from(User))
|
|
77
|
+
return result.scalar_one()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def _count_active_users(db: AsyncSession, *, days: int) -> int:
|
|
81
|
+
cutoff = datetime.now(UTC) - timedelta(days=days)
|
|
82
|
+
result = await db.execute(
|
|
83
|
+
select(func.count()).select_from(User).where(User.last_login_at >= cutoff)
|
|
84
|
+
)
|
|
85
|
+
return result.scalar_one()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def _count_products(db: AsyncSession) -> int:
|
|
89
|
+
result = await db.execute(
|
|
90
|
+
select(func.count()).select_from(Product).where(Product.is_active.is_(True))
|
|
91
|
+
)
|
|
92
|
+
return result.scalar_one()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_module_info(app: FastAPI) -> list[dict[str, str]]:
|
|
96
|
+
# Reads from the module list discovered once at startup, avoiding
|
|
97
|
+
# expensive entry-point rescans on every request.
|
|
98
|
+
return [{"name": m.meta.name, "status": "loaded"} for m in app.state.sm.modules]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def _run_health_checks(app: FastAPI) -> list[dict[str, str]]:
|
|
102
|
+
registry = app.state.sm.health_registry
|
|
103
|
+
checks = registry.all_checks
|
|
104
|
+
if not checks:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
async def _run_one(check: HealthCheck) -> dict[str, str]:
|
|
108
|
+
try:
|
|
109
|
+
result = await check.check()
|
|
110
|
+
return {"name": check.name, "status": result.status.value}
|
|
111
|
+
except Exception:
|
|
112
|
+
return {"name": check.name, "status": HealthStatus.UNHEALTHY.value}
|
|
113
|
+
|
|
114
|
+
return list(await asyncio.gather(*[_run_one(c) for c in checks]))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple_module_dashboard
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Admin landing page and sidebar menu host for authenticated users of a simple_module app
|
|
5
|
+
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
|
+
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
7
|
+
Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Anto Subash <antosubash@live.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: admin,dashboard,inertia,simple-module
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: simple-module-core==0.0.1
|
|
25
|
+
Requires-Dist: simple-module-db==0.0.1
|
|
26
|
+
Requires-Dist: simple-module-hosting==0.0.1
|
|
27
|
+
Requires-Dist: simple-module-products==0.0.1
|
|
28
|
+
Requires-Dist: simple-module-users==0.0.1
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# simple_module_dashboard
|
|
32
|
+
|
|
33
|
+
Admin landing page + sidebar menu host for authenticated users of a [simple_module](https://github.com/antosubash/simple_module_python) app. Renders `/dashboard`, collects menu entries registered by every other installed module, and provides the primary Inertia layout.
|
|
34
|
+
|
|
35
|
+
Pre-wired into any app scaffolded with `simple-module new`.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install simple_module_dashboard
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What it provides
|
|
44
|
+
|
|
45
|
+
- `/dashboard` Inertia view, a single entry point for logged-in users.
|
|
46
|
+
- Global sidebar renderer — aggregates `register_menu_items()` calls from all modules into one tree.
|
|
47
|
+
- Breadcrumb + page-title provider used by downstream module pages.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Install the module in a host, and any other module can register a menu entry:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# modules/orders/orders/module.py
|
|
55
|
+
from simple_module_core import ModuleBase, ModuleMeta
|
|
56
|
+
from simple_module_core.menus import MenuItem
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OrdersModule(ModuleBase):
|
|
60
|
+
meta = ModuleMeta(name="orders")
|
|
61
|
+
|
|
62
|
+
def register_menu_items(self):
|
|
63
|
+
return [MenuItem(label="Orders", href="/orders", icon="shopping-bag")]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The dashboard sidebar picks it up automatically.
|
|
67
|
+
|
|
68
|
+
## Depends on
|
|
69
|
+
|
|
70
|
+
- `simple_module_core`, `simple_module_db`, `simple_module_hosting`
|
|
71
|
+
- `simple_module_users`, `simple_module_products` (demo content used by the default layout)
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
dashboard/__init__.py,sha256=5c12Gf0Ehk8pjWvVP5stItAp63wtIRZB7yMY7ibZD44,24
|
|
2
|
+
dashboard/module.py,sha256=KfS1mWeA8DW4CJMLrgZyX-cvlj0UZ5eIh-fuRJcWips,1361
|
|
3
|
+
dashboard/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
dashboard/stats.py,sha256=eu1ygZeLhCiU_J_y7gsIF5w9aCfI-KWGSb16j7Gv2k4,3607
|
|
5
|
+
dashboard/endpoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
dashboard/endpoints/api.py,sha256=WPbzlnrByyr4cJBo51nNPhJMCVeOk09cnKC9igAD78A,544
|
|
7
|
+
dashboard/endpoints/views.py,sha256=lXGJ5-LLE9FtMZG0oP7UMDiIK9ustbjM-as0xfGCfGc,1097
|
|
8
|
+
dashboard/locales/en.json,sha256=uKLPMP5wIT6SbaLhanHZxDL_Uui5u4vDkZx3clopU3g,682
|
|
9
|
+
dashboard/locales/es.json,sha256=FVnhG-mCKhZcWlahBxufV6Z1NgYHJG0QP3-19Uen5cw,729
|
|
10
|
+
dashboard/pages/Home.tsx,sha256=6VmmjukBooSt6XQvrXpaafTJMJd3IpnJ5ou5ATOyrHM,6034
|
|
11
|
+
dashboard/package.json,sha256=rQk8G963uVGfmSjghde11OPEBgQrLfb7GpdKvibY-nU,381
|
|
12
|
+
simple_module_dashboard-0.0.1.dist-info/METADATA,sha256=0IBgXU7qYPohMhYHhKDbKmq4SD7FRsrrGJrZcAi86oI,2817
|
|
13
|
+
simple_module_dashboard-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
14
|
+
simple_module_dashboard-0.0.1.dist-info/entry_points.txt,sha256=ZETTXF8ULqw2Xv-nwuSEab5dgtdCsa2h6tITh6HwAEg,61
|
|
15
|
+
simple_module_dashboard-0.0.1.dist-info/licenses/LICENSE,sha256=Yn66lhLklsF5p7pa85_ksQrJ79Q-FgOaUAHevLBjer4,1068
|
|
16
|
+
simple_module_dashboard-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anto Subash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|