Initial commit: ArchiTools modular dashboard platform

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>
This commit is contained in:
Marius Tarau
2026-02-17 12:50:25 +02:00
commit 4c46e8bcdd
189 changed files with 33780 additions and 0 deletions

606
docs/DATA-MODEL.md Normal file
View File

@@ -0,0 +1,606 @@
# Data Model Reference
> Canonical type definitions and data modeling rules for ArchiTools.
---
## Overview
This document defines the shared data types, entity conventions, and relationship patterns used across all ArchiTools modules. Module-specific types extend these base types. All types live in TypeScript; the runtime representation is JSON stored through the storage abstraction layer.
---
## Base Entity
Every storable entity in the system extends `BaseEntity`:
```typescript
// src/types/base.ts
interface BaseEntity {
/** Unique identifier. Generated via crypto.randomUUID() or nanoid. */
id: string;
/** ISO 8601 timestamp. Set once at creation, never modified. */
createdAt: string;
/** ISO 8601 timestamp. Updated on every mutation. */
updatedAt: string;
/** User ID of the creator. Omitted until auth is implemented. */
createdBy?: string;
/** Controls who can see this entity. */
visibility: Visibility;
/** Cross-module tags attached to this entity. Array of tag IDs. */
tags: string[];
/** Which company owns this entity. 'group' for shared/cross-company items. */
company: CompanyId;
}
```
### Rules
1. `id` is always a string. Use `crypto.randomUUID()` for generation. Never use sequential integers (they create conflicts across clients and are not merge-safe).
2. `createdAt` is set once during creation and must never change.
3. `updatedAt` is set on every save operation, including the initial creation.
4. `createdBy` is `undefined` until authentication is implemented. When auth ships, it becomes a required field on new entities. Existing entities with `undefined` are treated as created by the system.
5. `tags` stores tag IDs (not labels). Tags are resolved through the tag service.
6. `company` defaults to `'group'` for entities that are not company-specific.
---
## Company Model
```typescript
// src/types/company.ts
type CompanyId = 'beletage' | 'urban-switch' | 'studii-de-teren' | 'group';
interface Company {
id: CompanyId;
name: string; // Full legal name: "Beletage SRL"
shortName: string; // Display name: "Beletage"
cui: string; // Romanian CUI (tax ID)
address?: string; // Registered address
email?: string; // Primary contact email
phone?: string; // Primary phone number
}
```
### Company Registry
Defined as static configuration in `src/config/companies.ts`:
```typescript
export const COMPANIES: Record<CompanyId, Company> = {
beletage: {
id: 'beletage',
name: 'Beletage SRL',
shortName: 'Beletage',
cui: '...', // populated in config
},
'urban-switch': {
id: 'urban-switch',
name: 'Urban Switch SRL',
shortName: 'Urban Switch',
cui: '...',
},
'studii-de-teren': {
id: 'studii-de-teren',
name: 'Studii de Teren SRL',
shortName: 'Studii de Teren',
cui: '...',
},
group: {
id: 'group',
name: 'Grup Beletage',
shortName: 'Grup',
cui: '',
},
};
```
`'group'` is a virtual company representing the entire organization. Used for shared resources (templates, tags, prompts) that are not company-specific.
---
## Visibility and Roles
### Visibility
```typescript
// src/types/visibility.ts
type Visibility = 'all' | 'internal' | 'admin' | 'guest';
```
| Value | Who can see | Use case |
|---|---|---|
| `all` | Everyone including future external/guest users | Public templates, shared contacts |
| `internal` | All authenticated internal users | Most operational data |
| `admin` | Admin users only | Passwords, sensitive config |
| `guest` | Guest users and above | Curated external-facing views |
Until authentication is implemented, all data is treated as `internal`. The visibility field is stored but not enforced at the storage layer. Enforcement will be added at the API/middleware layer when auth ships.
### Roles
```typescript
// src/types/roles.ts
type Role = 'admin' | 'user' | 'viewer' | 'guest';
```
| Role | Permissions |
|---|---|
| `admin` | Full CRUD on all entities. Access to all modules. System configuration. |
| `user` | CRUD on own entities. Access to enabled modules. |
| `viewer` | Read-only access to entities matching their visibility level. |
| `guest` | Read-only access to `guest` and `all` visibility entities. Limited module access. |
### Future Auth Integration
When Authentik SSO is integrated:
```typescript
interface UserProfile {
id: string;
email: string;
name: string;
role: Role;
companies: CompanyId[]; // Companies this user belongs to
modules: string[]; // Explicitly allowed modules (empty = all for role)
}
```
Users may belong to multiple companies. Entity filtering will combine role, visibility, and company membership.
---
## Tag Model
Tags are a cross-module resource managed by the Tag Manager module but consumed everywhere.
```typescript
// src/types/tag.ts
interface Tag {
/** Unique tag ID. */
id: string;
/** Human-readable label. Romanian. Example: "076 Casa Copernicus" */
label: string;
/**
* Tag category for grouping.
* Examples: 'project', 'client', 'phase', 'type', 'priority', 'domain'
*/
category: TagCategory;
/** Display color. Hex string: "#3B82F6" */
color: string;
/**
* If set, this tag is only visible/usable within the specified modules.
* If undefined or empty, the tag is available globally.
*/
moduleScope?: string[];
/** Company this tag belongs to. 'group' for cross-company tags. */
company: CompanyId;
/** Whether this tag is active. Archived tags are hidden from selectors but preserved on entities. */
archived: boolean;
createdAt: string;
updatedAt: string;
}
type TagCategory =
| 'project'
| 'client'
| 'phase'
| 'type'
| 'priority'
| 'domain'
| 'custom';
```
### Project Reference Pattern
Tags with category `'project'` follow the ManicTime naming convention used in the office:
```
"{number} {project name}"
```
Examples:
- `"076 Casa Copernicus"`
- `"081 PUZ Sector 3"`
- `"092 Reabilitare Scoala Nr 5"`
The number is a sequential project identifier scoped per company. The tag label is the canonical reference; there is no separate `projectNumber` field.
**Parsing utility:**
```typescript
// src/lib/tags/utils.ts
interface ParsedProjectTag {
number: string; // "076"
name: string; // "Casa Copernicus"
raw: string; // "076 Casa Copernicus"
}
function parseProjectTag(label: string): ParsedProjectTag | null {
const match = label.match(/^(\d{3})\s+(.+)$/);
if (!match) return null;
return { number: match[1], name: match[2], raw: label };
}
```
### Tag Relationships
Tags create implicit relationships between entities across modules. For example:
- A registratura entry tagged `"076 Casa Copernicus"` is linked to the same project as a word template with the same tag.
- Searching by tag ID across all namespaces yields every entity related to that project.
This is the primary cross-module linking mechanism until a dedicated relational backend is introduced.
---
## Module-Specific Entity Types
Each module defines its own entity types that extend `BaseEntity`. Below is an overview of the primary entity per module.
### Registratura
```typescript
// src/modules/registratura/types.ts
interface RegistryEntry extends BaseEntity {
/** Sequential registry number per company per year. "B-2025-0042" */
registryNumber: string;
/** Entry or exit. */
direction: 'incoming' | 'outgoing';
/** Date the document was registered. ISO 8601 date (not datetime). */
registryDate: string;
/** Document title / description. */
title: string;
/** Sender (for incoming) or recipient (for outgoing). */
correspondent: string;
/** Number of physical pages, if applicable. */
pageCount?: number;
/** Reference to another registry entry (e.g., reply to incoming). */
referenceTo?: string;
/** Free-text notes. */
notes?: string;
/** Attached file references (future: MinIO object keys). */
attachments: string[];
}
```
### IT Inventory
```typescript
// src/modules/it-inventory/types.ts
type DeviceType = 'laptop' | 'desktop' | 'monitor' | 'printer' | 'router' |
'switch' | 'nas' | 'phone' | 'tablet' | 'peripheral' | 'other';
type DeviceStatus = 'active' | 'storage' | 'repair' | 'retired' | 'disposed';
interface InventoryItem extends BaseEntity {
name: string;
deviceType: DeviceType;
manufacturer?: string;
model?: string;
serialNumber?: string;
purchaseDate?: string;
warrantyExpiry?: string;
assignedTo?: string;
location?: string;
status: DeviceStatus;
specs?: Record<string, string>; // CPU, RAM, storage, etc.
notes?: string;
}
```
### Address Book
```typescript
// src/modules/address-book/types.ts
type ContactType = 'person' | 'company' | 'institution' | 'supplier';
interface AddressContact extends BaseEntity {
contactType: ContactType;
name: string;
organization?: string;
role?: string;
email?: string;
phone?: string;
address?: string;
city?: string;
county?: string;
country?: string;
cui?: string; // For companies/institutions
iban?: string;
bank?: string;
website?: string;
notes?: string;
favorite: boolean;
}
```
### Prompt Generator
```typescript
// src/modules/prompt-generator/types.ts
type PromptDomain = 'architecture' | 'legal' | 'technical' | 'gis' |
'rendering' | 'urbanism' | 'bim' | 'procurement' | 'administrative';
type PromptTargetAI = 'text' | 'image' | 'code' | 'review' | 'rewrite';
interface PromptTemplate extends BaseEntity {
name: string;
category: string;
domain: PromptDomain;
description: string;
targetAI: PromptTargetAI;
blocks: PromptBlock[];
variables: PromptVariable[];
outputMode: OutputMode;
providerProfile?: string;
version: number;
parentTemplateId?: string; // For cloned/forked templates
favorite: boolean;
}
```
### Email Signature
```typescript
// src/modules/email-signature/types.ts
interface SignatureConfig extends BaseEntity {
employeeName: string;
jobTitle: string;
email: string;
phone?: string;
mobile?: string;
/** Generated HTML output. */
generatedHtml: string;
}
```
### Digital Signatures
```typescript
// src/modules/digital-signatures/types.ts
type SignatureAssetType = 'signature' | 'stamp' | 'initials';
interface SignatureAsset extends BaseEntity {
name: string;
assetType: SignatureAssetType;
/** Base64-encoded PNG for localStorage. MinIO object key for API backend. */
imageData: string;
owner?: string;
notes?: string;
}
```
### Password Vault
```typescript
// src/modules/password-vault/types.ts
interface VaultEntry extends BaseEntity {
service: string;
url?: string;
username: string;
/** Stored as plaintext in localStorage (demo-grade security). Encrypted in API backend. */
password: string;
notes?: string;
category?: string;
lastRotated?: string;
}
```
> **Security note:** The localStorage adapter stores passwords in plaintext. This is acceptable only for internal demo use. The API backend must encrypt the `password` field at rest.
### Word Templates / Word XML Generators
```typescript
// src/modules/word-templates/types.ts
type TemplateCategory = 'contract' | 'offer' | 'report' | 'letter' |
'header' | 'certificate' | 'minutes' | 'other';
interface WordTemplate extends BaseEntity {
name: string;
category: TemplateCategory;
description?: string;
/** File reference: base64 for localStorage, MinIO key for API backend. */
fileData: string;
fileName: string;
fileSize: number;
version: number;
}
```
---
## Naming Conventions
### Type Names
| Convention | Example | Used For |
|---|---|---|
| PascalCase | `RegistryEntry` | Interfaces, type aliases, classes |
| camelCase | `registryNumber` | Properties, variables, functions |
| UPPER_SNAKE_CASE | `COMPANIES` | Constants, static config objects |
| kebab-case | `'urban-switch'` | String literal union members (IDs, slugs) |
### Entity Type Naming
- Main entity per module: descriptive noun (`RegistryEntry`, `InventoryItem`, `AddressContact`).
- Do not prefix with module name unless ambiguity exists.
- Supporting types (enums, sub-objects): use descriptive names scoped by context (`DeviceType`, `ContactType`).
- Union string literal types: use the values themselves, not a wrapper. Prefer `type Direction = 'incoming' | 'outgoing'` over an enum.
### File Naming
- Type definition files: `types.ts` in the module root.
- Shared types: `src/types/{name}.ts`.
- Never put types in a barrel `index.ts` that also exports components (causes circular dependency issues with lazy loading).
---
## Schema Versioning
### Strategy
Each module's data schema has a version number tracked in the namespace metadata:
```typescript
// Stored at key "_meta" within each namespace
interface NamespaceMeta {
schemaVersion: number;
lastMigration: string;
itemCount: number;
}
```
### Migration Registration
```typescript
// src/modules/registratura/migrations.ts
import { defineMigrations } from '@/lib/storage/migrations';
export const registraturaMigrations = defineMigrations('registratura', [
{
version: 2,
description: 'Add attachments array to registry entries',
up: (data: Record<string, unknown>) => {
for (const [key, value] of Object.entries(data)) {
if (key === '_meta') continue;
const entry = value as Record<string, unknown>;
if (!entry.attachments) {
entry.attachments = [];
}
}
return data;
},
},
{
version: 3,
description: 'Rename sender field to correspondent',
up: (data: Record<string, unknown>) => {
for (const [key, value] of Object.entries(data)) {
if (key === '_meta') continue;
const entry = value as Record<string, unknown>;
if ('sender' in entry) {
entry.correspondent = entry.sender;
delete entry.sender;
}
}
return data;
},
},
]);
```
### Migration Execution
On module initialization, the storage layer:
1. Reads `_meta.schemaVersion` for the module's namespace.
2. Compares to the latest registered migration version.
3. If behind, runs each migration sequentially (`version N` to `version N+1`).
4. Updates `_meta.schemaVersion` after each successful migration.
5. If any migration fails, the process halts and logs an error. The module should display a degraded state rather than corrupting data.
---
## Entity Relationships
ArchiTools uses a flat document model (not relational). Relationships are expressed through:
### 1. Tags (Primary Cross-Module Link)
Entities share project tags. Querying all namespaces for a tag ID returns every entity related to that project.
```
RegistryEntry.tags: ["tag-076-casa-copernicus"]
WordTemplate.tags: ["tag-076-casa-copernicus"]
PromptTemplate.tags: ["tag-076-casa-copernicus"]
```
### 2. Direct References
Some entities reference others by ID within the same namespace:
- `RegistryEntry.referenceTo` points to another registry entry ID.
- `PromptTemplate.parentTemplateId` points to the template it was cloned from.
### 3. Company Scoping
The `company` field on every entity enables filtering by company. A user at Beletage sees Beletage and group entities; never Urban Switch entities (unless their role grants cross-company access).
### 4. Future: Explicit Project Entity
When the platform matures, a `Project` entity may be introduced:
```typescript
interface Project extends BaseEntity {
number: string; // "076"
name: string; // "Casa Copernicus"
company: CompanyId;
status: 'active' | 'completed' | 'archived';
client?: string; // AddressContact ID
startDate?: string;
endDate?: string;
}
```
At that point, tags with category `'project'` would reference a `Project.id`, and the project number/name would be the single source of truth. Until then, the tag label is the canonical project identifier.
---
## Shared Type Exports
All shared types are re-exported from `src/types/index.ts`:
```typescript
// src/types/index.ts
export type { BaseEntity } from './base';
export type { CompanyId, Company } from './company';
export type { Visibility } from './visibility';
export type { Role, UserProfile } from './roles';
export type { Tag, TagCategory } from './tag';
```
Module-specific types are not exported from this barrel. They are imported directly from the module:
```typescript
import type { RegistryEntry } from '@/modules/registratura/types';
```
This keeps the shared type surface minimal and avoids pulling module code into unrelated bundles.