diff --git a/framework/cli/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py b/framework/cli/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py
index 82788b90..4ef96412 100644
--- a/framework/cli/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py
+++ b/framework/cli/simple_module_cli/templates/host/_optional/background_tasks/run_worker.py
@@ -11,7 +11,16 @@
from __future__ import annotations
-from background_tasks.celery_app import build_celery
-from background_tasks.settings import BackgroundTasksSettings
+from pathlib import Path
+
+from simple_module_core.dotenv import load_dotenv_into_environ
+
+# Match the precedence uvicorn gives the web process: load ``.env`` into the
+# environment before importing settings, so the worker doesn't fall back to
+# SQLite defaults when celery is launched outside the host's cwd.
+load_dotenv_into_environ(Path(__file__).resolve().parent.parent / ".env")
+
+from background_tasks.celery_app import build_celery # noqa: E402
+from background_tasks.settings import BackgroundTasksSettings # noqa: E402
celery = build_celery(BackgroundTasksSettings())
diff --git a/framework/cli/simple_module_cli/templates/host/client_app/vite.config.ts b/framework/cli/simple_module_cli/templates/host/client_app/vite.config.ts
index 969688a1..5bed11e8 100644
--- a/framework/cli/simple_module_cli/templates/host/client_app/vite.config.ts
+++ b/framework/cli/simple_module_cli/templates/host/client_app/vite.config.ts
@@ -22,11 +22,21 @@ if (fs.existsSync(manifestPath)) {
export default defineConfig({
plugins: [react(), tailwindcss()],
root: __dirname,
- // Pre-bundle the use-sync-external-store CJS shim so its named export
- // resolves under ESM. recharts (and anything else pulling in react-redux)
- // breaks without this.
+ // Force every importer to resolve to one React copy — without it,
+ // plugin-react's Fast Refresh preamble check fires in a realm where its
+ // global was never set ("can't detect preamble").
+ resolve: {
+ dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
+ },
+ // ``use-sync-external-store`` is the CJS shim recharts/react-redux pull
+ // in; pre-bundling resolves its named export under ESM.
optimizeDeps: {
include: [
+ 'react',
+ 'react-dom',
+ 'react-dom/client',
+ 'react/jsx-runtime',
+ 'react/jsx-dev-runtime',
'use-sync-external-store',
'use-sync-external-store/shim',
'use-sync-external-store/shim/with-selector',
diff --git a/framework/cli/simple_module_cli/templates/module/tests/__init__.py b/framework/cli/simple_module_cli/templates/module/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/framework/cli/tests/test_scaffolding_module.py b/framework/cli/tests/test_scaffolding_module.py
index ece250ff..4773e570 100644
--- a/framework/cli/tests/test_scaffolding_module.py
+++ b/framework/cli/tests/test_scaffolding_module.py
@@ -19,13 +19,17 @@ async def test_creates_expected_module_files(self, tmp_path):
"my_feature/module.py",
"my_feature/endpoints/__init__.py",
"my_feature/endpoints/api.py",
- "tests/__init__.py",
"tests/test_module.py",
".gitignore",
"README.md",
]:
assert (dest / relpath).is_file(), f"missing: {relpath}"
+ # `tests/__init__.py` is intentionally NOT shipped: under
+ # `--import-mode=importlib`, having one in two modules makes
+ # pytest try to register `tests.conftest` as a plugin twice.
+ assert not (dest / "tests" / "__init__.py").exists()
+
async def test_pyproject_declares_entry_point_and_deps(self, tmp_path):
"""pyproject.toml sets the entry_point and pins the framework API range."""
from simple_module_cli.scaffolding import create_module
diff --git a/framework/core/simple_module_core/dotenv.py b/framework/core/simple_module_core/dotenv.py
index 4c75bc11..6842ca80 100644
--- a/framework/core/simple_module_core/dotenv.py
+++ b/framework/core/simple_module_core/dotenv.py
@@ -1,4 +1,4 @@
-"""Minimal ``.env`` parser — dependency-free.
+"""Minimal ``.env`` parser + env-var helpers — dependency-free.
Used in places that can't or shouldn't pull in ``pydantic-settings`` (the
diagnostics CLI runs before the host package is imported; the users-module
@@ -11,6 +11,9 @@
import os
from pathlib import Path
+BOOL_LITERALS_TRUE = frozenset({"1", "true", "t", "yes", "y", "on"})
+BOOL_LITERALS_FALSE = frozenset({"0", "false", "f", "no", "n", "off"})
+
def parse_dotenv(path: Path | None = None) -> dict[str, str]:
"""Parse a ``.env`` file into a dict. Empty dict if the file is missing.
@@ -36,3 +39,31 @@ def parse_dotenv(path: Path | None = None) -> dict[str, str]:
key, _, value = line.partition("=")
parsed[key.strip()] = value.strip().strip('"').strip("'")
return parsed
+
+
+def load_dotenv_into_environ(path: Path | None = None) -> None:
+ """Merge ``parse_dotenv(path)`` into ``os.environ`` via ``setdefault``.
+
+ Same precedence as the web process under uvicorn: real environment wins
+ over file values. Worker entrypoints call this before importing settings.
+ """
+ for key, value in parse_dotenv(path).items():
+ os.environ.setdefault(key, value)
+
+
+def env_str(name: str, default: str) -> str:
+ """Return ``$name`` if set and non-empty, else ``default``."""
+ value = os.environ.get(name, "").strip()
+ return value or default
+
+
+def env_bool(name: str, default: bool = False) -> bool:
+ """Parse ``$name`` as a boolean, returning ``default`` when unset/blank."""
+ raw = os.environ.get(name, "").strip().lower()
+ if not raw:
+ return default
+ if raw in BOOL_LITERALS_TRUE:
+ return True
+ if raw in BOOL_LITERALS_FALSE:
+ return False
+ return default
diff --git a/framework/core/simple_module_core/module.py b/framework/core/simple_module_core/module.py
index da88f952..84b91610 100644
--- a/framework/core/simple_module_core/module.py
+++ b/framework/core/simple_module_core/module.py
@@ -107,8 +107,13 @@ def register_permissions(self, registry: PermissionRegistry) -> None:
def register_feature_flags(self, registry: FeatureFlagRegistry) -> None:
"""Declare feature flags this module exposes."""
- def register_event_handlers(self, bus: EventBus) -> None:
- """Subscribe to events published by other modules."""
+ def register_event_handlers(self, bus: EventBus, app: FastAPI | None = None) -> None:
+ """Subscribe to events published by other modules.
+
+ ``app`` is optional for back-compat; pass it through to handlers
+ that need ``app.state.sm.db.session_factory`` to persist on the
+ framework's engine instead of building their own.
+ """
def register_health_checks(self, registry: HealthRegistry) -> None:
"""Contribute health checks for the ``/health/ready`` endpoint."""
diff --git a/framework/db/simple_module_db/base.py b/framework/db/simple_module_db/base.py
index 36d428fd..1def82c1 100644
--- a/framework/db/simple_module_db/base.py
+++ b/framework/db/simple_module_db/base.py
@@ -4,6 +4,7 @@
import os
+from simple_module_core.dotenv import env_bool
from sqlalchemy import MetaData
from sqlmodel import SQLModel
@@ -39,13 +40,28 @@ def _register_base(base: type[SQLModel]) -> None:
def _default_provider() -> DatabaseProvider:
- """Resolve the active provider from ``SM_DATABASE_URL`` at import time.
-
- Falls back to SQLite when the variable is unset, keeping the common
- dev-loop happy without requiring callers to plumb the provider through.
+ """Resolve the schema layout to register module tables under.
+
+ Drives whether module tables get a dedicated Postgres schema
+ (``orders.
``) or share the public schema with prefixed names
+ (``orders_
``). ``SM_SCHEMA_PER_MODULE`` is authoritative when
+ set; the ``SM_DATABASE_URL`` fallback is kept for back-compat with
+ deployments that haven't migrated to the explicit knob — workers and
+ the web process can disagree on whether the URL is set, so prefer
+ the explicit form going forward.
"""
+ explicit = os.environ.get("SM_SCHEMA_PER_MODULE")
+ if explicit is not None:
+ return (
+ DatabaseProvider.POSTGRESQL
+ if env_bool("SM_SCHEMA_PER_MODULE")
+ else DatabaseProvider.SQLITE
+ )
+
url = os.environ.get("SM_DATABASE_URL", "")
- return detect_provider(url) if url else DatabaseProvider.SQLITE
+ if url:
+ return detect_provider(url)
+ return DatabaseProvider.SQLITE
def create_module_base(
diff --git a/framework/db/simple_module_db/session.py b/framework/db/simple_module_db/session.py
index a3f0c3df..eb3843f5 100644
--- a/framework/db/simple_module_db/session.py
+++ b/framework/db/simple_module_db/session.py
@@ -33,12 +33,15 @@ def init_db(
max_overflow: int = 20,
pool_pre_ping: bool = True,
pool_recycle: int = 1800,
+ poolclass: type | None = None,
) -> DatabaseState:
"""Create an async engine and session factory.
- The pool options only take effect for server-side providers (Postgres).
- SQLite uses SQLAlchemy's default pool (single-file, no network), so
- passing ``pool_size``/etc. would raise ``TypeError`` — skipped below.
+ Pool tuning only applies to server-side providers (Postgres) — SQLite
+ rejects ``pool_size``/etc. Pass ``poolclass=NullPool`` from test
+ fixtures running against asyncpg/Postgres so pytest-asyncio's per-test
+ event loops don't outlive pooled connections; the pool-tuning kwargs
+ are ignored in that case.
Returns a ``DatabaseState`` that should be stored on ``app.state.db``.
"""
@@ -46,9 +49,11 @@ def init_db(
connect_args: dict = {}
engine_kwargs: dict = {"echo": echo, "connect_args": connect_args}
+ if poolclass is not None:
+ engine_kwargs["poolclass"] = poolclass
if provider == DatabaseProvider.SQLITE:
connect_args["check_same_thread"] = False
- else:
+ elif poolclass is None:
engine_kwargs.update(
pool_size=pool_size,
max_overflow=max_overflow,
diff --git a/framework/hosting/simple_module_hosting/app_builder.py b/framework/hosting/simple_module_hosting/app_builder.py
index 79d8945d..c30d7028 100644
--- a/framework/hosting/simple_module_hosting/app_builder.py
+++ b/framework/hosting/simple_module_hosting/app_builder.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import inspect
import logging
import os
from collections.abc import AsyncGenerator
@@ -75,6 +76,22 @@ def _resolve_project_root() -> Path:
_PROJECT_ROOT = _resolve_project_root()
+def _register_event_handlers(mod, event_bus: EventBus, app: FastAPI) -> None:
+ """Dispatch to ``mod.register_event_handlers`` with or without ``app``.
+
+ Back-compat shim for modules that still override the one-arg form
+ ``(self, bus)``; passing ``app=`` to those crashes.
+ """
+ sig = inspect.signature(mod.register_event_handlers)
+ accepts_app = "app" in sig.parameters or any(
+ p.kind is inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
+ )
+ if accepts_app:
+ mod.register_event_handlers(event_bus, app=app)
+ else:
+ mod.register_event_handlers(event_bus)
+
+
def create_app(settings: Settings | None = None) -> FastAPI:
"""Build and configure the full FastAPI application.
@@ -218,7 +235,7 @@ class _HostServices:
mod.register_menu_items(menu_registry)
mod.register_permissions(perm_registry)
mod.register_feature_flags(ff_registry)
- mod.register_event_handlers(event_bus)
+ _register_event_handlers(mod, event_bus, app)
mod.register_health_checks(health_registry)
logger.info(
diff --git a/framework/hosting/simple_module_hosting/middleware.py b/framework/hosting/simple_module_hosting/middleware.py
index 3fdbce37..174647f6 100644
--- a/framework/hosting/simple_module_hosting/middleware.py
+++ b/framework/hosting/simple_module_hosting/middleware.py
@@ -79,6 +79,11 @@ class SecurityHeadersMiddleware:
"img-src 'self' data: blob:; "
"font-src 'self' https://fonts.gstatic.com data:; "
"connect-src 'self'; "
+ # Allow blob: workers — MapLibre, comlink, web-tree-sitter and most
+ # WASM libs ship their worker as a Blob URL. `child-src` is the
+ # legacy fallback some browsers consult before `worker-src`.
+ "worker-src 'self' blob:; "
+ "child-src 'self' blob:; "
"frame-ancestors 'self'; "
"base-uri 'self'; "
"form-action 'self'"
@@ -104,6 +109,8 @@ def dev_csp(vite_dev_url: str) -> str:
"img-src 'self' data: blob:; "
"font-src 'self' https://fonts.gstatic.com data:; "
f"connect-src 'self' {vite_dev_url} {ws_url}; "
+ "worker-src 'self' blob:; "
+ "child-src 'self' blob:; "
"frame-ancestors 'self'; "
"base-uri 'self'; "
"form-action 'self'"
diff --git a/framework/testing/simple_module_test/plugin.py b/framework/testing/simple_module_test/plugin.py
index 8d53d563..8276909f 100644
--- a/framework/testing/simple_module_test/plugin.py
+++ b/framework/testing/simple_module_test/plugin.py
@@ -8,12 +8,32 @@
from __future__ import annotations
+import contextlib
+
import pytest
from simple_module_test.app_factory import build_test_app as _build_test_app
from simple_module_test.fake_events import FakeEventBus
+def _bootstrap_eager_celery() -> None:
+ """Register a process-wide eager Celery app before any test runs.
+
+ Tests that don't use the ``client`` fixture never trigger the lifespan,
+ so the host's ``build_celery`` call never runs and ``task.delay()``
+ falls through to the broker. Skipped silently when ``background_tasks``
+ isn't installed.
+ """
+ with contextlib.suppress(ImportError):
+ from background_tasks.celery_app import build_celery
+ from background_tasks.settings import BackgroundTasksSettings
+
+ build_celery(BackgroundTasksSettings(task_always_eager=True, task_eager_propagates=True))
+
+
+_bootstrap_eager_celery()
+
+
@pytest.fixture
def fake_event_bus() -> FakeEventBus:
"""Fresh recording EventBus for each test."""
diff --git a/host/client_app/vite.config.ts b/host/client_app/vite.config.ts
index a3fc9514..c8900d89 100644
--- a/host/client_app/vite.config.ts
+++ b/host/client_app/vite.config.ts
@@ -49,6 +49,20 @@ export default defineConfig({
// feed Vite the per-module tsconfigs.
resolve: {
tsconfigPaths: true,
+ // Force every importer (host, workspace module, wheel-installed module)
+ // to resolve to one React copy — without it, plugin-react's Fast
+ // Refresh preamble check fires in a realm where its global was never
+ // set ("can't detect preamble").
+ dedupe: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
+ },
+ optimizeDeps: {
+ include: [
+ 'react',
+ 'react-dom',
+ 'react-dom/client',
+ 'react/jsx-runtime',
+ 'react/jsx-dev-runtime',
+ ],
},
root: __dirname,
build: {
diff --git a/modules/background_tasks/background_tasks/celery_app.py b/modules/background_tasks/background_tasks/celery_app.py
index e7aa87d0..1eaa5b40 100644
--- a/modules/background_tasks/background_tasks/celery_app.py
+++ b/modules/background_tasks/background_tasks/celery_app.py
@@ -55,6 +55,11 @@ def build_celery(settings: BackgroundTasksSettings) -> Celery:
broker_url=settings.broker_url,
result_backend=settings.result_backend,
task_default_queue=settings.task_default_queue,
+ # Run tasks synchronously inside the calling process. Tests
+ # toggle this on (via ``SM_BG_TASKS_TASK_ALWAYS_EAGER=true``) so
+ # ``task.delay()`` doesn't reach for a real broker.
+ task_always_eager=settings.task_always_eager,
+ task_eager_propagates=settings.task_eager_propagates,
# ``task_track_started`` gives us the ``STARTED`` state so
# ``task_prerun`` can flip our row to ``running``.
task_track_started=True,
diff --git a/modules/background_tasks/background_tasks/settings.py b/modules/background_tasks/background_tasks/settings.py
index bfe03a0e..63d84a6d 100644
--- a/modules/background_tasks/background_tasks/settings.py
+++ b/modules/background_tasks/background_tasks/settings.py
@@ -22,6 +22,7 @@
from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
+from simple_module_core.dotenv import env_bool
from simple_module_core.environments import NON_PROD_ENVIRONMENTS
from background_tasks.constants import (
@@ -47,6 +48,13 @@ class BackgroundTasksSettings(BaseSettings):
result_backend: str = Field(default=DEFAULT_RESULT_BACKEND, json_schema_extra=_CELERY_RESTART)
task_default_queue: str = Field(default=DEFAULT_QUEUE, json_schema_extra=_CELERY_RESTART)
+ # Run tasks synchronously inside the calling process. Read at
+ # module-import time so tests can flip it on via ``SM_BG_TASKS_*``
+ # without going through DB-backed hydration (which never fires for
+ # suites that don't use the FastAPI lifespan).
+ task_always_eager: bool = env_bool("SM_BG_TASKS_TASK_ALWAYS_EAGER")
+ task_eager_propagates: bool = True
+
# A task that has been ``running`` longer than this without a heartbeat is
# flipped to ``stuck`` by the beat sweep. 5 min is long enough to cover
# normal slow jobs while still surfacing wedged workers within one UI
diff --git a/modules/users/users/endpoints/views.py b/modules/users/users/endpoints/views.py
index 333df581..fd4955f7 100644
--- a/modules/users/users/endpoints/views.py
+++ b/modules/users/users/endpoints/views.py
@@ -68,7 +68,11 @@ async def login_page(request: Request, inertia: InertiaDep) -> InertiaResponse:
dev_accounts.append({"label": "User", "email": user_email, "password": user_password})
return await inertia.render(
_PAGE_LOGIN,
- {"allow_signup": users_settings.allow_signup, "dev_accounts": dev_accounts},
+ {
+ "allow_signup": users_settings.allow_signup,
+ "dev_accounts": dev_accounts,
+ "login_redirect_url": users_settings.login_redirect_url,
+ },
)
diff --git a/modules/users/users/module.py b/modules/users/users/module.py
index 390c0f83..16d46c7e 100644
--- a/modules/users/users/module.py
+++ b/modules/users/users/module.py
@@ -147,6 +147,14 @@ async def on_startup(self, app: FastAPI) -> None:
window_seconds=s.auth_rate_limit_window_seconds,
)
+ # Auto-fall-back from the default ``/dashboard/`` to ``/`` when the
+ # Dashboard module isn't installed, so ``--preset minimal`` doesn't
+ # 404 on login. Operator-set overrides are preserved.
+ if s.login_redirect_url == "/dashboard/" and not any(
+ m.meta.name == "Dashboard" for m in app.state.sm.modules
+ ):
+ s.login_redirect_url = "/"
+
reconfigure_cookie_transport(auth_backend, s)
await asyncio.gather(
diff --git a/modules/users/users/pages/Login.tsx b/modules/users/users/pages/Login.tsx
index 1b21d5e8..0733be38 100644
--- a/modules/users/users/pages/Login.tsx
+++ b/modules/users/users/pages/Login.tsx
@@ -21,10 +21,12 @@ interface DevAccount {
interface Props {
allow_signup: boolean;
dev_accounts: DevAccount[];
+ login_redirect_url: string;
}
function Login() {
- const { allow_signup, dev_accounts } = usePage<{ props: Props }>().props as unknown as Props;
+ const { allow_signup, dev_accounts, login_redirect_url } = usePage<{ props: Props }>()
+ .props as unknown as Props;
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -34,8 +36,8 @@ function Login() {
const nextUrl =
typeof window !== 'undefined'
- ? new URLSearchParams(window.location.search).get('next') || '/dashboard/'
- : '/dashboard/';
+ ? new URLSearchParams(window.location.search).get('next') || login_redirect_url
+ : login_redirect_url;
const submitLogin = (username: string, pwd: string) => {
setError(null);
diff --git a/modules/users/users/settings.py b/modules/users/users/settings.py
index 3e6cba57..64b20e03 100644
--- a/modules/users/users/settings.py
+++ b/modules/users/users/settings.py
@@ -1,13 +1,11 @@
"""Users module settings — DB-backed via ``register_module_settings``.
-Construction no longer reads ``SM_USERS_*`` environment variables. Values
-come from pydantic defaults at boot, then get hydrated from the DB by the
-hosting lifespan before module ``on_startup`` runs. Runtime changes go
-through ``settings.reload.apply_changes_and_reload``.
-
-The one remaining env read is ``SM_ENVIRONMENT``, consulted by the
-``@model_validator`` to refuse placeholder token secrets in production —
-that's a host-level setting, not a users-module field.
+Most fields hydrate from the DB after boot, with pydantic defaults filling in
+until then. Runtime changes go through ``settings.reload.apply_changes_and_reload``.
+
+The two token-secret fields read ``SM_USERS_*`` at import time as a bootstrap
+path: a fresh production deploy needs to clear the validator below before any
+DB-backed settings can be seeded, otherwise the two paths deadlock.
"""
from __future__ import annotations
@@ -16,6 +14,7 @@
from pydantic import Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
+from simple_module_core.dotenv import env_str
from simple_module_core.environments import NON_PROD_ENVIRONMENTS
_PLACEHOLDER_RESET_SECRET = "dev-reset-token-secret-change-me"
@@ -31,10 +30,23 @@ class UsersSettings(BaseSettings):
allow_signup: bool = False
require_verification: bool = True
+ # Where the login page sends a successful sign-in. Sites without the
+ # bundled ``dashboard`` module (``sm new --preset minimal``) override
+ # this to wherever their post-login landing lives.
+ login_redirect_url: str = "/dashboard/"
+
# Token secrets — MUST be set in production. Dev default is a deterministic
# placeholder that's obvious in logs so it can't be mistaken for a real key.
- reset_password_token_secret: str = "dev-reset-token-secret-change-me"
- verification_token_secret: str = "dev-verify-token-secret-change-me"
+ # Resolved at module-import time so ``info.default`` stays serializable
+ # for the settings admin UI.
+ reset_password_token_secret: str = env_str(
+ "SM_USERS_RESET_PASSWORD_TOKEN_SECRET",
+ _PLACEHOLDER_RESET_SECRET,
+ )
+ verification_token_secret: str = env_str(
+ "SM_USERS_VERIFICATION_TOKEN_SECRET",
+ _PLACEHOLDER_VERIFY_SECRET,
+ )
reset_password_token_lifetime_seconds: int = 60 * 60 # 1 hour
verification_token_lifetime_seconds: int = 60 * 60 * 24 * 7 # 7 days
diff --git a/packages/ui/src/components/NavIcon.tsx b/packages/ui/src/components/NavIcon.tsx
index 3037b7e7..7bd90a39 100644
--- a/packages/ui/src/components/NavIcon.tsx
+++ b/packages/ui/src/components/NavIcon.tsx
@@ -1,181 +1,159 @@
-import type React from 'react';
+import {
+ Activity,
+ AlertCircle,
+ Archive,
+ BarChart3,
+ Bell,
+ Bookmark,
+ Briefcase,
+ Calendar,
+ Camera,
+ Check,
+ ChevronDown,
+ ChevronRight,
+ Clock,
+ Cloud,
+ Code,
+ Cog,
+ Copy,
+ Cpu,
+ CreditCard,
+ Database,
+ Download,
+ Edit,
+ Eye,
+ EyeOff,
+ File,
+ Files,
+ FileText,
+ Filter,
+ Flag,
+ Folder,
+ Gift,
+ Globe,
+ Heart,
+ Home,
+ Image,
+ Inbox,
+ Info,
+ Key,
+ Layers,
+ Layout,
+ Link,
+ List,
+ Lock,
+ LogOut,
+ Mail,
+ Map as MapIcon,
+ Menu,
+ MessageSquare,
+ Package,
+ Pencil,
+ Plus,
+ RefreshCw,
+ Save,
+ Search,
+ Send,
+ Server,
+ Settings,
+ Share,
+ Shield,
+ ShieldCheck,
+ ShoppingBag,
+ ShoppingCart,
+ Sparkles,
+ Star,
+ Tag,
+ Terminal,
+ Trash,
+ Upload,
+ User,
+ Users,
+ X,
+ Zap,
+} from 'lucide-react';
-const ICONS: Record = {
- home: (
-
- ),
- package: (
-
- ),
- users: (
-
- ),
- user: (
-
- ),
- settings: (
-
- ),
- shield: (
-
- ),
- activity: (
-
- ),
- files: (
-
- ),
- layers: (
-
- ),
- 'shield-check': (
-
- ),
- 'log-out': (
-
- ),
-};
+const ICON_MAP = {
+ activity: Activity,
+ 'alert-circle': AlertCircle,
+ archive: Archive,
+ 'bar-chart-3': BarChart3,
+ bell: Bell,
+ bookmark: Bookmark,
+ briefcase: Briefcase,
+ calendar: Calendar,
+ camera: Camera,
+ check: Check,
+ 'chevron-down': ChevronDown,
+ 'chevron-right': ChevronRight,
+ clock: Clock,
+ cloud: Cloud,
+ code: Code,
+ cog: Cog,
+ copy: Copy,
+ cpu: Cpu,
+ 'credit-card': CreditCard,
+ database: Database,
+ download: Download,
+ edit: Edit,
+ eye: Eye,
+ 'eye-off': EyeOff,
+ file: File,
+ 'file-text': FileText,
+ files: Files,
+ filter: Filter,
+ flag: Flag,
+ folder: Folder,
+ gift: Gift,
+ globe: Globe,
+ heart: Heart,
+ home: Home,
+ image: Image,
+ inbox: Inbox,
+ info: Info,
+ key: Key,
+ layers: Layers,
+ layout: Layout,
+ link: Link,
+ list: List,
+ lock: Lock,
+ 'log-out': LogOut,
+ mail: Mail,
+ map: MapIcon,
+ menu: Menu,
+ 'message-square': MessageSquare,
+ package: Package,
+ pencil: Pencil,
+ plus: Plus,
+ 'refresh-cw': RefreshCw,
+ save: Save,
+ search: Search,
+ send: Send,
+ server: Server,
+ settings: Settings,
+ share: Share,
+ shield: Shield,
+ 'shield-check': ShieldCheck,
+ 'shopping-bag': ShoppingBag,
+ 'shopping-cart': ShoppingCart,
+ sparkles: Sparkles,
+ star: Star,
+ tag: Tag,
+ terminal: Terminal,
+ trash: Trash,
+ upload: Upload,
+ user: User,
+ users: Users,
+ x: X,
+ zap: Zap,
+} as const;
+
+export type NavIconName = keyof typeof ICON_MAP;
export function NavIcon({ name }: { name: string }) {
- return <>{ICONS[name] || }>;
+ if (!(name in ICON_MAP)) {
+ return ;
+ }
+ const Icon = ICON_MAP[name as NavIconName];
+ return ;
}
diff --git a/scripts/run_worker.py b/scripts/run_worker.py
index 9eb20f87..7efe7438 100644
--- a/scripts/run_worker.py
+++ b/scripts/run_worker.py
@@ -12,8 +12,17 @@
from __future__ import annotations
-from background_tasks.celery_app import build_celery
-from background_tasks.settings import BackgroundTasksSettings
+from pathlib import Path
+
+from simple_module_core.dotenv import load_dotenv_into_environ
+
+# Match the precedence uvicorn gives the web process: load ``host/.env`` into
+# the environment before importing settings, so the worker doesn't fall back
+# to SQLite defaults when celery is launched outside the host's cwd.
+load_dotenv_into_environ(Path(__file__).resolve().parent.parent / "host" / ".env")
+
+from background_tasks.celery_app import build_celery # noqa: E402
+from background_tasks.settings import BackgroundTasksSettings # noqa: E402
# Module-level name ``celery`` is what ``celery -A scripts.run_worker:celery``
# looks for. Keep it stable.