Complete Next.js 16 application with 13 fully implemented modules: Email Signature, Word XML Generator, Registratura, Dashboard, Tag Manager, IT Inventory, Address Book, Password Vault, Mini Utilities, Prompt Generator, Digital Signatures, Word Templates, and AI Chat. Includes core platform systems (module registry, feature flags, storage abstraction, i18n, theming, auth stub, tagging), 16 technical documentation files, Docker deployment config, and legacy HTML tool reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
513 lines
19 KiB
Markdown
513 lines
19 KiB
Markdown
# Module System
|
|
|
|
> ArchiTools internal architecture reference -- module registry, loader, and lifecycle.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
ArchiTools is composed of discrete **modules**, each representing a self-contained functional unit (e.g., *Devize*, *Oferte*, *Pontaj*). Modules are registered in a central registry, gated behind feature flags, lazily loaded at runtime, and isolated from one another. This design provides three guarantees:
|
|
|
|
1. **Zero bundle cost** for disabled modules -- they are never imported.
|
|
2. **Runtime safety** -- a crash in one module cannot propagate to another.
|
|
3. **Clear ownership boundaries** -- each module owns its types, storage, and UI.
|
|
|
|
---
|
|
|
|
## Module Definition Model
|
|
|
|
Every module exposes a `config.ts` at its root that exports a `ModuleConfig` object. This is the module's identity card -- it tells the platform everything it needs to know to register, gate, load, and display the module.
|
|
|
|
### `ModuleConfig` Interface
|
|
|
|
```typescript
|
|
// src/core/module-registry/types.ts
|
|
|
|
type ModuleCategory = 'operations' | 'generators' | 'management' | 'tools' | 'ai';
|
|
type Visibility = 'all' | 'admin' | 'internal';
|
|
|
|
interface ModuleConfig {
|
|
id: string; // unique kebab-case identifier, e.g. "devize-generator"
|
|
name: string; // Romanian display name, e.g. "Generator Devize"
|
|
description: string; // Romanian description shown in module catalog
|
|
icon: string; // Lucide icon name, e.g. "file-spreadsheet"
|
|
route: string; // base route path, e.g. "/devize"
|
|
category: ModuleCategory; // sidebar grouping
|
|
featureFlag: string; // corresponding key in the feature flag system
|
|
visibility: Visibility; // audience restriction
|
|
version: string; // semver, e.g. "1.0.0"
|
|
dependencies?: string[]; // other module IDs this depends on
|
|
storageNamespace: string; // isolated localStorage/IndexedDB prefix
|
|
navOrder: number; // sidebar sort order within its category
|
|
tags?: string[]; // capability tags for search/filtering
|
|
}
|
|
```
|
|
|
|
### Field Contracts
|
|
|
|
| Field | Constraint |
|
|
|---|---|
|
|
| `id` | Must be unique across all modules. Kebab-case. Used as the key in the registry map. |
|
|
| `route` | Must start with `/`. Must not collide with another module's route. |
|
|
| `featureFlag` | Must have a matching entry in `src/config/flags.ts`. |
|
|
| `storageNamespace` | Must be unique. Convention: `architools.[module-id]`. |
|
|
| `navOrder` | Lower numbers appear first. Modules in the same category are sorted by this value. |
|
|
| `dependencies` | If specified, the platform will refuse to load this module unless all listed module IDs are also enabled and loaded. |
|
|
|
|
### Example
|
|
|
|
```typescript
|
|
// src/modules/devize-generator/config.ts
|
|
|
|
import { ModuleConfig } from '@/core/module-registry/types';
|
|
|
|
export const registraturaConfig: ModuleConfig = {
|
|
id: 'registratura',
|
|
name: 'Registratură',
|
|
description: 'Registru de corespondență multi-firmă cu urmărire documente.',
|
|
icon: 'book-open',
|
|
route: '/registratura',
|
|
category: 'operations',
|
|
featureFlag: 'module.registratura',
|
|
visibility: 'all',
|
|
version: '1.0.0',
|
|
dependencies: [],
|
|
storageNamespace: 'architools.registratura',
|
|
navOrder: 10,
|
|
tags: ['registry', 'documents', 'correspondence'],
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## Module Directory Structure
|
|
|
|
Every module lives under `src/modules/` and follows a strict directory convention:
|
|
|
|
```
|
|
src/modules/[module-name]/
|
|
components/ # React components specific to this module
|
|
hooks/ # Custom hooks encapsulating business logic
|
|
services/ # Data services (CRUD, transformations) using the storage abstraction
|
|
types.ts # Module-specific TypeScript types and interfaces
|
|
config.ts # ModuleConfig export (the module's identity)
|
|
index.ts # Public API barrel export
|
|
```
|
|
|
|
### Rules
|
|
|
|
- **`components/`** -- Only UI components consumed within this module. If a component is needed by multiple modules, it belongs in `src/shared/components/common/`.
|
|
- **`hooks/`** -- Business logic hooks. Must not contain UI rendering. Must not directly call `localStorage` or `IndexedDB` -- use the storage abstraction via services.
|
|
- **`services/`** -- All data access goes through services. Services consume the platform's storage abstraction layer (`src/core/storage/`) and must scope all keys under `storageNamespace`.
|
|
- **`types.ts`** -- All TypeScript types that are internal to this module. Shared types go in `src/core/*/types.ts`.
|
|
- **`config.ts`** -- Exports exactly one `ModuleConfig` object. No side effects.
|
|
- **`index.ts`** -- The barrel export. This is the **only** file that other parts of the system (the registry, the loader) may import from. It must export `config` and the lazy-loadable root component.
|
|
|
|
### Barrel Export Convention
|
|
|
|
```typescript
|
|
// src/modules/registratura/index.ts
|
|
|
|
export { registraturaConfig as config } from './config';
|
|
export { default as RegistraturaModule } from './components/RegistraturaModule';
|
|
```
|
|
|
|
The root component (`RegistraturaModule`) is what gets lazy-loaded and rendered at the module's route.
|
|
|
|
---
|
|
|
|
## Module Registry
|
|
|
|
The central registry lives at `src/config/modules.ts`. It imports every module's config and builds a lookup map.
|
|
|
|
```typescript
|
|
// src/config/modules.ts
|
|
|
|
import { registraturaConfig } from '@/modules/registratura/config';
|
|
import { emailSignatureConfig } from '@/modules/email-signature/config';
|
|
import { wordXmlConfig } from '@/modules/word-xml/config';
|
|
import { promptGeneratorConfig } from '@/modules/prompt-generator/config';
|
|
// ... all module configs
|
|
|
|
import type { ModuleConfig } from '@/core/module-registry/types';
|
|
|
|
const moduleConfigs: ModuleConfig[] = [
|
|
registraturaConfig,
|
|
emailSignatureConfig,
|
|
wordXmlConfig,
|
|
promptGeneratorConfig,
|
|
// ... add new modules here
|
|
];
|
|
|
|
/** Map of module ID -> ModuleConfig for O(1) lookups */
|
|
export const moduleRegistry: Map<string, ModuleConfig> = new Map(
|
|
moduleConfigs.map((config) => [config.id, config])
|
|
);
|
|
|
|
/** All registered configs, sorted by category then navOrder */
|
|
export const allModules: ModuleConfig[] = [...moduleConfigs].sort((a, b) => {
|
|
if (a.category !== b.category) return a.category.localeCompare(b.category);
|
|
return a.navOrder - b.navOrder;
|
|
});
|
|
|
|
/** Get configs for a specific category */
|
|
export function getModulesByCategory(category: ModuleConfig['category']): ModuleConfig[] {
|
|
return allModules.filter((m) => m.category === category);
|
|
}
|
|
|
|
/** Validate that all module IDs are unique. Called once at startup in dev mode. */
|
|
export function validateRegistry(): void {
|
|
const ids = moduleConfigs.map((m) => m.id);
|
|
const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
if (duplicates.length > 0) {
|
|
throw new Error(`Duplicate module IDs detected: ${duplicates.join(', ')}`);
|
|
}
|
|
|
|
const routes = moduleConfigs.map((m) => m.route);
|
|
const duplicateRoutes = routes.filter((r, i) => routes.indexOf(r) !== i);
|
|
if (duplicateRoutes.length > 0) {
|
|
throw new Error(`Duplicate module routes detected: ${duplicateRoutes.join(', ')}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
The registry is a static, build-time artifact. It does not perform any dynamic discovery -- every module must be explicitly imported. This is intentional: it guarantees that the dependency graph is fully visible to the bundler for tree-shaking and code-splitting.
|
|
|
|
---
|
|
|
|
## Module Loader
|
|
|
|
The loader is responsible for conditionally importing and rendering a module based on its feature flag status.
|
|
|
|
### Lazy Loading Strategy
|
|
|
|
Each module's root component is wrapped in `React.lazy()`. The import only fires when the component is actually rendered, which only happens when the feature flag is enabled.
|
|
|
|
```typescript
|
|
// src/core/module-registry/loader.ts
|
|
|
|
import React, { Suspense } from 'react';
|
|
import type { ModuleConfig } from '@/core/module-registry/types';
|
|
import { ModuleErrorBoundary } from '@/shared/components/common/module-error-boundary';
|
|
import { ModuleSkeleton } from '@/shared/components/common/module-skeleton';
|
|
|
|
/** Registry of lazy component factories, keyed by module ID */
|
|
const lazyComponents: Record<string, React.LazyExoticComponent<React.ComponentType>> = {
|
|
'registratura': React.lazy(() => import('@/modules/registratura')),
|
|
'email-signature': React.lazy(() => import('@/modules/email-signature')),
|
|
'word-xml': React.lazy(() => import('@/modules/word-xml')),
|
|
'prompt-generator': React.lazy(() => import('@/modules/prompt-generator')),
|
|
// ... add new modules here
|
|
};
|
|
|
|
interface ModuleLoaderProps {
|
|
config: ModuleConfig;
|
|
}
|
|
|
|
export function ModuleLoader({ config }: ModuleLoaderProps) {
|
|
const LazyComponent = lazyComponents[config.id];
|
|
|
|
if (!LazyComponent) {
|
|
return <ModuleNotFound moduleId={config.id} />;
|
|
}
|
|
|
|
return (
|
|
<ModuleErrorBoundary moduleId={config.id} moduleName={config.name}>
|
|
<Suspense fallback={<ModuleSkeleton moduleName={config.name} />}>
|
|
<LazyComponent />
|
|
</Suspense>
|
|
</ModuleErrorBoundary>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Key Mechanics
|
|
|
|
1. **`React.lazy()`** accepts a function that returns a dynamic `import()`. The import is only executed the first time the component renders.
|
|
2. **`Suspense`** shows a skeleton loader while the chunk downloads and parses.
|
|
3. **`ModuleErrorBoundary`** catches any rendering error within the module and displays a localized error state without crashing the entire application.
|
|
|
|
### Why Not Automatic Discovery?
|
|
|
|
We intentionally avoid filesystem-based auto-discovery (e.g., `import.meta.glob`) because:
|
|
|
|
- It defeats tree-shaking: the bundler must include every discovered module.
|
|
- It hides the dependency graph from code review.
|
|
- It makes the build non-deterministic if modules are added/removed from the filesystem.
|
|
|
|
Explicit registration is a small amount of boilerplate that pays for itself in predictability.
|
|
|
|
---
|
|
|
|
## Navigation Auto-Discovery
|
|
|
|
The sidebar reads directly from the module registry to build its navigation tree. No separate nav configuration file exists.
|
|
|
|
```typescript
|
|
// src/components/navigation/Sidebar.tsx (simplified)
|
|
|
|
import { allModules } from '@/config/modules';
|
|
import { useFeatureFlag } from '@/core/feature-flags';
|
|
import { groupBy } from '@/shared/lib/utils';
|
|
|
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
operations: 'Operatiuni',
|
|
generators: 'Generatoare',
|
|
management: 'Management',
|
|
tools: 'Instrumente',
|
|
ai: 'AI & Automatizari',
|
|
};
|
|
|
|
export function Sidebar() {
|
|
const grouped = groupBy(allModules, (m) => m.category);
|
|
|
|
return (
|
|
<nav>
|
|
{Object.entries(CATEGORY_LABELS).map(([category, label]) => {
|
|
const modules = grouped[category] ?? [];
|
|
const visibleModules = modules.filter((m) => {
|
|
const flagEnabled = useFeatureFlag(m.featureFlag);
|
|
return flagEnabled;
|
|
});
|
|
|
|
if (visibleModules.length === 0) return null;
|
|
|
|
return (
|
|
<SidebarGroup key={category} label={label}>
|
|
{visibleModules.map((m) => (
|
|
<SidebarItem key={m.id} icon={m.icon} href={m.route}>
|
|
{m.name}
|
|
</SidebarItem>
|
|
))}
|
|
</SidebarGroup>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|
|
```
|
|
|
|
This means:
|
|
- Adding a module to the registry and enabling its flag automatically adds it to the sidebar.
|
|
- Disabling a flag automatically removes the module from navigation.
|
|
- `navOrder` controls position within each category group.
|
|
- `visibility` can be checked against the current user's role for further filtering.
|
|
|
|
---
|
|
|
|
## Module Isolation Rules
|
|
|
|
Modules must be isolated from one another. This is enforced by convention and code review.
|
|
|
|
### What Is Allowed
|
|
|
|
| From | To | Allowed? |
|
|
|---|---|---|
|
|
| Module A component | Module A hook | Yes |
|
|
| Module A service | `src/lib/storage/` | Yes |
|
|
| Module A component | `src/components/shared/` | Yes |
|
|
| Module A hook | `src/lib/*` (platform utilities) | Yes |
|
|
| Module A | Module B (any file) | **No** |
|
|
|
|
### What Is Not Allowed
|
|
|
|
- **Direct imports between modules.** Module A must never `import { something } from '@/modules/module-b/...'`. If two modules need to share data, they do so through a shared service in `src/lib/` or through an event bus.
|
|
- **Shared mutable state.** No module may write to a global store that another module reads, unless that store is a platform-level shared service (e.g., user context, notification system).
|
|
- **Cross-module storage access.** Each module's `storageNamespace` is its own territory. Module A must never read or write keys prefixed with Module B's namespace.
|
|
|
|
### Enforcement
|
|
|
|
- **ESLint rule** (planned): a custom `no-restricted-imports` pattern that forbids `@/modules/*/` imports from within a different module directory.
|
|
- **Code review**: any PR that introduces a cross-module import must be flagged.
|
|
|
|
---
|
|
|
|
## Module Lifecycle
|
|
|
|
A module goes through the following stages from registration to active use:
|
|
|
|
```
|
|
1. Registration
|
|
Module config is imported into src/config/modules.ts.
|
|
Module appears in the registry map.
|
|
|
|
2. Feature Flag Check
|
|
At render time, the navigation system and route handler check
|
|
the module's featureFlag via useFeatureFlag(). If disabled,
|
|
the module is invisible and its code is never loaded.
|
|
|
|
3. Lazy Load
|
|
When the user navigates to the module's route (and the flag is
|
|
enabled), React.lazy() triggers the dynamic import(). The
|
|
bundler fetches the module's code chunk over the network.
|
|
|
|
4. Suspense Boundary
|
|
While the chunk loads, the Suspense fallback (ModuleSkeleton)
|
|
is displayed.
|
|
|
|
5. Error Boundary Setup
|
|
The ModuleErrorBoundary wraps the module. If any error occurs
|
|
during mount or subsequent renders, the boundary catches it.
|
|
|
|
6. Mount
|
|
The module's root component mounts. It may initialize hooks,
|
|
call services, and read from its storage namespace.
|
|
|
|
7. Storage Namespace Init
|
|
On first use, the module's services initialize their storage
|
|
keys under the module's storageNamespace prefix. This happens
|
|
lazily -- no storage is allocated until the module actually
|
|
writes data.
|
|
|
|
8. Active
|
|
The module is fully interactive. It manages its own state
|
|
and communicates with the platform only through shared services.
|
|
|
|
9. Unmount
|
|
When the user navigates away, the module unmounts normally via
|
|
React's unmount lifecycle. Cleanup happens in useEffect
|
|
return functions as usual.
|
|
```
|
|
|
|
### Diagram
|
|
|
|
```
|
|
Registration ──> Flag Check ──┬──> [disabled] ──> Not loaded, not visible
|
|
│
|
|
└──> [enabled] ──> Lazy Load ──> Suspense ──> Mount ──> Active
|
|
│
|
|
└──> [error] ──> Error Boundary UI
|
|
```
|
|
|
|
---
|
|
|
|
## Error Boundaries
|
|
|
|
Every module is wrapped in a `ModuleErrorBoundary`. This is a React error boundary class component that:
|
|
|
|
1. Catches JavaScript errors in the module's component tree.
|
|
2. Logs the error with the module's ID for diagnostics.
|
|
3. Renders a fallback UI with the module name and a retry button.
|
|
4. Does **not** crash the shell (sidebar, header, other modules remain functional).
|
|
|
|
```typescript
|
|
// src/components/ModuleErrorBoundary.tsx
|
|
|
|
import React from 'react';
|
|
|
|
interface Props {
|
|
moduleId: string;
|
|
moduleName: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
interface State {
|
|
hasError: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export class ModuleErrorBoundary extends React.Component<Props, State> {
|
|
state: State = { hasError: false, error: null };
|
|
|
|
static getDerivedStateFromError(error: Error): State {
|
|
return { hasError: true, error };
|
|
}
|
|
|
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
console.error(
|
|
`[ModuleError] ${this.props.moduleId}:`,
|
|
error,
|
|
errorInfo.componentStack
|
|
);
|
|
// Future: send to error reporting service
|
|
}
|
|
|
|
handleRetry = () => {
|
|
this.setState({ hasError: false, error: null });
|
|
};
|
|
|
|
render() {
|
|
if (this.state.hasError) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
<h2 className="text-lg font-semibold text-destructive">
|
|
Eroare in modulul “{this.props.moduleName}”
|
|
</h2>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
A aparut o eroare neasteptata. Incercati din nou sau contactati echipa tehnica.
|
|
</p>
|
|
<button
|
|
onClick={this.handleRetry}
|
|
className="mt-4 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground"
|
|
>
|
|
Reincearca
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return this.props.children;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## How to Add a New Module
|
|
|
|
Step-by-step checklist for adding a module to ArchiTools. For full code examples, see [MODULE-DEVELOPMENT.md](/docs/guides/MODULE-DEVELOPMENT.md).
|
|
|
|
### 1. Create the directory structure
|
|
|
|
```bash
|
|
mkdir -p src/modules/[module-name]/{components,hooks,services}
|
|
touch src/modules/[module-name]/{types.ts,config.ts,index.ts}
|
|
```
|
|
|
|
### 2. Write `config.ts`
|
|
|
|
Export a `ModuleConfig` with a unique `id`, `route`, `featureFlag`, and `storageNamespace`.
|
|
|
|
### 3. Implement the module
|
|
|
|
Build types, services, hooks, and components inside the module directory.
|
|
|
|
### 4. Create `index.ts` barrel export
|
|
|
|
Export `config` and the default root component.
|
|
|
|
### 5. Register in `src/config/modules.ts`
|
|
|
|
Import the config and add it to the `moduleConfigs` array.
|
|
|
|
### 6. Register the lazy component in `src/lib/module-loader.ts`
|
|
|
|
Add a `React.lazy(() => import(...))` entry keyed by the module ID.
|
|
|
|
### 7. Add a feature flag in `src/config/flags.ts`
|
|
|
|
Create a flag with the key matching the module's `featureFlag` field.
|
|
|
|
### 8. Create the route
|
|
|
|
Add `src/app/(modules)/[module-name]/page.tsx` that renders the `ModuleLoader`.
|
|
|
|
### 9. Test
|
|
|
|
- Verify the module appears in the sidebar when the flag is enabled.
|
|
- Verify the module is completely absent (no network requests for its chunk) when the flag is disabled.
|
|
- Verify the error boundary catches a thrown error gracefully.
|
|
- Verify storage operations use the correct namespace prefix.
|
|
|
|
---
|
|
|
|
## Appendix: Module Category Descriptions
|
|
|
|
| Category | Key | Intended Use |
|
|
|---|---|---|
|
|
| Operațiuni | `operations` | Day-to-day operational tools (registratura, password vault) |
|
|
| Generatoare | `generators` | Document/artifact generators (email signature, Word XML, Word templates) |
|
|
| Management | `management` | Registries and asset management (IT inventory, address book, digital signatures) |
|
|
| Instrumente | `tools` | Utility tools (mini utilities, tag manager) |
|
|
| AI & Automatizări | `ai` | AI-powered features (prompt generator, AI chat) |
|