Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
+24
View File
@@ -0,0 +1,24 @@
# CRUX Documentation
Documentation home for the CRUX monorepo. See also [Architecture](./architecture/Readme.md), [Packages](./packages/Readme.md). App docs live with each app: [admin UI style guide](../apps/admin/ui-style-guide.md), [web](../apps/web/Readme.md).
## Core Modules
| Module | Backend | Frontend | Documentation |
| ---------------- | -------------------------------------------- | -------------------------------------- | --------------------------------------------- |
| Access Control | Admin, Role, Permission base modules | `apps/admin/src/core/AccessControl/` | [Overview](./core/access-control/Readme.md) |
| Account Settings | Admin profile endpoints | `apps/admin/src/core/AccountSettings/` | [Overview](./core/account-settings/Readme.md) |
| Auth | `apps/backend/app/base_modules/Auth/` | `apps/admin/src/core/Auth/` | [Overview](./core/auth/Readme.md) |
| Devtools | Dev-only | `apps/admin/src/devtools/` | [Overview](./core/devtools/Readme.md) |
| Fallback Screens | Frontend-only | `apps/admin/src/core/FallbackScreens/` | [Overview](./core/fallback-screens/Readme.md) |
| File Manager | `apps/backend/app/base_modules/FileManager/` | `apps/admin/src/core/FileManager/` | [Overview](./core/file-manager/Readme.md) |
| Permission | Permission service and policies | `apps/admin/src/core/Permission/` | [Overview](./core/permission/Readme.md) |
| Web Manager | `apps/backend/app/base_modules/WebManager/` | `apps/admin/src/core/WebStudio/` | [Overview](./core/web-manager/Readme.md) |
## Project Modules
Project-specific modules: [docs/modules/](./modules/Readme.md).
## Shared Packages
Package docs live with the package source; see the [Packages index](./packages/Readme.md).
+45
View File
@@ -0,0 +1,45 @@
# CRUX Architecture
System-level docs and stack-scoped convention guides. Built-in feature docs live in `docs/core/`, project-module docs in `docs/modules/`, single-app docs with the app (`apps/<app>/Readme.md`).
## Stack Guides
| Guide | Applies to |
| --------------------------------------------------------- | ------------------------------------------------- |
| [React frontend architecture](./react-frontend/Readme.md) | Frontend apps and frontend modules under `apps/*` |
| [Backend architecture](./backend/Readme.md) | AdonisJS backend apps and backend modules |
| [Next.js web architecture](./web/Readme.md) | The public website (`apps/web/`) |
## System Overview
| App | Source | Stack | Role |
| -------------- | --------------- | ------------ | ---------------------------------------------------- |
| Backend API | `apps/backend/` | AdonisJS v6 | REST API, business logic, database, authorization |
| Admin frontend | `apps/admin/` | React (Vite) | Admin panel: core modules, devtools, project modules |
| Public website | `apps/web/` | Next.js | Server-rendered public site fed by the public API |
```txt
┌──────────────────┐ authenticated REST ┌──────────────────────┐
│ Admin (React) │────────────────────────►│ Backend (AdonisJS) │
│ apps/admin │◄────────────────────────│ apps/backend │
└──────────────────┘ DTOs └──────────┬───────────┘
│
public REST + │ cache purge
revalidation hooks │ webhooks
▼
┌──────────────────────────┐
│ Public site (Next.js) │
│ apps/web │
└──────────────────────────┘
```
Apps share contracts, never runtime code:
- **Admin ↔ Backend** — authenticated REST, authorized per action via policies and permission slugs. See [Auth](../core/auth/Readme.md), [Access Control](../core/access-control/Readme.md), [Permission](../core/permission/Readme.md).
- **Web ↔ Backend** — public REST plus cache revalidation webhooks. See [Web Manager integration](../core/web-manager/integration.md).
Feature-scoped contracts (DTOs, cache tags, env vars) live next to the feature in `docs/core/<feature>/integration.md`.
## Shared Packages
`packages/` holds `@crux-kit/*` packages shared across apps. Each documents itself in its own `Readme.md`; see the [Packages index](../packages/Readme.md).
+37
View File
@@ -0,0 +1,37 @@
# Backend Architecture
The current backend is based on AdonisJS v6. Use the official AdonisJS folder structure as the default baseline:
https://v6-docs.adonisjs.com/guides/getting-started/folder-structure
## Default Folders
| Folder | Purpose |
| ----------- | -------------------------------------------------------------------------------------------- |
| `app/` | Application code: controllers, services, middleware, policies, validators, and domain logic. |
| `start/` | Boot lifecycle files such as routes, kernel setup, env loading, and event registration. |
| `config/` | Runtime configuration for AdonisJS and installed packages. |
| `database/` | Migrations and seeders. |
| `bin/` | Entrypoints for server, console, and tests. |
| `public/` | Static files and generated public assets. |
| `tests/` | Backend tests. |
## Basic Flow
```txt
start/routes.ts
-> controller
-> validator / policy
-> service
-> model / database / external service
```
Controllers should handle HTTP concerns. Services should hold business logic. Validators should own request validation. Policies and abilities should own authorization.
## Rules
- Follow AdonisJS conventions first.
- Keep HTTP logic out of services.
- Keep business logic out of controllers.
- Validate request input before calling services.
- Use policies or abilities for protected operations.
@@ -0,0 +1,82 @@
# CRUX React Admin Panel Architecture
## Core Principles
1. **Blocks are composable and isolated** — each block (component, form, widget) has a single responsibility; higher-level blocks (widgets, pages) compose lower-level ones.
2. **Data vs. UI separation** — `data` handles API, state, and business data; `view` handles UI and interaction. No API logic in UI components — UI calls hooks from `data`.
3. **Entities are global types** — placed in `entities/`, usable by any block.
4. **Shared** — `shared/` = globally reusable components, hooks, utilities.
5. **Infrastructure** — `infrastructure/` = app-wide technical adapters: HTTP clients, React Query, storage adapters, formatting service wrappers.
6. **Reference data** — `refdata/` = app-wide API-backed lookup datasets shared by multiple modules.
## Recommended Layering
```plaintext
src/
├── app/ # composition root: providers, routing, config, i18n, layouts
├── infrastructure/ # technical adapters: HTTP, query, storage, error handling
├── refdata/ # API-backed common reference datasets
├── shared/ # reusable app-wide UI, hooks, utilities, and widgets
├── core/ # built-in product modules
├── modules/ # project-specific modules
└── devtools/ # development-only modules and tools
```
## Architecture Scope
The layer structure can be used at three levels:
| Scope | Example | Meaning |
| --------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| App-level architecture | `apps/admin/src/app`, `apps/admin/src/shared` | Global app shell, providers, routing, layout, infrastructure, reusable app-wide building blocks. |
| Core module architecture | `apps/admin/src/core/FileManager` | Built-in product modules shipped with the admin template. Each large core module can own its own `data`, `entities`, `pages`, `view`, and `shared`, plus feature-scoped context when needed. |
| Project module architecture | `apps/admin/src/modules/*` | Generated or project-specific modules. Each module should follow the same layer rules inside its own folder when it becomes more than a simple page. |
For small modules, do not create empty layers only to match the template. Add a layer when the module has real ownership for that layer.
## Full documentation
- [Entities Layer](./layers/entities.md)
- [Data Layer](./layers/data.md)
- [App Layer](./layers/app.md)
- [Infrastructure Layer](./layers/infrastructure.md)
- [Reference Data Layer](./layers/refdata.md)
- [View Layer](./layers/view.md)
- [Shared Layer](./layers/shared.md)
- [File & Folder Naming Convention](./naming.md)
## Disclaimer
This architecture guide is intended as a **recommendation**, not a strict rulebook. **Feel free to adapt and modify it based on your team’s needs and project context**.
Key points to keep in mind:
1. **Maintainability comes first**.
The main goal of this structure is to help someone new to the project quickly understand where to make changes.
It’s about finding the right place to touch the code now, rather than strictly following a pattern for its own sake.
**Ultimately, the main goal is to make development easier for everyone**
2. **Naming debates are inevitable**.
Any architecture guide may spark discussions about file, folder, or function naming. Use this guide as a reference, but remember that practicality and clarity are more important than strict adherence.
3. **Flexibility is allowed**.
- You can adjust layers, module structure, or naming conventions if it improves readability or reduces complexity.
- New patterns or exceptions can be added as the project evolves.
In short: this guide exists to make the project easier to understand and maintain, not to create unnecessary friction. Use judgment and prioritize clarity over rigid consistency.
---
Цей посібник з архітектури є **рекомендацією**, а не суворим зводом правил. **Не соромтеся адаптувати та змінювати його відповідно до потреб вашої команди та контексту проєкту**.
Ключові моменти, які варто мати на увазі:
1. **Насамперед — зручність підтримки**.
Головна мета цієї структури — допомогти новому учаснику проєкту швидко зрозуміти, де вносити зміни.
Йдеться про те, щоб знайти правильне місце в коді просто зараз, а не сліпо дотримуватися патерну заради самого патерну.
**Зрештою, головна мета — зробити розробку простішою для всіх.**
2. **Дискусії щодо назв неминучі**.
Будь-який посібник з архітектури може спровокувати суперечки щодо назв файлів, папок або функцій. Використовуйте цей посібник як орієнтир, але пам'ятайте, що практичність і зрозумілість важливіші за суворе дотримання правил.
3. **Гнучкість допускається**.
- Ви можете коригувати шари, структуру модулів або угоди щодо найменування, якщо це покращує читабельність або зменшує складність.
- У міру розвитку проєкту можна додавати нові патерни або винятки.
Коротко: цей посібник існує для того, щоб проєкт був зрозумілішим і простішим у підтримці, а не для того, щоб створювати зайві труднощі. Керуйтеся здоровим глуздом і надавайте перевагу ясності, а не жорсткій послідовності.
@@ -0,0 +1,33 @@
# App Layer
The composition root: owns concerns that exist once for the whole app and coordinates layers and modules without containing feature business logic. It composes global providers, configures app-wide settings and environment values, initializes i18n and global styles, defines top-level routing, provides layouts and menus, and registers core/project/development modules.
```plaintext
app/
├── config/ # global settings, environment values, and config types
├── css/ # global styles and app-level third-party overrides
├── i18n/ # languages, messages, and internationalization config
├── layouts/ # top-level Admin layout variants
├── providers/ # providers composed once for the entire application
│ ├── AppProviders.tsx
│ ├── I18nProvider.tsx
│ ├── QueryProvider.tsx
│ ├── SettingsProvider.tsx
│ └── ThemeProvider.tsx
├── routing/
│ ├── AppRouting.tsx
│ └── AppRoutingSetup.tsx
├── services/
│ └── modules/ # module registry, routes, menus, and messages
└── menu.tsx # app-level menu composition
```
Files may evolve, but ownership stays app-wide — a concern used by only one feature belongs to that feature module.
## Notes
- **Providers** — `app/providers/AppProviders.tsx` is the single composition point: TanStack Query, auth context (from the Auth core module), settings, tooltip/notification UI, theme, i18n and text direction, React Router.
- **Routing** — `app/routing/` composes top-level routes from routes contributed by registered modules; feature pages stay in their modules.
- **Config & i18n** — centralize environment access through `app/config/` instead of repeating it in feature code; `app/i18n/` owns languages and messages.
- **Layouts & CSS** — `app/layouts/` holds the Admin shell variants, `app/css/` global and third-party overrides; feature styles stay with the feature.
- **Module registration** — `app/services/modules/` wires modules in through their routes, menus, and messages; module implementation stays under `core/`, `modules/`, or `devtools/`.
@@ -0,0 +1,149 @@
# Data Layer
The `data` layer owns all data access and transformation: API calls, DTO types, DTO → entity mappers, React Query hooks, caching and errors. Higher layers (`forms`, `widgets`, `pages`) never call APIs directly — they consume data-layer hooks.
## Organization
Two ways to structure the layer:
| Approach | Layout | Best for | Trade-off |
| --------- | ------------------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------- |
| By Module | `data/api/`, `data/hooks/`, `data/dto/`, `data/mappers/` | Small apps, simple data layers | One feature's files scattered; hard to scale |
| By Entity | `data/user/`, `data/product/` — each with `api.ts`, `hooks.ts`, `dto.ts`, `mapper.ts`, `types.ts` | Medium/large apps | Harder to see all APIs/hooks globally |
```plaintext
data/
| --- user/
| | --- api.ts # user-related API functions (getUsers, createUser, etc.)
| | --- hooks.ts # React Query hooks (useUsersQuery, useCreateUserMutation)
| | --- dto.ts # API DTO types for user
| | --- mapper.ts # map user DTO → entity
| | --- types.ts # API types/interfaces (optional)
```
## Data Flow
### 1. DTO Types
```ts
export type UserDTO = {
id: number;
full_name: string;
email_address: string;
};
export type CreateUserDTO = {
full_name: string;
email_address: string;
};
```
### 2. API Layer
API functions or class methods perform HTTP requests and always return DTOs.
```ts
export class UserApi {
private http: HttpClient;
async getDetailed(id: number): Promise<UserDTO> {
return this.http.get(`/users/${id}`);
}
async getList(): Promise<UserDTO[]> {
return this.http.get(`/users`);
}
async create(dto: CreateUserDTO): Promise<UserDTO> {
return this.http.post(`/users`, dto);
}
}
```
### 3. Mapper
Mappers convert `DTO` to `Entity`.
```ts
import { User } from "@/entities/user";
export class UserMapper {
toEntity(dto: UserDTO): User {
return {
id: dto.id,
name: dto.full_name,
email: dto.email_address,
};
}
toEntities(dtos: UserDTO[]): User[] {
return dtos.map((dto) => this.toEntity(dto));
}
toPaginated(dto: PaginatedDataDTO<UserDTO[]>): PaginatedResult<User[]> => {
return {
meta: this.paginationMetaMapper.toMeta(dto.meta),
data: this.toEntities(dto.data),
};
};
}
```
### Mapper Testing
Mappers are part of the data layer contract and need unit tests whenever they rename fields, normalize values (paths, dates, statuses, enums, defaults), map nested DTOs, arrays, paginated data, or optional fields. Cover: `toEntity` on a representative DTO, `toEntities` on arrays, defaults for missing fields, nested mapper shapes, enum/status casts. Test only transformation — no React, no rendering, no real API calls.
Keep tests at the app root, mirroring the source path:
```plaintext
apps/admin/
├── src/core/FileManager/data/mappers/entryMapper.ts
└── tests/unit/core/FileManager/data/mappers/entryMapper.test.ts
```
### 4. Query Hooks
Hooks combine API + mapper + React Query.
```ts
export const useUserDetailed = (id: number) => {
const api = new UserApi(http);
const mapper = new UserMapper();
return useQuery({
queryKey: ["user", id],
queryFn: async () => {
const dto = await api.getDetailed(id);
return mapper.toEntity(dto);
},
});
};
```
### Pagination
Pagination is not entity-specific — keep it in a shared data module.
```ts
export type PaginatedMetaDTO = {
total: number;
perPage: number;
currentPage: number;
lastPage: number;
};
export type PaginatedDTO<T> = {
data: T;
meta: PaginatedMetaDTO;
};
export class PaginationMapper {
toMeta(dto: PaginatedMetaDTO) {
return {
total: dto.total,
perPage: dto.perPage,
currentPage: dto.currentPage,
lastPage: dto.lastPage,
};
}
}
```
@@ -0,0 +1,25 @@
# Entities
Core domain models of the application — pure type definitions for business objects (`User`, `Product`, `Message`), their enums, and related constants. Stable, reusable, usable from any layer.
```plaintext
entities/
| --- user.ts # { id: string; name: string; email: string }
| --- product.ts # { id: string; name: string; price: number }
| --- enums.ts # { UserRole, ProductCategory }
```
Larger entities can use a folder (`user/` with `interfaces.ts`, `enums.ts`, `rules.ts`, `index.ts`).
```ts
export type User = {
id: number;
name: string;
email: string;
};
export enum UserRole {
ADMIN = "admin",
SIMLPE_JACK = "simple_jack",
}
```
@@ -0,0 +1,51 @@
# Infrastructure Layer
App-wide technical adapters connecting the app to technical systems and shared runtime services: HTTP clients and interceptors, shared React Query helpers (keys, response types, error normalization), storage/browser adapters, formatting service adapters, technical config wrappers. No feature business logic.
```plaintext
infrastructure/
├── axios/
│ ├── index.ts # configured axios client
│ ├── interceptors.ts # request/response interceptors
│ └── axios.d.ts # axios type augmentation
│
├── reactQuery/
│ ├── index.ts
│ ├── errors.ts # query error handling helpers
│ ├── helpers.ts # query key and pagination helpers
│ ├── queryParams.ts # table/query-param mapping helpers
│ └── types.ts # shared API response/query types
│
├── IntlDate.ts # date formatting service adapter
└── IntlName.ts # name formatting service adapter
```
## Rules
1. Feature-agnostic: no module business rules, no page/component composition, no entity mapping that belongs in a module `data/mapper`.
2. API modules use infrastructure clients (`import { axiosClient } from "#infrastructure/axios"`) instead of creating their own.
3. React Query helpers shared across modules (key builders, list response types, pagination, error extraction) live here.
4. UI never calls raw HTTP clients — view/page code uses data-layer hooks; direct `#infrastructure/axios` usage belongs in API/data modules.
5. Generic formatting adapters (`IntlDate`, `IntlName`) may be consumed by UI.
6. Keep app-state coupling narrow — prefer dependency injection; don't import feature modules into infrastructure.
## Examples
```ts
import { axiosClient } from "#infrastructure/axios";
export class UserAPI {
getDetailed(id: number) {
return axiosClient.get(`/users/${id}`).then((response) => response.data);
}
}
```
```ts
import {
createKeyBuilder,
getPaginationInfo,
} from "#infrastructure/reactQuery";
export const usersCacheKey = createKeyBuilder("USERS", "LIST");
```
@@ -0,0 +1,55 @@
# Reference Data Layer
App-wide API-backed lookup datasets reused across modules — countries, languages, currencies, time zones, common option dictionaries. The layer fetches them, exposes reusable query hooks, and owns their cache keys.
Small dataset → one file; larger → a folder:
```plaintext
refdata/
├── countries.ts
├── languages.ts
└── currencies/
├── api.ts
├── dto.ts
├── mapper.ts
├── queries.ts
└── keys.ts
```
## Rules
1. App-wide datasets only — a dataset owned by one feature belongs in that module's `data/` layer.
2. May use `#infrastructure/axios` and `#infrastructure/reactQuery`; must not import from `view/`, `pages/`, or feature modules.
3. Expose hooks or small service functions — consumers should not know endpoints or cache keys.
4. Add mappers when backend DTOs differ from app-facing data, tested like module data mappers.
## Example
```ts
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { axiosClient } from "#infrastructure/axios";
import { createKeyBuilder } from "#infrastructure/reactQuery";
export const countriesCacheKey = createKeyBuilder(
"REF_DATA",
"COUNTRIES",
"LIST",
);
interface CountryData {
code: string;
name: string;
}
export const useCountries = () => {
return useQuery({
queryKey: countriesCacheKey.store(),
placeholderData: keepPreviousData,
queryFn: async () => {
return axiosClient
.get<Array<CountryData>>("/datasets/countries")
.then((data) => data.data);
},
});
};
```
@@ -0,0 +1,56 @@
# Shared Layer
Global reusable building blocks used across the entire application — framework-level, never feature-specific.
```plaintext
shared/
| --- ui/ # primitive UI components (Button, Modal, Card)
| --- widgets/ # reusable composed UI blocks (DataGrid, screens)
| --- forms/ # reusable form composition (composed inputs)
| --- editors/ # reusable editor wrappers (tinymce, image_input)
| --- hooks/ # generic reusable hooks (useDebounce, useToggle, useLocalStorage)
| --- utils/ # pure helper functions (formatDate, generateId, sleep)
| --- lib/ # pure helpers and utility services (time/, phone_number/, resolveFileURL)
| --- assets/ # images, icons, fonts
| --- styles/ # global CSS, variables, Tailwind overrides
| --- constants/ # global constants
```
## UI Components
Simple, composable, customizable building blocks — Lego pieces that combine into larger UI:
- **Simple** — Button, Input, Card, Modal, Dropdown.
- **Composable** — complex UI is built by composition, not by modifying base components:
```tsx
<Card>
<CardHeader />
<CardContent />
<CardFooter />
</Card>
```
- **Customizable** — through `className`, variants, props, and composition:
```tsx
<Button variant="outline" size="lg">
Save
</Button>
```
- **No business logic** — presentation only (local state allowed): no API calls, React Query, form submission, or domain logic.
### Styling
Utility-first, design tokens, variants instead of style duplication, minimal centralized custom CSS.
Stack: **TailwindCSS** (primary styling), **class-variance-authority** (variants), **CSS variables** (design tokens), **Radix primitives** (accessible building blocks).
## Other Folders
- `hooks/` — generic hooks with no entity or business-logic dependencies.
- `utils/` — stateless pure helper functions.
- `lib/` — pure helpers and utility services (date, name, path, phone formatting).
- `assets/` — static resources (icons, fonts).
- `styles/` — CSS variables, Tailwind extensions, global resets.
@@ -0,0 +1,150 @@
# Views Layer
## Components
Reusable UI blocks: render UI, accept props, handle simple UI state (hover, toggle, open/close), compose other components. `UserAvatar`, `ProductBadge`.
No API calls, mutations, complex state management, or business workflows.
## Widgets
Self-contained UI blocks that implement a complete piece of functionality — panels, sections, complex interactive blocks used to compose pages. A widget combines UI, logic, and interactions: it composes multiple components, may include forms, dialogs, or actions, may fetch data via data hooks or take it from props, and exposes a simple API to pages while hiding its internals.
A widget can use another widget when the nested widget is a clear functional sub-block, the parent is grouping or orchestrating UI, and the dependency stays loose:
```ts
export const ProjectOverviewWidget = ({ projectId }: Props) => {
return (
<div>
<ProjectStatsWidget projectId={projectId} />
<ProjectActivityWidget projectId={projectId} />
<ProjectMembersWidget projectId={projectId} />
</div>
);
};
```
## Forms
### Form Architecture
```plaintext
view/forms/SomeForm/
| --- form.ts # validation schema, types, default values
| --- Component.tsx # UI + react-hook-form integration
| --- submit.ts # submission logic
| --- constants.ts # Optional: form IDs, UI constants
| --- schema.ts # Optional validation schema. Good for large or multistep forms
| --- index.ts # public exports
```
### Form Logic
`form.ts` contains pure form logic: validation schema, form types, default values, helper templates.
```ts
export const validationSchema = z.object({
first_name: z.string().min(3),
last_name: z.string().min(3),
});
```
With `i18n`, create a function that returns the schema:
```ts
function createValidationSchema(
translationFn: TFunction<"translation", undefined>, // Depend on library you use
) {
return z.object({
first_name: z.string({ message: translationFn("required") }),
last_name: z.string({ message: translationFn("required") }),
});
}
```
> [! WARNING]
> Dont include `i18n` singleton last_name: z.string().min(1, i18n.t('required'))
> It will not translate errors when language state change
Default values come from factory functions, not inline objects:
```ts
export function createFormValues(): FormValues {
return { first_name: "", last_name: "" };
}
export function toFormValues(data: SomeData): FormValues {
return {
first_name: data?.first_name ?? "",
last_name: data?.last_name ?? "",
};
}
```
Types for values and submit functions:
```ts
export type FormValues = z.infer<typeof validationSchema>;
export type SubmitFunc = (
values: FormValues,
handlers: UseFormReturn<FormValues>,
) => void;
```
### Submit Functions
A `useSubmit` hook centralizes submission: it picks create vs update by `values.id`, calls the `data/` layer mutations, invalidates list/detail queries on success, maps API validation errors onto form fields, and exposes `submit` + `isSubmitting`.
```ts
export const useSubmit = (params?: { onSuccess?: (id?: number) => void }) => {
const { mutate: createMutation, isPending: isCreatePending } = useCreate();
const { mutate: updateMutation, isPending: isUpdatePending } = useUpdate();
const handleSubmit: SubmitFunc = (values, handlers) => {
const mutation = values.id ? updateMutation : createMutation;
mutation(values, {
onSuccess() {
invalidateList();
if (values.id) invalidateDetailed(values.id);
params?.onSuccess?.(values.id ?? undefined);
},
onError(error) {
extractValidationErrors(error).forEach((e) => {
handlers.setError(e.field as keyof typeof values, {
type: "apiError",
message: e.message,
});
});
},
});
};
return {
submit: handleSubmit,
isSubmitting: isCreatePending || isUpdatePending,
};
};
```
### Composed Form Elements
To avoid form boilerplate, wrap React Hook Form + shared UI into composed inputs. Each combines label, input, validation message, and layout, works with `FormProvider` via `useFormContext`, and can encapsulate any input type: text, switch, select, date, textarea, etc.
```tsx
<FormProvider {...form}>
<Component.FormElement
id={MAIN_FORM_ID}
onSubmit={form.handleSubmit((values) => onSubmit(values, form))}
>
<Component.FormGrid columns={2}>
<Component.GridElement colSpan={1}>
<Component.InputSwitch
name="notificationsEnabled"
required
label="Enable notifications"
/>
</Component.GridElement>
</Component.FormGrid>
</Component.FormElement>
</FormProvider>
```
@@ -0,0 +1,62 @@
# File & Folder Naming
Standard React community conventions:
| Type | Convention | Example |
| ---------------------- | -------------------------------------------- | ------------------------------------- |
| Folders | lowercase (PascalCase for component folders) | `user/`, `shared/`, `CreateUserForm/` |
| React component files | PascalCase | `UserProfile.tsx`, `Button.tsx` |
| Hook files | camelCase (`use` prefix) | `useDebounce.ts`, `useToggle.ts` |
| Utility / helper files | camelCase | `formatDate.ts`, `generateId.ts` |
| API / service files | camelCase (acronyms uppercase) | `userAPI.ts`, `productAPI.ts` |
| DTO / mapper files | camelCase (acronyms uppercase) | `userDTO.ts`, `userMapper.ts` |
| Type / interface files | camelCase | `types.ts`, `dto.ts`, `interfaces.ts` |
| Constants files | camelCase | `constants.ts`, `formConstants.ts` |
| Config / entry files | camelCase | `index.ts`, `queryClient.ts` |
Layer, entity, and infrastructure folders are lowercase; component, form, and widget folders are PascalCase, matching the component they contain:
```plaintext
src/
data/
user/
userAPI.ts
hooks.ts # or useUserQuery.ts, useCreateUserMutation.ts
userDTO.ts
userMapper.ts
view/
components/
UserAvatar.tsx
widgets/
UserProfileWidget/
index.ts
UserProfileWidget.tsx
forms/
CreateUserForm/
form.ts
Component.tsx
submit.ts
index.ts
shared/
ui/
Button.tsx
hooks/
useDebounce.ts
utils/
formatDate.ts
```
## Case-Only Rename Problem (macOS + Git)
macOS file systems are case-insensitive by default while Git is case-sensitive internally, so a rename like `api.ts` → `Api.ts` may not be detected by Git, break CI on case-sensitive Linux, or leave the repo inconsistent.
**Required: rename in two committed steps**, for files and folders alike — never by case only in one step, never relying on IDE rename alone; verify with `git status`:
```bash
git mv userApi.ts userApi-temp.ts
git commit -m "chore: temporary rename userApi.ts to userApi-temp.ts"
git mv userApi-temp.ts UserApi.ts
git commit -m "chore: rename userApi-temp.ts to UserApi.ts (case change)"
```
Optionally make Git case-sensitive for this repo with `git config core.ignorecase false` (verify with `git config core.ignorecase`). Even then, the file system stays case-insensitive and case-only renames can still misbehave — the two-step rename remains the safest approach.
+118
View File
@@ -0,0 +1,118 @@
# Next.js Web Architecture
Architecture of the public website (`apps/web/`): a Next.js App Router app that server-renders the pages editors publish in [Web Manager](../../core/web-manager/Readme.md). App-specific setup (env vars, dev commands, tests) lives in [apps/web/Readme.md](../../../apps/web/Readme.md).
## How a Request Works
```txt
GET /en/about-us
proxy.ts (next-intl middleware) validates the locale segment
app/[locale]/[[...path]]/page.tsx
resolvePageRenderData('en', 'about-us') web-runtime/api/render-data.ts
GET {BACKEND_API_URL}/web-manager/public/pages/resolve?locale=en&path=about-us
normalizePageResolution(payload) web-runtime/data/normalize.ts
PageRenderer web-runtime/renderer.tsx
SiteHeader → RegisteredPageLayout → SiteFooter
├─ PageHero
├─ Breadcrumbs
└─ PageBlocks → block registry → ContentBlock, FaqBlock, …
```
## Layout
```txt
apps/web/
├── app/ Next.js routes — thin, they only resolve data and delegate
│ ├── [locale]/
│ │ ├── layout.tsx <html>, NextIntlClientProvider, metadataBase, globals.css
│ │ ├── [[...path]]/page.tsx catch-all: every CMS page in this locale
│ │ ├── preview/page.tsx token-gated draft rendering (force-dynamic, noindex)
│ │ └── not-found.tsx 404 screen
│ ├── api/revalidate/route.ts secret-guarded cache purge, called by the backend
│ ├── sitemap.ts, robots.ts generated from the public API
│ └── globals.css one line: @import 'tailwindcss'
├── web-runtime/ the rendering engine — everything that turns CMS data into a page
│ ├── api/ HTTP client, render-data resolution, cache tags
│ ├── data/normalize.ts untrusted payload → typed contract
│ ├── types.ts that contract
│ ├── blocks/ 6 block renderers + registry + unsupported fallback
│ ├── layouts/ 3 page layouts + registry, hero, breadcrumbs
│ ├── chrome/ site header, nav, footer, language switch
│ ├── components/ link, image and rich-text primitives
│ ├── lib/ metadata, site URL, sitemap, structured data
│ └── renderer.tsx header + layout + footer
├── i18n/ next-intl wiring, navigation helpers, translation JSON
├── shared/ui/ design-system primitives (Button)
├── site.config.ts locales and default locale
├── proxy.ts the next-intl middleware
└── tests/ Vitest specs for the pure logic
```
### Layer responsibilities
| Layer | Owns | Never does |
| ------------------- | --------------------------------------------------------------- | ----------------------------- |
| `app/` | Route params, data resolution, `notFound()`, `generateMetadata` | Markup beyond composition |
| `web-runtime/api/` | Every backend call, cache tags, `server-only` | Rendering |
| `web-runtime/data/` | Turning untrusted JSON into `types.ts` shapes | Fetching or throwing |
| `web-runtime/` | All page markup, registries, primitives | Direct `fetch` calls |
| `i18n/` | Locale resolution, `Link`/`getPathname`, message loading | Content |
| `shared/ui/` | Reusable, content-agnostic primitives | Knowing about pages or blocks |
## Backend Boundary
`web-runtime/api/web-runtime-api.ts` is the single door to the backend — a `server-only` module, a lazily constructed singleton, and the only place `fetch` appears.
| Call | Endpoint | Cache |
| ------------------ | ----------------------------------- | ------------------------- |
| `resolvePage` | `/web-manager/public/pages/resolve` | `revalidate: 60` + tags |
| `listBlogPosts` | `/web-manager/public/blog/pages` | `revalidate: 60` + tags |
| `listSitemapPages` | `/web-manager/public/pages/sitemap` | `revalidate: 3600` + tags |
| `getSettings` | `/web-manager/public/settings` | `revalidate: 60` + tags |
| `getPreview` | `/web-manager/public/pages/preview` | `cache: 'no-store'` |
A `404` becomes `null` (the caller renders the 404 page); any other non-OK status throws. The sitemap walk is capped at 50 requests.
Every response goes through `data/normalize.ts` first. Its one rule is **degrade, never throw**: a missing field becomes a default, an unknown block type becomes `UnknownBlock`, a malformed media object disappears. `types.ts` is what the rest of the app may assume after that.
Publishing purges this app's cache: the backend POSTs tag names to `app/api/revalidate/route.ts`, guarded by `REVALIDATE_SECRET`. The tags in `web-runtime/api/cache-tags.ts` mirror the backend's — see [Web Manager caching](../../core/web-manager/integration.md#caching-and-revalidation).
## Rendering
Two registries decide what gets drawn, and both fall back rather than fail:
```txt
layouts/registry.tsx landing → StandardPageLayout unknown → StandardPageLayout
blog → BlogPageLayout
blog-item → EditorialPageLayout
blocks/registry.tsx content, faq, media-content, articles, banner, media
unknown → UnsupportedBlock (dev-only banner, nothing in production)
```
The CMS can publish a page type or block the site has not learned yet — the worst case is a missing section, never a broken deploy.
`StandardPageLayout` is hero + blocks. `EditorialPageLayout` adds breadcrumbs, `Article` JSON-LD, and a narrower reading measure. `BlogPageLayout` ignores the page's blocks and fetches its own post list (12 per page, `?page=` pager). Each sets `data-page-layout` on its `<main>` — the hook for tests and debugging. `Breadcrumbs` renders nothing for a single-item trail and emits `BreadcrumbList` JSON-LD.
Three shared primitives:
- **`components/links.tsx`** — `PageLink` validates the href (`/`, `#`, or `http/https/mailto/tel`) and renders an unusable link as disabled text. Link variants map onto `shared/ui/Button` variants.
- **`components/media-image.tsx`** — a native `<img>`, not `next/image`: media origins are CMS-managed and unrestricted, so the optimizer's allowlist would be a permanent tax.
- **`components/rich-text.tsx`** — `dangerouslySetInnerHTML`. Safe **only** because the backend sanitizes rich text in `PublicContentService`; never render HTML from any other source through it.
## Internationalization
Locales live in `site.config.ts` (`en`, `uk`, `de`). `i18n/routing.ts` sets `localePrefix` to `always` with multiple locales and `never` with one, so a single-language site serves clean URLs.
- Navigate with `Link`, `redirect`, `usePathname`, `useRouter`, `getPathname` from `i18n/navigation.ts` — never `next/link` or `next/navigation` directly, or the locale prefix is lost.
- CMS hrefs arrive locale-prefixed; pass them through `toPublicHref` (`i18n/content-href.ts`).
- Messages are five namespaces (`messages`, `pages`, `components`, `errors`, `form`) under `i18n/translations/`, merged at request time; a missing locale file falls back per namespace.
- `i18n/types.ts` derives the `Messages` type from the English files — a missing key is a type error.
## SEO
- `app/[locale]/layout.tsx` sets `metadataBase` from `siteUrl()` plus the default title template.
- `web-runtime/lib/page-metadata.ts` builds per-page metadata — title, description, canonical, hreflang with `x-default`, and Open Graph / Twitter cards from the hero image. A pure function of the render data, which is why it has direct tests.
- `app/sitemap.ts` walks the paginated backend listing and `buildSitemap` groups translations into `alternates.languages`.
- `app/robots.ts` allows everything except `/*/preview`.
- `web-runtime/lib/structured-data.tsx` emits `BreadcrumbList` and, on editorial pages, `Article`.
+53
View File
@@ -0,0 +1,53 @@
# Access Control Module
The Access Control module manages admin accounts, roles, and permissions in the admin app. It is the UI boundary for deciding who can access which admin features.
## Source Map
| Area | Path |
| ------------------------- | ------------------------------------------------------- |
| Frontend module | `apps/admin/src/core/AccessControl/` |
| Frontend routes | `apps/admin/src/core/AccessControl/config/routing.tsx` |
| Frontend translations | `apps/admin/src/core/AccessControl/config/translation/` |
| Backend admin module | `apps/backend/app/base_modules/Admin/` |
| Backend role module | `apps/backend/app/base_modules/Role/` |
| Backend permission module | `apps/backend/app/base_modules/Permission/` |
| Permission loader | `apps/backend/app/services/permission_service.ts` |
## Documentation
| Page | Purpose |
| ------------------------- | ------------------------------------------------------------------------------------- |
| [Frontend](./frontend.md) | Routes, UI structure, data layer, forms, translations, and cache invalidation. |
| [Backend](./backend.md) | Admin, role, and permission API routes, policies, validators, and permission loading. |
## Main Capabilities
- List, search, sort, paginate, create, edit, and delete admin accounts.
- Assign roles to admin accounts.
- List, create, edit, and delete roles.
- Assign permissions to roles.
- Load available permission definitions from backend module permission configs.
- Gate access to screens and actions through the Permission core module.
## Ownership Rules
- AccessControl owns management UI for admins, roles, and role permissions.
- Backend Admin owns account persistence and profile update endpoint.
- Backend Role owns role persistence and role-permission sync.
- Backend Permission owns the available permission list endpoint.
- Permission core module owns permission checking and React gates.
- Auth core module owns the current authenticated admin and session state.
## Common Change Guide
| Change | Start Here |
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
| Add an admin field | `entities/AdminEntity.ts`, `data/api/dto.ts`, `data/mappers/AdminMapper.ts`, `view/forms/AdminForm/` |
| Change admin list columns | `view/widgets/AdminTable.tsx` |
| Change role form behavior | `view/forms/RoleForm/` |
| Add a new permission source | Backend module `config/permissions.ts` and `apps/backend/app/services/permission_service.ts` |
| Change cache invalidation | `data/queries/cache.ts` |
| Change translations | `config/translation/en.json` and `config/translation/uk.json` |
| Change backend admin API | `apps/backend/app/base_modules/Admin/controller/admin_controller.ts` |
| Change backend role API | `apps/backend/app/base_modules/Role/controller/role_controller.ts` |
+89
View File
@@ -0,0 +1,89 @@
# Access Control Backend
Access Control frontend uses three backend base modules:
```txt
apps/backend/app/base_modules/Admin/
apps/backend/app/base_modules/Role/
apps/backend/app/base_modules/Permission/
```
## Routes
| Method | Route | Backend | Purpose |
| -------- | ----------------------- | ---------- | ------------------------------------------- |
| `GET` | `/admin` | Admin | Paginated admin list. |
| `GET` | `/admin/:id` | Admin | Admin details with roles. |
| `POST` | `/admin` | Admin | Create admin and sync roles. |
| `PUT` | `/admin/:id` | Admin | Update admin and sync roles. |
| `DELETE` | `/admin/:id` | Admin | Delete admin. |
| `PUT` | `/admin/update-profile` | Admin | Update current authenticated admin profile. |
| `GET` | `/role` | Role | Paginated role list. |
| `GET` | `/role/:id` | Role | Role details with permissions. |
| `POST` | `/role` | Role | Create role and role-permission rows. |
| `PUT` | `/role/:id` | Role | Update role and role-permission rows. |
| `DELETE` | `/role/:id` | Role | Delete role. |
| `GET` | `/permission` | Permission | List all available permission definitions. |
All routes use `middleware.auth()` except login routes owned by Auth.
## Authorization
Policies:
```txt
apps/backend/app/base_modules/Admin/policy/admin_policy.ts
apps/backend/app/base_modules/Role/policy/role_policy.ts
apps/backend/app/base_modules/Permission/policy/permission_policy.ts
```
Permission slugs are defined in:
```txt
apps/backend/app/base_modules/Admin/config/permissions.ts
apps/backend/app/base_modules/Role/config/permissions.ts
```
Current AccessControl slugs:
```txt
admins.view
admins.create
admins.update
admins.delete
role.view
role.create
role.update
role.delete
```
## Permission Loading
Available permissions are collected by:
```txt
apps/backend/app/services/permission_service.ts
```
`getAllPermissions()` scans `app/base_modules/*/config/permissions.ts` and `app/modules/*/config/permissions.ts`. Add permissions to the owning backend module config; do not hard-code them in the frontend.
## Validation
Validators:
```txt
apps/backend/app/base_modules/Admin/validators/admin_create_validator.ts
apps/backend/app/base_modules/Admin/validators/admin_update_validator.ts
apps/backend/app/base_modules/Admin/validators/admin_update_profile_validator.ts
apps/backend/app/base_modules/Role/validator/role_create_validator.ts
apps/backend/app/base_modules/Role/validator/role_update_validator.ts
```
Keep frontend form rules compatible with backend validators. Backend validation is authoritative.
## Data Notes
- Admin create/update syncs assigned roles through the admin-role relation.
- Role create/update rewrites `role_permissions` rows for the role.
- Permission records returned by `/permission` are loaded from permission config files, not only database rows.
- Deleting the final admin is blocked by the Admin controller.
+141
View File
@@ -0,0 +1,141 @@
# Access Control Frontend
The frontend module provides the admin-facing UI for managing admins, roles, and permissions.
Source: `apps/admin/src/core/AccessControl/`
## Directory Structure
```txt
AccessControl/
config/ Routes, menu, messages, translations.
data/
api/ Raw HTTP calls and DTOs.
mappers/ DTO-to-entity mapping.
queries/ React Query hooks, keys, and cache invalidation.
entities/ Admin, role, permission entities and enums.
pages/ Route-level pages.
view/
forms/ Admin and role forms.
layouts/ Module layout.
widgets/ Tables and modal triggers.
```
## Routes
Frontend route config:
```txt
config/routing.tsx
```
Mounted module path:
```txt
/access-control
```
| Route | Page | Purpose |
| ----------------------- | --------------- | ----------------------------------------- |
| `/access-control/admin` | `AdminListPage` | Manage admin accounts and assigned roles. |
| `/access-control/role` | `RoleListPage` | Manage roles and assigned permissions. |
## Data Layer
Raw API calls live in:
```txt
data/api/AdminAPI.ts
data/api/RoleAPI.ts
data/api/PermissionAPI.ts
data/api/dto.ts
```
Query hooks live in:
```txt
data/queries/admin.ts
data/queries/role.ts
data/queries/permission.ts
```
Cache invalidation helpers live in:
```txt
data/queries/cache.ts
```
Current helpers:
```ts
invalidateAdminList();
invalidateAdminDetailed(id?);
invalidateRoleList();
invalidateRoleDetailed(id?);
invalidatePermissionList();
```
## DTO Mapping
The API layer returns DTOs. Query hooks map DTOs into entities before data reaches UI code.
```txt
data/mappers/AdminMapper.ts
data/mappers/RoleMapper.ts
data/mappers/PermissionMapper.ts
entities/AdminEntity.ts
entities/RoleEntity.ts
entities/PermissionEntity.ts
```
Keep backend response shape in DTOs. Keep UI/domain shape in entities.
## Forms
Admin form:
```txt
view/forms/AdminForm/
```
Responsibilities:
- Create and update admin data.
- Select admin status.
- Assign roles.
- Optionally update password.
Role form:
```txt
view/forms/RoleForm/
```
Responsibilities:
- Create and update role name.
- Group available permissions by module slug prefix.
- Assign permission slugs to the role.
## Widgets
| Widget | Purpose |
| -------------------- | -------------------------------------------- |
| `AdminTable` | Admin list, filters, actions, create button. |
| `RoleTable` | Role list, actions, create button. |
| `ManageAdminTrigger` | Admin create/edit dialog orchestration. |
| `ManageRoleTrigger` | Role create/edit dialog orchestration. |
| `DeleteAdminTrigger` | Admin delete confirmation and mutation. |
| `DeleteRoleTrigger` | Role delete confirmation and mutation. |
## Translations
Translations are module-local:
```txt
config/messages.ts
config/translation/en.json
config/translation/uk.json
```
Use only `ACCESS_CONTROL.*` keys inside this module.
+36
View File
@@ -0,0 +1,36 @@
# Account Settings Module
The Account Settings module provides the current admin user's profile settings screen. It lets the signed-in admin update profile fields and password, and view assigned roles.
## Source Map
| Area | Path |
| ------------------------- | ---------------------------------------------------------------------------------- |
| Frontend module | `apps/admin/src/core/AccountSettings/` |
| Frontend routes | `apps/admin/src/core/AccountSettings/config/routing.tsx` |
| Frontend translations | `apps/admin/src/core/AccountSettings/config/translation/` |
| Auth provider dependency | `apps/admin/src/core/Auth/providers/AuthProvider/` |
| Backend profile endpoint | `apps/backend/app/base_modules/Admin/controller/admin_controller.ts` |
| Backend profile validator | `apps/backend/app/base_modules/Admin/validators/admin_update_profile_validator.ts` |
## Documentation
| Page | Purpose |
| ------------------------- | --------------------------------------------------------- |
| [Frontend](./frontend.md) | Route, page, form, profile update flow, and translations. |
| [Backend](./backend.md) | Current-user profile endpoint and validation. |
## Main Capabilities
- Display current authenticated admin profile.
- Update first name, last name, email, status, and optional password.
- Show assigned roles.
- Crop and stage an avatar image in the form UI.
- Use module-local translations through `ACCOUNT.*` keys.
## Ownership Rules
- AccountSettings owns the current-user profile UI.
- Auth owns current-user state, token storage, and API calls used by the profile page.
- Backend Admin owns profile persistence and validation.
- AccountSettings should not manage other admins; that belongs to AccessControl.
+69
View File
@@ -0,0 +1,69 @@
# Account Settings Backend
Account Settings does not have a separate backend base module. It uses the Admin backend module's current-user profile endpoint.
## Route
| Method | Route | Controller | Purpose |
| ------ | ----------------------- | ------------------------------- | ----------------------------------------- |
| `PUT` | `/admin/update-profile` | `AdminController.updateProfile` | Update the currently authenticated admin. |
Source:
```txt
apps/backend/app/base_modules/Admin/routes.ts
apps/backend/app/base_modules/Admin/controller/admin_controller.ts
```
The route uses `middleware.auth()`.
## Request Flow
```txt
PUT /admin/update-profile
-> auth middleware
-> AdminController.updateProfile
-> admin_update_profile_validator
-> current auth user merge/save
-> reload roles
-> query role_permissions
-> return { user, permissions }
```
## Validation
Validator:
```txt
apps/backend/app/base_modules/Admin/validators/admin_update_profile_validator.ts
```
Keep frontend validation compatible with this validator. Backend validation is authoritative.
## Response Contract
The endpoint returns the same shape consumed by Auth:
```ts
type GetUserResponse = {
user: {
id: number;
email: string;
firstName: string;
lastName: string;
status: string;
createdAt: string;
roles: Array<{ id: number; name: string }>;
};
permissions: string[];
};
```
AuthProvider maps this response into `AuthUser` and updates current user state.
## Ownership Rules
- AccountSettings owns the UI and form behavior.
- Auth owns `updateUser` and current user state updates.
- Backend Admin owns profile persistence and response shape.
- AccessControl owns managing other admin accounts.
+99
View File
@@ -0,0 +1,99 @@
# Account Settings Frontend
The frontend module implements the current admin profile settings page.
Source: `apps/admin/src/core/AccountSettings/`
## Directory Structure
```txt
AccountSettings/
config/ Routes, messages, translations.
pages/ Profile route page.
view/
forms/ Profile form and form fields.
layouts/ Account layout.
```
## Routes
Frontend route config:
```txt
config/routing.tsx
```
Mounted module path:
```txt
/account/*
```
| Route | Page | Purpose |
| ------------------ | ------------- | ------------------------------- |
| `/account/profile` | `ProfilePage` | Current admin profile settings. |
The index route redirects to `/account/profile`.
## Profile Page
Main file:
```txt
pages/Profile/index.tsx
```
Responsibilities:
- Read the current user from `useAuth()`.
- Submit profile updates through `updateUser`.
- Map backend validation errors into form fields.
- Render assigned roles.
## Form
Profile form files:
```txt
view/forms/ProfileForm/Component.tsx
view/forms/ProfileForm/form.ts
view/forms/ProfileForm/fields/PasswordField.tsx
view/forms/ProfileForm/fields/AvatarCropperField.tsx
```
Password behavior:
- Password fields are hidden until toggled open.
- Password is optional.
- If provided, password must be at least 8 characters.
- If provided, confirmation must match.
Avatar behavior:
- The field supports local crop/preview UI.
- The form currently stages avatar data in the form value.
- Profile update payload strips avatar before calling `updateUser`.
## Auth Dependency
## Translations
Translations are module-local:
```txt
config/messages.ts
config/translation/en.json
config/translation/uk.json
```
Use only `ACCOUNT.*` keys inside this module.
## Common Change Guide
| Change | Start Here |
| ---------------------------- | ----------------------------------------------------------------------------- |
| Add a profile field | `ProfileForm/form.ts`, `ProfileForm/Component.tsx`, backend profile validator |
| Change password validation | `ProfileForm/form.ts` |
| Change avatar UI | `ProfileForm/fields/AvatarCropperField.tsx` |
| Change profile save behavior | `pages/Profile/index.tsx` and AuthProvider `updateUser` |
| Change translations | `config/translation/en.json` and `config/translation/uk.json` |
+39
View File
@@ -0,0 +1,39 @@
# Auth Module
The Auth module owns admin authentication, current-user state, token persistence, auth routes, and route guarding.
## Source Map
| Area | Path |
| --------------------- | ---------------------------------------------------- |
| Frontend module | `apps/admin/src/core/Auth/` |
| Frontend routes | `apps/admin/src/core/Auth/config/routing.tsx` |
| Frontend provider | `apps/admin/src/core/Auth/providers/AuthProvider/` |
| Frontend translations | `apps/admin/src/core/Auth/config/translation/` |
| Backend module | `apps/backend/app/base_modules/Auth/` |
| Backend admin model | `apps/backend/app/base_modules/Admin/model/admin.ts` |
| Auth config | `apps/backend/config/auth.ts` |
## Documentation
| Page | Purpose |
| ------------------------- | ------------------------------------------------------------------------------------- |
| [Frontend](./frontend.md) | AuthProvider, login page, route guard, token storage, and translations. |
| [Backend](./backend.md) | Login/logout/current-user routes, validators, token behavior, and response contracts. |
## Main Capabilities
- Sign in with admin email and password.
- Store bearer token client-side.
- Verify and load current authenticated admin.
- Expose current user, permissions, login, logout, verify, and profile update methods.
- Guard app routes until authentication is known.
- Clear React Query cache on logout.
## Ownership Rules
- Auth owns session state and current user state.
- Auth owns token persistence.
- Auth owns auth route UI and auth guard widget.
- AccountSettings may call AuthProvider `updateUser` but does not own auth state.
- Permission gates consume `AuthUser.permissions` but do not fetch auth state themselves.
+105
View File
@@ -0,0 +1,105 @@
# Auth Backend
The backend Auth module exposes token-based admin authentication routes.
Source: `apps/backend/app/base_modules/Auth/`
## Routes
| Method | Route | Controller Method | Auth Required | Purpose |
| ------ | -------------- | ----------------- | ------------- | ------------------------------------------- |
| `POST` | `/auth/login` | `login` | No | Verify credentials and create bearer token. |
| `POST` | `/auth/logout` | `logout` | Yes | Invalidate current bearer token. |
| `GET` | `/auth/user` | `user` | Yes | Return current admin and permission slugs. |
Route file:
```txt
apps/backend/app/base_modules/Auth/routes.ts
```
## Request Flow
### Login
```txt
POST /auth/login
-> auth_login_validator
-> Admin.findBy('email')
-> hash.verify(password)
-> Admin.accessTokens.create(admin)
-> return { type, token, user }
```
### Current User
```txt
GET /auth/user
-> auth middleware
-> auth.use('api').user
-> load roles
-> query role_permissions
-> return { user, permissions }
```
## Validation
Login validator:
```txt
apps/backend/app/base_modules/Auth/validators/auth_login_validator.ts
```
Current rules:
- `email`: trimmed email string.
- `password`: trimmed string with minimum length 8.
## Token Guard
Auth config:
```txt
apps/backend/config/auth.ts
```
The admin model is configured as the token user provider:
```txt
apps/backend/app/base_modules/Admin/model/admin.ts
```
## Response Contracts
Login response consumed by frontend:
```ts
type LoginResponse = {
type: "bearer";
token: string;
user: unknown;
};
```
Current-user response consumed by frontend:
```ts
type GetUserResponse = {
user: {
id: number;
email: string;
firstName: string;
lastName: string;
status: string;
createdAt: string;
roles: Array<{ id: number; name: string }>;
};
permissions: string[];
};
```
## Ownership Rules
- Auth backend owns token creation, token invalidation, and current-user response shape.
- Admin backend owns admin persistence and profile updates.
- Permission data in `/auth/user` is derived from role-permission rows.
+127
View File
@@ -0,0 +1,127 @@
# Auth Frontend
The frontend module implements authentication screens, session provider, current-user loading, and route guards.
Source: `apps/admin/src/core/Auth/`
## Directory Structure
```txt
Auth/
config/ Routes, messages, translations.
data/
api/ Raw auth/profile API calls and DTOs.
mappers/ Current-user DTO mapping.
stores/ Local token storage helpers.
entities/ Auth and user types.
pages/ Sign-in page.
providers/ AuthProvider and context.
view/
forms/ Sign-in form.
layouts/ Auth layouts.
widgets/ Auth guard.
```
## Routes
Frontend route config:
```txt
config/routing.tsx
```
Mounted module path:
```txt
/auth/*
```
| Route | Page | Purpose |
| -------------- | ------------ | --------------------- |
| `/auth/signin` | `SignInPage` | Admin sign-in screen. |
The index route redirects to `signin`.
## AuthProvider
Provider file:
```txt
providers/AuthProvider/AuthProvider.tsx
```
Exposes:
```ts
auth;
user;
loading;
login(email, password);
logout();
verify();
getUser();
updateUser(payload);
saveAuth(auth);
setUser(user);
```
Important behavior:
- `login` stores the access token, then loads the current user.
- `verify` reloads current user when a token exists.
- `logout` calls the backend, clears auth state, and clears React Query cache.
- `updateUser` calls `/admin/update-profile` and updates current user state.
## API Boundary
Raw API calls:
```txt
data/api/api.ts
data/api/dto.ts
```
| Method | Backend Route | Purpose |
| ------------ | --------------------------- | ---------------------------------- |
| `login` | `POST /auth/login` | Create bearer token. |
| `logout` | `POST /auth/logout` | Invalidate current token. |
| `getUser` | `GET /auth/user` | Load current user and permissions. |
| `updateUser` | `PUT /admin/update-profile` | Update current admin profile. |
Current-user DTO mapping:
```txt
data/mappers/userMapper.ts
```
## Token Storage
Token persistence helpers live in:
```txt
data/stores/localStorage.ts
```
Keep token storage behind these helpers so storage format changes stay local.
## Route Guard
Auth guard widget:
```txt
view/widgets/AuthGuardWidget.tsx
```
Use this widget to protect authenticated app routes. It coordinates auth loading and redirects unauthenticated users to auth routes.
## Translations
Translations are module-local:
```txt
config/messages.ts
config/translation/en.json
config/translation/uk.json
```
Use only `AUTH.*` keys inside this module.
+28
View File
@@ -0,0 +1,28 @@
# Devtools
The admin devtools module contains development-only interfaces for generating business modules, reviewing UI and form components in the sandbox, and managing the local application version.
Source: `apps/admin/src/devtools/`
The frontend module is registered through `apps/admin/src/devtools/config/module.ts`. Its routes, menu, and translations are exposed only when `IS_DEV_MODE` is enabled. All devtools frontend routes are protected routes.
## Documentation
| Area | Purpose | Documentation |
| ---------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------ |
| Module generator | Generates backend and admin frontend modules from a database table and UI configuration | [Module generator](./module-generator/Readme.md) |
| Sandbox | Interactive examples and copyable source snippets for shared UI and form controls | [Sandbox](./sandbox/Readme.md) |
The version manager is part of the same source module but is outside the scope of these guides.
## Frontend Routes
| Route | Page |
| ------------------------------------ | ------------------------------ |
| `/devtools/module-generator` | Module generator |
| `/devtools/module-generator/docs` | Module generator documentation |
| `/devtools/sandbox/ui` | Shared UI component sandbox |
| `/devtools/sandbox/forms` | Composed form-control sandbox |
| `/devtools/module-generator/version` | Version manager |
Devtools module is enabled only in dev mode!
@@ -0,0 +1,53 @@
# Module Generator
Creates a backend module and a matching admin frontend module from an existing PostgreSQL table, combining database metadata with frontend code builders and Handlebars templates. The output is a maintainable starting point — generated authorization, validation, relationships, API behavior, labels, and translations must still be reviewed for the product domain.
| Part | Source |
| ------------------------------- | --------------------------------------------------- |
| Generator page and form | `apps/admin/src/devtools/` |
| Copied frontend module template | `apps/admin/src/modules/_template/` |
| Generator HTTP endpoints | `apps/backend/start/routes.ts` |
| Generator controller | `apps/backend/app/controllers/module_controller.ts` |
| Database inspection | `apps/backend/app/services/database_inspector.ts` |
| Backend Handlebars templates | `apps/backend/app/templates/backend/` |
| Frontend Handlebars templates | `apps/backend/app/templates/frontend/` |
Details: [frontend.md](./frontend.md) (UI, payload builders, generated frontend), [backend.md](./backend.md) (endpoints, inspection, templates, generated backend), [troubleshooting.md](./troubleshooting.md) (module not loaded / missing from menu).
## Prerequisites
1. Backend running from `apps/backend/` (destinations resolve relative to `process.cwd()`); admin app in dev mode; authenticated session (all endpoints use auth middleware).
2. The source table created and migrated — the form reads PostgreSQL `information_schema`.
3. A module name valid as a TypeScript identifier, route segment, permission slug, and folder name.
## Generation Flow
```mermaid
flowchart LR
A["Select table and assets"] --> B["Configure form and table"]
B --> C["Frontend builders create source strings"]
C --> D["POST /makeModule/generate"]
D --> E["Inspect database metadata"]
E --> F["Copy frontend _template"]
E --> G["Render backend templates"]
F --> H["Write configured frontend files"]
G --> I["Write backend module and tests"]
```
Output goes to `apps/backend/modules/<ModuleName>/`, `apps/backend/tests/modules/<ModuleName>/`, and `apps/admin/src/modules/<name>/`. Frontend modules are discovered through their `config/module.ts` manifest; backend routes load from `apps/backend/modules/` at startup.
## Operational Behavior
- Generation writes directly into both workspaces — no preview, transaction, or rollback; existing files may be overwritten. Generate into a clean name and inspect the Git diff.
- Select `model` whenever you select `api` — API generation imports the model for its Swagger schema.
- The frontend template is always copied; form and datatable files are populated only when selected.
- Generated code is not formatted or committed automatically.
## Verification Checklist
1. Inspect the Git diff; confirm route paths, API prefixes, permission slugs, and naming.
2. Review nullable fields, dates, passwords, foreign keys, validation, relationship names; replace generic labels and translations.
3. Verify list/create/update/delete, authorization, and validation-error behavior.
4. Run `npm run build:backend` and `npm run build:admin`; run the generated backend tests and add domain coverage.
Generated frontend follows the [React frontend architecture](../../../architecture/react-frontend/Readme.md); review backend modules against the [backend architecture](../../../architecture/backend/Readme.md).
@@ -0,0 +1,61 @@
# Module Generator Backend
Authenticated schema-inspection and generation endpoints; backend files rendered from Handlebars templates, frontend files written from a copied template plus source strings supplied by the admin app.
## Endpoints
Registered in `apps/backend/start/routes.ts`, all behind `middleware.auth()`:
| Method | Path | Purpose |
| ------ | ---------------------------- | -------------------------------------------------- |
| `GET` | `/makeModule/tables` | List eligible public PostgreSQL tables |
| `GET` | `/makeModule/columns/:table` | Return columns used to configure form/table fields |
| `POST` | `/makeModule/generate` | Generate selected frontend and backend assets |
## Database Inspection
`database_inspector.ts` reads `information_schema`: only the `public` schema, excluding framework/auth/access-control/migration/system tables. Columns ending in `_id` become belongs-to candidates; tables containing a `<table>_id` column become has-many candidates; searchable string/numeric columns are derived for resource controllers. **Relationship inference is heuristic** — review generated model imports, class and property names.
## Type Mapping
| Database category | Lucid TypeScript type | Vine type | Frontend type |
| --------------------- | --------------------- | --------- | --------------- |
| integer/numeric types | `number` | `number` | `number` |
| boolean | `boolean` | `boolean` | `boolean` |
| date/timestamp | `DateTime` | `date` | `IsoDateString` |
| other values | `string` | `string` | `string` |
Nullable columns get `| null` and optional validation; `created_at`, `updated_at`, `deleted_at` receive special handling.
## Backend Output
```txt
apps/backend/modules/<ModuleName>/
├── api/ # controller, create/update validators, routes.ts
├── config/permissions.ts
├── controller/<entity>_controller.ts
├── model/<entity>.ts
├── policy/<entity>_policy.ts
├── validators/ # <entity>_create/_update_validator.ts
└── routes.ts
apps/backend/tests/modules/<ModuleName>/ # <entity>.spec.ts, <entity>.api.spec.ts
```
`<entity>` is the table name minus its final character — assumes a plural table name; review irregular plurals.
## Asset Behavior
- **Model** — `model.hbs` with columns, inferred relationships, searchable fields, optional password hashing.
- **Controller and routes** — authenticated resource controller with list/search/sort/pagination and CRUD; `routes.hbs` registers an `apiOnly` resource route with authentication.
- **Validators** — separate create/update Vine validators; primary keys and timestamp/deletion fields excluded; email, password, nullable, and trimming behavior inferred from names and metadata.
- **Policy and permissions** — Bouncer policy with view/create/update/delete, `config/permissions.ts`, and a lazy policy import added to `app/policies/main.ts` when missing. Slugs derive from the module name (`<name>s.view`, `.create`, `.update`, `.delete`) — review already-plural names, this is not a general pluralization system.
- **API subtree** — a second CRUD controller and routes under `/api/<entity>` with validators and a test; imports the model for `modelToSchema` Swagger metadata. **The generated API template adds no authentication middleware — add auth and authorization before exposing the route.**
## Frontend Writes
The backend copies `apps/admin/src/modules/_template` to `apps/admin/src/modules/<name>`, then writes the configured form (component + definition), datatable and list page, manage trigger, menu, module manifest, generated `config/meta.ts`, and database-derived `entities/Entity.ts`. See [frontend.md](./frontend.md).
## Extending
Handlebars helpers available: `eq`, `not`, `or`, `and`. To add a generated artifact: add the template under `apps/backend/app/templates/`, an explicit destination in the controller `paths` object (parent dir via existing path initialization), render only when its asset is selected, and pass normalized metadata — never query the database inside templates. Add generation tests for nullable values, dates, numerics, passwords, and irregular names; verify both builds. No hidden writes — every destination, including shared registries like `app/policies/main.ts`, stays visible in the controller flow and documented here.
@@ -0,0 +1,69 @@
# Module Generator Frontend
The generator UI collects module metadata, selected assets, form layout, and datatable configuration, converts it into TypeScript/TSX source strings, and sends them to the backend generator.
| Responsibility | Source (under `apps/admin/src/devtools/`) |
| ------------------------------------------------------ | ------------------------------------------------------------- |
| Page submission, notifications, and mutation callbacks | `pages/ModuleGeneratorPage/index.tsx` |
| Payload assembly | `pages/ModuleGeneratorPage/lib/buildGenerateModulePayload.ts` |
| Form state, initial values, and sections | `view/forms/ModuleGeneratorForm/` |
| Payload DTOs and HTTP client | `data/api/` |
| React Query hooks | `data/queries/` |
| Source-code builders | `data/builders/` |
The page submits through `useGenerateModule`; success resets the form and shows an alert, errors go through the shared React Query error helpers.
## Metadata and Assets
Required metadata: `name` (frontend folder, backend module identity, route segment, permission-slug base), `table` (existing PostgreSQL table), `generate` (selected assets). Backend assets are all selected by default; form and datatable are opt-in.
| Asset | Workspace | Output |
| ------------ | --------- | --------------------------------------------------- |
| `model` | Backend | Lucid model |
| `controller` | Backend | Authenticated resource controller and test |
| `routes` | Backend | Resource route registration |
| `validator` | Backend | Create and update Vine validators |
| `policy` | Backend | Bouncer policy and permission configuration |
| `api` | Backend | API controller, validators, routes, and test |
| `form` | Frontend | Form definition, component, and modal/sheet trigger |
| `datatable` | Frontend | List page table and menu entry |
Before generation the UI reads `GET /makeModule/tables` (table selector) and `GET /makeModule/columns/:table` (field choices) — cached via React Query, columns disabled until a table is selected.
## Payload Builders
`buildGenerateModulePayload` delegates to focused builders:
| Builder | Payload field | Generated destination |
| ----------------------------------- | ---------------- | ----------------------------------- |
| `FormSourceBuilder.buildComponent` | `form` | `view/forms/MainForm/Component.tsx` |
| `FormSourceBuilder.buildDefinition` | `formDefinition` | `view/forms/MainForm/form.ts` |
| `ManageTriggerSourceBuilder` | `formDialog` | `view/widgets/ManageTrigger.tsx` |
| `DataTableSourceBuilder` | `dataTable` | `view/widgets/DataTable.tsx` |
| `ListPageSourceBuilder` | `list` | `pages/ListPage/index.tsx` |
| `MenuSourceBuilder` | `menu` | `config/menu.ts` |
| `ModuleConfigSourceBuilder` | `module` | `config/module.ts` |
Form disabled → `form`, `formDefinition`, `formDialog` are `null`. Datatable disabled → `dataTable` and `menu` are `null`, and the list-page builder emits a page without a table.
## Generated Frontend Architecture
```txt
apps/admin/src/modules/<name>/
├── api/ # create.ts, delete.ts, detailed.ts, keys.ts, list.ts, update.ts
├── config/ # translation/, menu.ts, messages.ts, meta.ts, module.ts, routing.tsx
├── entities/ # Entity.ts (from database columns)
├── pages/ListPage/
└── view/
├── forms/MainForm/ # Component.tsx, constants.ts, form.ts, index.ts, submit.ts
├── layouts/Layout.tsx
└── widgets/ # DataTable.tsx, DeleteTrigger.tsx, ManageTrigger.tsx
```
`api/` deliberately has no barrel file — each CRUD operation is its own file, imported directly (`import { useCreate } from "../../../api/create"`); `keys.ts` owns query key builders, `list.ts`/`detailed.ts` their invalidation helpers.
Layer ownership: `entities/Entity.ts` is shared by API hooks, forms, and widgets; `api/` owns Axios calls, hooks, keys, and notifications; `pages/` owns route composition and permission gates; `view/forms/` owns schema, values, UI, and submission; `view/widgets/` owns stateful interactions; `config/` integrates routes, menu, translations, and permissions with the module registry.
## Extending
To add a generated frontend artifact: add its configuration type under `entities/`, UI controls to `ModuleGeneratorForm`, a focused builder under `data/builders/` (for dynamic source), the payload field to `GenerateModulePayload` + `buildGenerateModulePayload`, accept and write it in `module_controller.ts`, and add static structure to `_template/` when every module needs it. Verify generation with and without the asset selected.
@@ -0,0 +1,31 @@
# Module Generator Troubleshooting
## Generated Files Exist but the Module Is Not Loaded
Docker file watching does not reliably pick up entirely new directories: the backend discovers `apps/backend/modules/` routes at startup, and the admin app discovers `apps/admin/src/modules/**/config/module.ts` through Vite's module registry. Restart both services and reload the admin app:
```bash
docker compose restart
```
Still not loaded? Check logs for TypeScript errors, invalid generated imports, route-loading or manifest errors:
```bash
docker compose logs --tail=200 backend admin
```
(Outside Compose, restart the backend and admin dev processes manually.)
## Module Loads but Is Missing from the Menu
Usually a permission mismatch or invalid menu/module configuration. Check in order:
1. **Permission slugs match exactly** — backend `apps/backend/modules/<ModuleName>/config/permissions.ts` (e.g. `{ slug: 'products.view' }`) vs frontend `apps/admin/src/modules/<name>/config/meta.ts` (`MODULE_PERMISSION_VIEW = 'products.view'`), for view/create/update/delete. Pluralization is heuristic — already-plural or irregular names need manual correction.
2. **The current role owns the view permission** — the menu filters on `MODULE_PERMISSION_VIEW`: the permission exists in access control, the role has it with the exact slug, and the session was refreshed after role changes.
3. **`config/menu.ts`** — valid exported menu factory, `path` uses `MODULE_ROOT_PATH`, `permissions` contains `MODULE_PERMISSION_VIEW`, the route exists. The menu is generated with the Datatable asset — without it, add a menu manually or keep the module hidden.
4. **`config/module.ts`** — exports a valid `AppModuleConfig`, unique module ID, not `enabled: false`, exposes `menu`, `routes`, `messages`.
5. **`config/routing.tsx`** — `MODULE_ROOT_PATH`, the menu path, and the generated route all point to the same URL (a broken route and an invisible menu item are separate problems).
## Final Checks
Run `npm run build:backend` and `npm run build:admin` to expose generated TypeScript or import errors, then verify: both module directories exist with the selected assets, services were restarted, slugs match, the role has the view permission, and the menu path matches the route. Module discovery errors are reported in backend and admin startup logs.
+19
View File
@@ -0,0 +1,19 @@
# Devtools Sandbox
The sandbox is an interactive development surface for reviewing shared admin UI components and composed form controls. Each example renders the real component and exposes a copyable usage snippet.
## Routes and Source
| Route | Source | Coverage |
| ------------------------- | ------------------------------------------------- | --------------------------------------------- |
| `/devtools/sandbox/ui` | `apps/admin/src/devtools/pages/SandboxUIPage/` | Shared UI primitives and composed UI behavior |
| `/devtools/sandbox/forms` | `apps/admin/src/devtools/pages/SandboxFormsPage/` | Shared composed form controls |
## Maintenance Rules
- Use real shared components; do not duplicate their implementation in devtools.
- Keep examples small enough to show one API or behavior clearly.
- Keep rendered examples and source snippets synchronized.
- Prefer adding edge-state examples when a component has loading, empty, error, disabled, or destructive behavior.
- Do not add backend dependencies to the sandbox unless the example explicitly documents and isolates them.
- Remove sandbox examples when the underlying shared API is removed.
+47
View File
@@ -0,0 +1,47 @@
# Fallback Screens Module
The Fallback Screens module owns route-level and reusable fallback pages in the admin app. It currently provides HTTP-style error screens for forbidden, not found, and server error states, and it is the intended home for future fallback pages such as maintenance, unavailable, empty-state, or generic recovery screens.
## Source Map
| Area | Path |
| --------------------- | ------------------------------------------------------------ |
| Frontend module | `apps/admin/src/core/FallbackScreens/` |
| Frontend routes | `apps/admin/src/core/FallbackScreens/config/routing.tsx` |
| Frontend translations | `apps/admin/src/core/FallbackScreens/config/translation/` |
| Frontend layout | `apps/admin/src/core/FallbackScreens/view/layouts/` |
| Backend module | Not applicable. Fallback Screens is currently frontend-only. |
## Documentation
| Page | Purpose |
| ------------------------- | ----------------------------------------------------------------- |
| [Frontend](./frontend.md) | Routes, page structure, translations, ownership, and conventions. |
| [Backend](./backend.md) | Backend ownership notes and when a backend change may be needed. |
## Main Capabilities
- Render full-page fallback screens under `/error/*`.
- Provide reusable fallback page components for other core modules.
- Keep fallback screen translations module-local.
- Provide a consistent layout for fallback pages.
- Preserve stable public fallback URLs such as `/error/403`, `/error/404`, and `/error/500`.
## Ownership Rules
- FallbackScreens owns fallback page UI, fallback routing, and fallback-specific translations.
- Other modules may import reusable fallback pages, for example permission gates using `Error403Page`.
- Shared API/query error parsing does not belong here; keep it in shared error/query utilities.
- Domain-specific empty states should stay in the owning domain module unless they are generic enough to reuse across modules.
- Backend modules own the actual API status codes and response payloads. FallbackScreens only decides how the admin frontend presents fallback states.
## Common Change Guide
| Change | Start Here |
| ------------------------------ | ------------------------------------------------------------------------------ |
| Add a new fallback route | `apps/admin/src/core/FallbackScreens/config/routing.tsx` |
| Add a new fallback page | `apps/admin/src/core/FallbackScreens/pages/` |
| Change fallback page layout | `apps/admin/src/core/FallbackScreens/view/layouts/FallbackScreensLayout.tsx` |
| Change translations | `apps/admin/src/core/FallbackScreens/config/translation/en.json` and `uk.json` |
| Reuse a fallback in a module | Export it from `apps/admin/src/core/FallbackScreens/index.ts` |
| Change global fallback routing | `apps/admin/src/app/routing/app_routing_setup.tsx` |
+29
View File
@@ -0,0 +1,29 @@
# Fallback Screens Backend
Fallback Screens is currently a frontend-only core module.
There is no backend source folder for this module.
## Backend Relationship
Backend modules may return HTTP status codes or API error payloads that eventually lead the admin frontend to show a fallback screen, but the fallback UI is owned by:
```txt
apps/admin/src/core/FallbackScreens/
```
Examples:
| Status | Frontend fallback | Notes |
| ------ | ---------------------- | --------------------------------------------- |
| `403` | `/error/403` or inline | Used for permission denied states. |
| `404` | `/error/404` | Used for unknown frontend routes. |
| `500` | `/error/500` | Used for generic server failure presentation. |
## When Backend Changes Are Needed
Backend changes are only needed when:
- A backend endpoint should return a different status code.
- A redirect or auth flow should navigate the frontend to a fallback route.
- A feature needs a new machine-readable error contract that the frontend can map to a fallback screen.
+79
View File
@@ -0,0 +1,79 @@
# Fallback Screens Frontend
The frontend module provides fallback UI for app-level error and recovery states.
Source: `apps/admin/src/core/FallbackScreens/`
## Directory Structure
```txt
FallbackScreens/
config/ Routes, messages, and translations.
pages/ Route-level fallback pages.
view/
layouts/ Shared fallback page layout.
```
## Routes
Frontend route config:
```txt
config/routing.tsx
```
Mounted route path:
```txt
/error/*
```
| Route | Page | Purpose |
| ------------ | -------------- | ------------------------------------ |
| `/error` | `Error404Page` | Default fallback route. |
| `/error/403` | `Error403Page` | Forbidden or permission denied page. |
| `/error/404` | `Error404Page` | Not found page. |
| `/error/500` | `Error500Page` | Internal server error page. |
Unknown `/error/*` paths redirect to `/error/404`.
## App Integration
The app-level router mounts `fallbackScreensRoute` from:
```txt
apps/admin/src/app/routing/app_routing_setup.tsx
```
The app-level i18n config merges `fallbackScreensMessages` from:
```txt
apps/admin/src/app/i18n/config.ts
```
## Translations
Translations are module-local:
```txt
config/messages.ts
config/translation/en.json
config/translation/uk.json
```
Use only `FALLBACK_SCREENS.*` keys inside this module.
Do not put fallback page strings in app-level files such as:
```txt
apps/admin/src/app/i18n/messages/en.json
apps/admin/src/app/i18n/messages/uk.json
```
## Page Conventions
- Keep route-level fallback pages under `pages/*Page/index.tsx`.
- Keep shared wrapper layout under `view/layouts/`.
- Keep public URLs stable unless there is a migration plan.
- Prefer module-local translation keys for all visible text, including generic-looking actions like return-home links.
- Add a route only when the fallback is intended to be navigable. Inline-only reusable fallbacks can be exported without a route.
+102
View File
@@ -0,0 +1,102 @@
# File Manager Module
Authenticated file browsing and file operations for the admin app: a backend API for storage operations and a frontend UI for browsing, previewing, uploading, renaming, deleting, duplicating, and editing supported files.
## Source Map
| Area | Path |
| ---------------------- | ------------------------------------------------------------------------------------------ |
| Backend module | `apps/backend/app/base_modules/FileManager/` |
| Backend storage config | `apps/backend/config/drive.ts` |
| Backend S3 adapter | `apps/backend/app/base_modules/FileManager/services/storage_service/s3_storage_service.ts` |
| Frontend module | `apps/admin/src/core/FileManager/` |
| Frontend file assets | `apps/admin/public/media/files/` |
| Frontend routes | `apps/admin/src/core/FileManager/config/routing.tsx` |
## Documentation
| Page | Purpose |
| ------------------------- | ---------------------------------------------------------------------------------- |
| [Backend](./backend.md) | API routes, services, storage adapter contract, permissions, and extension points. |
| [Frontend](./frontend.md) | UI structure, React Query and MobX, cache updates, pages, and widgets. |
## Main Capabilities
- Browse, filter, sort, paginate, and search folders and files.
- Preview details and text content; stream raw files.
- Create folders; upload with progress and cancellation; rename, delete, duplicate entries.
- Image editor and image map zones for supported image files.
- Reusable picker components for choosing files and inserting them into the RichText editor.
- Local filesystem or S3-compatible storage through Adonis Drive.
## High-Level Flow
```txt
Admin route
-> FileManager frontend page
-> React Query API hook
-> /file-manager backend route
-> FileManagerController
-> FileManagerService
-> StorageService adapter
-> Adonis Drive fs or s3 disk
```
## Data Contract
Backend responses use `Entry` objects:
```ts
type Entry = FileEntry | FolderEntry;
type FileEntry = {
type: "file";
name: string;
path: string;
url: string;
size?: number;
updatedAt?: string;
};
type FolderEntry = {
type: "folder";
name: string;
path: string;
updatedAt?: string;
};
```
The frontend maps DTOs into frontend entities and adds UI-oriented metadata such as `parent` and
`extension`.
## Ownership Rules
- Backend owns storage, validation, authorization, path safety, and API response shape.
- Frontend owns navigation, selection, view settings, operation progress, and user interactions.
- React Query owns server state in the frontend.
- MobX stores own local UI and operation state in the frontend.
- Storage adapters must implement `storage_service_interface.ts`; callers should depend on that
interface, not a concrete disk implementation.
- The active storage adapter is selected by `DRIVE_DISK`.
## Common Change Guide
| Change | Start Here |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Add or change an API route | `apps/backend/app/base_modules/FileManager/routes.ts` and `controller/file_manager_controller.ts` |
| Change path validation or naming rules | `services/path_service.ts` |
| Change list filtering, sorting, or pagination | `services/entry_list_service.ts` and frontend query payloads |
| Add or change a storage backend | `services/storage_service/storage_service_interface.ts`, `create_storage_service.ts`, and [Backend storage docs](./backend.md#storage) |
| Add a frontend operation | `apps/admin/src/core/FileManager/data/queries/` and relevant store/widget |
| Add file type icons | `apps/admin/public/media/files/` and `shared/file/fileTypeImportMap.ts` |
| Add preview support | Backend `file_preview_service.ts` and frontend `view/ui/FilePreview/` |
## Planned Work
- Add more non-filesystem storage adapters such as:
- Google Cloud Storage
- Supabase Storage
- Digital Ocean Spaces
- Cloudflare R2
- Add testing notes once module-level backend and frontend tests are established.
- Add libs for rendering large lists.
+110
View File
@@ -0,0 +1,110 @@
# File Manager Backend
Authenticated file operations under `/file-manager`: validation, authorization, path normalization, orchestration, with raw storage delegated to a storage adapter. Source: `apps/backend/app/base_modules/FileManager/`.
## Request Flow
```txt
routes.ts
-> controller/file_manager_controller.ts
-> validator/*
-> policy/file_manager_policy.ts
-> services/file_manager_service.ts
-> services/storage_service/ (interface -> factory -> fs | s3 adapter)
-> Adonis Drive
```
The controller stays thin (auth checks, validation, path extraction, status codes, response shape). `FileManagerService` owns orchestration; storage adapters own raw persistence.
## Routes
All prefixed with `/file-manager`, behind `middleware.auth()`; controller methods check policy abilities (`policy/file_manager_policy.ts`) before doing work. When adding an operation, add or reuse an explicit ability first.
| Method | Route | Controller Method | Ability | Purpose |
| -------- | ------------------ | ----------------- | ---------- | ------------------------------------------------------- |
| `GET` | `/` and `/*` | `index` | `viewList` | List entries for root or a nested directory. |
| `GET` | `/files/details/*` | `show` | `view` | Return file metadata and optional text preview content. |
| `GET` | `/files/raw/*` | `raw` | `view` | Stream raw file content. |
| `GET` | `/search?q=value` | `search` | `viewList` | Search entries recursively. |
| `POST` | `/folders` | `createFolder` | `create` | Create a folder. |
| `POST` | `/files` | `upload` | `create` | Upload a file. |
| `POST` | `/files/duplicate` | `duplicate` | `create` | Duplicate one or more **file** entries. |
| `PUT` | `/entries/rename` | `rename` | `update` | Rename or move an entry within its parent path. |
| `DELETE` | `/*` | `destroy` | `delete` | Delete an entry. |
## Validation
Validators in `validator/`: `list_validator.ts` (filter, sort, order, max results, pagination token), `create_directory_validator.ts`, `rename_entry_validator.ts`, `upload_file_validator.ts`, `duplicate_entries_validator.ts`, plus shared helpers in `schemas.ts`.
Path extraction from wildcard routes goes through `helpers/get_validated_path_from_request.ts` — never parse wildcard paths in controllers or storage adapters.
## Services
| Service | Owns |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `file_manager_service.ts` | Orchestration: listing with filter/sort/pagination, file detail + preview composition, moves, unique upload and copy names, search, storage calls. |
| `path_service.ts` | Client↔storage path conversion, safe joins, dirname/basename, extension metadata, filename sanitizing, unique names. Use it instead of string manipulation. |
| `entry_list_service.ts` | List behavior on raw storage entries: category filter, sort, folders-first, pagination + token encoding. Frontend list controls must stay compatible with its accepted values. |
| `file_preview_service.ts` | Text preview content for previewable extensions (defined in the same file). For rich previews, keep the response small and stream via `/files/raw/*`. |
| `entry_search_service.ts` | Path search including matching parent folders for context. Receives an abort signal from the controller — long searches should respect it. |
## Storage
Abstractions in `services/storage_service/`:
- `storage_service_interface.ts` — the adapter contract: list (flat and recursive), existence, metadata, read/stream/write, create directory, move, delete, copy. Services call the interface, never a concrete adapter.
- `fs_storage_service.ts` — filesystem adapter over the Adonis Drive `fs` disk: maps filesystem objects to entries, handles streams and metadata, preserves directory marker behavior.
- `s3_storage_service.ts` — S3-compatible adapter over the `s3` disk: maps objects and common prefixes to entries, uploads multipart files by streaming the temp file via `putStream`, creates folders with directory marker objects, moves folders by moving every object under the prefix, deletes files exactly and folders by prefix, copies file entries. S3 has no native directories, so it shares the `.keep` marker convention. **Folder operations must use slash-terminated prefixes** — deleting `test/` must not delete `test123.txt`.
- `fs_directory_marker_service.ts` — encapsulates the `.keep` marker convention; never duplicate marker logic outside it.
- `create_storage_service.ts` — factory; the active adapter (`fs` or `s3`) is selected by `DRIVE_DISK`.
## Drive Configuration
`apps/backend/config/drive.ts` configures two disks: `fs` (local development, filesystem storage) and `s3` (S3-compatible object storage, including local MinIO).
S3 environment variables:
| Variable | Local Docker Value | Purpose |
| ----------------------- | --------------------------------------------------------------------- | --------------------------------------------- |
| `AWS_ACCESS_KEY_ID` | `minioadmin` | S3 access key. |
| `AWS_SECRET_ACCESS_KEY` | `minioadmin` | S3 secret key. |
| `AWS_REGION` | `us-east-1` | S3 region. |
| `S3_BUCKET` | `crux-drive` | Bucket used by the file manager. |
| `S3_ENDPOINT` | `http://minio:9000` in Docker, `http://localhost:9000` outside Docker | S3 API endpoint. |
| `S3_CDN_URL` | `http://localhost:9000/crux-drive/` | Browser-facing public URL base for file URLs. |
For MinIO the disk uses `forcePathStyle: true` and `supportsACL: false`; bucket public-read is handled by the Docker bucket setup container.
## Local S3 Simulation
`docker-compose.yml` runs `minio` (the S3 server) and `minio_create_bucket` (one-shot setup that creates the bucket and enables anonymous download — it exits after setup, which is normal). Console at `http://localhost:9001`, credentials `minioadmin` / `minioadmin`. After dependency or Dockerfile changes: `docker compose build --no-cache && docker compose up`.
## Response Shape
```ts
interface EntryBase {
name: string;
path: string;
updatedAt?: string;
}
interface FileEntry extends EntryBase {
type: "file";
size?: number;
url: string;
}
interface FolderEntry extends EntryBase {
type: "folder";
}
```
List responses: `{ data: Entry[]; meta: { nextPaginationToken: string | null } }`.
## Backend Change Checklist
1. Confirm the policy ability for the operation.
2. Add or update a validator for all request input.
3. Keep client paths and storage paths separated through `path_service.ts`.
4. Update the frontend DTO or mapper if the response shape changes.
5. Update this documentation when routes, permissions, storage contracts, or response shapes change.
+123
View File
@@ -0,0 +1,123 @@
# File Manager Frontend
Admin file manager UI: browsing, previews, uploads, delete/duplicate/rename, image editing and image map tools, settings, and reusable file pickers. Source: `apps/admin/src/core/FileManager/`.
**Main principle: React Query owns server state** (entries, file details, raw blobs, search, mutations); **MobX owns local UI and operation state** (active/selected/expanded paths, filters and sort, dialog state, upload/delete progress, loaded-entry lookup).
## Directory Structure
```txt
FileManager/
config/ Routes, menu entries, messages, translations.
data/
api/ Raw HTTP methods and DTOs.
hooks/ Feature hooks that combine queries with stores.
mappers/ DTO-to-entity mapping.
queries/ React Query hooks, keys, mutations, and cache helpers.
stores/ MobX local state stores.
entities/ Domain entities and settings types.
providers/ Feature providers and context hooks.
pages/ Route-level screens.
shared/ FileManager utilities that do not depend on view code.
view/ UI components, widgets, forms, and layouts.
```
## Routes
Module mounted at `/file-manager/*`, configured in `config/routing.tsx`. Menu groups: `Drive` (browsing, viewer), `Tools` (image editor, image map), `Settings`.
| Route | Page | Purpose |
| -------------------------------------- | ------------------- | ------------------------------------------- |
| `/file-manager/drive/explorer/*` | `DriveExplorerPage` | Browse folders and files. |
| `/file-manager/drive/viewer/-/*` | `DriveViewerPage` | View file details and preview. |
| `/file-manager/drive/settings` | `DriveSettingsPage` | Configure file-manager UI settings. |
| `/file-manager/tools/image-editor/-/*` | `ImageEditorPage` | Edit image files. |
| `/file-manager/tools/image-map/-/*` | `ImageMapPage` | Create and edit image map areas for images. |
Build file-manager URLs with the helpers in `shared/routing/driveLinks.ts`, `shared/routing/openEntryNewTab.ts`, `shared/path/leadingSlash.ts` — never manually.
## API Boundary
Raw backend calls are centralized in `data/api/api.ts` + `data/api/dto.ts`, calling the routes documented in [backend.md](./backend.md):
| API Method | Backend Route |
| ----------------- | ------------------------------------ |
| `getEntries` | `GET /file-manager/*` |
| `getFile` | `GET /file-manager/files/details/*` |
| `getRawFile` | `GET /file-manager/files/raw/*` |
| `createDirectory` | `POST /file-manager/folders` |
| `renameEntry` | `PUT /file-manager/entries/rename` |
| `removeEntry` | `DELETE /file-manager/*` |
| `uploadFile` | `POST /file-manager/files` |
| `duplicateFiles` | `POST /file-manager/files/duplicate` |
| `searchEntries` | `GET /file-manager/search` |
DTOs are mapped before reaching UI code (`entities/entry.ts`, `data/mappers/entryMapper.ts`).
## Data Flow
- **Reading entries** — `useEntries()` builds the query payload from `ExplorerStore`, fetches via React Query (`useGetEntries`), and syncs loaded entries into `EntriesStore`. `EntriesStore` is not the fetch owner — it is an index of loaded entries for the sidebar tree, selection, active-entry lookup, and operation state.
- **Deep links** — for a nested folder opened directly, `useEntries()` preloads ancestor folders so the sidebar tree and entry index work for paths like `/folder/subfolder/current`.
- **Mutations** — `useCreateDirectory`, `useRenameEntry`, `useDeleteEntry`, `useDuplicateFiles`, `useUploadFile`. After success, the UI updates the local cache and stores directly instead of invalidating entries queries — no refetch storm when many items are created, deleted, or uploaded in sequence.
## Query Cache Updates
`data/queries/cache.ts` updates existing React Query cache data in memory (no network request):
```ts
addEntriesToCache(parentPath, entries); // filter-aware insertion into the parent folder's cached lists
removeEntriesFromCache(paths); // removal from all cached entry pages
renameEntryInCache(prevPath, renamedEntry);
```
Tradeoff: since the module avoids refetching, exact backend sort order may drift after local insertion until the folder is naturally refetched.
## Stores
All in `data/stores/`:
| Store | Owns |
| ---------------------- | ------------------------------------------------------------------------------------------------------ |
| `EntriesStore` | Index of loaded entries by path, child/selected/active lookup, local remove and rename. Never fetches. |
| `ExplorerStore` | Active path, selected paths, expanded tree paths, breadcrumbs, filter, sort. |
| `DriveDialogStore` | Create-directory and rename dialogs, entry selected for rename. |
| `UploadOperationStore` | Upload queue, progress, retry/remove/abort, operation card visibility. |
| `DeleteOperationStore` | Delete queue, progress, retry/remove, operation card visibility. |
| `SettingsStore` | Persisted view mode, compact mode, sidebar visibility, grid size. |
Operation stores receive the React Query mutation function from the view layer and manage only operation state.
## Providers
`DriveProvider` creates and exposes the feature stores — `const { entriesStore, explorerStore } = useDrive();`. `SettingsProvider` owns persisted settings.
## Pages and Widgets
- `DriveExplorerPage` — main browser: `DriveProvider`, toolbar, content, sidebar, footer, operation watcher.
- `DriveViewerPage` — details and preview, via `useGetFile()`.
- `DriveSettingsPage` — persisted UI preferences.
- `ImageEditorPage` / `ImageMapPage` — use `useGetFile()` + `useGetRawFile()` (editor also `useUploadFile()`). Config and helpers live in `view/widgets/ImageEditor/` so `view` code never imports from `pages`.
- `FilePickerDialog` — reusable picker widgets in `view/widgets/FilePickerDialog/`; keep picker behavior feature-scoped and pass reusable UI state through props/context instead of coupling low-level components to page-level stores.
## View Layer Rules
Feature widgets may use `useDrive()` and `useSettings()`. Lower-level reusable components (`ExplorerFileEntryGrid`, `FileManagerFileIcon`, `FilePreview`, …) stay prop-driven. Never import backend DTOs into view components — views consume frontend entities and props.
## File Types and Icons
Helpers in `shared/file/`; icons in `apps/admin/public/media/files/`. To add an icon: add the asset there, update `shared/file/fileTypeImportMap.ts`, and confirm `FileManagerFileIcon` falls back correctly for unknown extensions.
## Adding a Query or Mutation
1. Add/reuse a method in `data/api/api.ts`.
2. Query: add a key in `data/queries/keys.ts` and a query file in `data/queries/`, mapping DTOs to entities before returning. Mutation: add a mutation hook in `data/queries`, call `mutateAsync` from the consuming widget or operation store.
3. Update `EntriesStore` if lookup state changes; update `data/queries/cache.ts` if the visible cache must change.
4. Never import MobX stores or providers inside `data/queries`. Don't invalidate entries queries per operation unless a full refetch is intentional.
## Frontend Change Checklist
1. Is the data server state or local UI state?
2. Backend calls stay in `data/api`; React Query hooks stay free of MobX provider imports.
3. Map DTOs before view components.
4. Update cache helpers when mutation results should appear immediately.
5. Update backend docs if the change requires a backend contract change.
+46
View File
@@ -0,0 +1,46 @@
# Permission Module
The Permission module provides frontend permission checks and gates. It also corresponds to the backend permission endpoint and permission discovery service used by AccessControl.
## Source Map
| Area | Path |
| -------------------------- | -------------------------------------------------------- |
| Frontend module | `apps/admin/src/core/Permission/` |
| Frontend gates | `apps/admin/src/core/Permission/view/gates/` |
| Frontend hook | `apps/admin/src/core/Permission/hooks/usePermissions.ts` |
| Frontend types | `apps/admin/src/core/Permission/entities/permission.ts` |
| Backend module | `apps/backend/app/base_modules/Permission/` |
| Backend permission service | `apps/backend/app/services/permission_service.ts` |
## Documentation
| Page | Purpose |
| ------------------------- | ------------------------------------------------------------------ |
| [Frontend](./frontend.md) | Permission types, hook, gates, and usage rules. |
| [Backend](./backend.md) | Permission endpoint, permission discovery, and permission configs. |
## Main Capabilities
- Check whether current user has required permission slugs.
- Support `all` and `any` matching modes.
- Render or hide UI actions through `PermissionGate`.
- Protect full pages through `PermissionPageGate`.
- Treat super admin as an all-permissions user.
## High-Level Flow
```txt
AuthProvider current user
-> AuthUser.permissions
-> usePermissions()
-> hasPermission()
-> PermissionGate or PermissionPageGate
```
## Ownership Rules
- Permission frontend owns permission checks and gate components.
- Auth owns loading current user permissions.
- AccessControl owns managing roles and assigning permissions.
- Backend modules own their own permission definitions in `config/permissions.ts`.
+77
View File
@@ -0,0 +1,77 @@
# Permission Backend
The backend Permission module exposes available permission definitions. It is used by AccessControl when assigning permissions to roles.
Source: `apps/backend/app/base_modules/Permission/`
## Route
| Method | Route | Controller Method | Auth Required | Purpose |
| ------ | ------------- | ----------------- | ------------- | -------------------------------------------- |
| `GET` | `/permission` | `index` | Yes | Return all available permission definitions. |
Route file:
```txt
apps/backend/app/base_modules/Permission/routes.ts
```
## Request Flow
```txt
GET /permission
-> auth middleware
-> PermissionController.index
-> getAllPermissions()
-> scan backend module permission configs
-> return PermissionItem[]
```
## Permission Discovery
Permission discovery lives in:
```txt
apps/backend/app/services/permission_service.ts
```
`getAllPermissions()` scans:
```txt
apps/backend/app/base_modules/*/config/permissions.ts
apps/backend/app/modules/*/config/permissions.ts
```
Each config exports permission items:
```ts
type PermissionItem = {
slug: string;
name: string;
};
```
## Current Permission Configs
Core permission definitions currently include:
```txt
apps/backend/app/base_modules/Admin/config/permissions.ts
apps/backend/app/base_modules/Role/config/permissions.ts
apps/backend/app/base_modules/FileManager/config/permissions.ts
```
When adding a feature that needs access control, add permission definitions to the owning backend module's `config/permissions.ts`.
## Admin Permission Slugs
`getAdminPermissionSlugs(admin)` loads an admin's roles and returns a set of permission slugs from `role_permissions`.
This function is used by backend policies to authorize operations.
## Ownership Rules
- Backend modules own their own permission definitions.
- Permission service owns discovering all available permission definitions.
- AccessControl owns assigning permission slugs to roles.
- Auth owns returning the current user's permission slugs to the frontend.
+98
View File
@@ -0,0 +1,98 @@
# Permission Frontend
The frontend module provides reusable permission-checking primitives for the admin app.
Source: `apps/admin/src/core/Permission/`
## Directory Structure
```txt
Permission/
config/ Static module config and messages.
entities/ Permission types.
hooks/ Current-user permission hook.
lib/ Pure permission check function.
view/gates/ React gate components.
```
## Types
Permission types live in:
```txt
entities/permission.ts
```
Key concepts:
```ts
type PermissionCheckMode = "all" | "any";
type RequiredPermissions = string | string[];
```
## Hook
Current-user permission access:
```txt
hooks/usePermissions.ts
```
The hook reads auth state from AuthProvider and returns:
```ts
permissions;
isAdmin;
loading;
```
`isAdmin` is based on the configured super admin id.
## Pure Permission Check
Permission check logic:
```txt
lib/hasPermission.ts
```
Behavior:
- Super admin passes all checks.
- `all` mode requires every permission.
- `any` mode requires at least one permission.
- Empty required permissions should be treated carefully at call sites.
## Gates
### `PermissionGate`
Use for conditional UI controls such as buttons and menu items.
```tsx
<PermissionGate requiredPermissions="admins.update">
<Button>Edit</Button>
</PermissionGate>
```
### `PermissionPageGate`
Use for route-level access protection.
```tsx
<PermissionPageGate requiredPermissions={["admins.view", "role.view"]}>
<AdminListPage />
</PermissionPageGate>
```
If access is denied, it renders the provided fallback or a 403 page.
## Translations
The module has a local message bundle for consistency:
```txt
config/messages.ts
config/translation/en.json
config/translation/uk.json
```
+65
View File
@@ -0,0 +1,65 @@
# Web Manager
The CMS behind the public website. Editors assemble localized pages from ready-made blocks in the
admin panel; the public Next.js site renders what they publish.
| Part | Source | Owns |
| ---------------------- | ------------------------------------------- | -------------------------------------------------------- |
| Backend (`WebManager`) | `apps/backend/app/base_modules/WebManager/` | Data, validation, draft/publish transactions, APIs |
| Admin (`Web Studio`) | `apps/admin/src/core/WebStudio/` | Editor UI, drafts, publishing actions, preview, settings |
| Public site | `apps/web/web-runtime/`, `apps/web/app/` | Server rendering, layouts, blocks, caching, SEO |
## Documentation
| Page | Purpose |
| ------------------------------- | ------------------------------------------------------------------ |
| [Backend](./backend.md) | Data model, HTTP API, query budget, tuning constants. |
| [Admin](./admin.md) | Web Studio: routes, editor, config files, UI conventions. |
| [Web](./web.md) | Public site rendering, layouts, hero, SEO, degradation. |
| [Integration](./integration.md) | Contracts, cross-app flows, caching, env wiring, extending, tests. |
## For Editors
A page is a title, an optional hero, an ordered list of blocks, and SEO fields. Blocks are the
prebuilt sections — text columns, FAQ, media, banner, article list — so building a page is picking
blocks and filling them in, not writing markup.
| Term | What it means |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **Page** | One address in one language, e.g. `/en/about-us`. |
| **Group key** | Ties the same page across languages together (`about-us` links `/en/about-us` and `/uk/pro-nas`) so the site can offer a language switch. |
| **Page type** | Landing, Blog, or Blog item. Decides the layout and which blocks are offered. |
| **Hero** | The opening section. Four intensities: none, low, middle, high. |
| **Draft** | Your working copy. Never visible to visitors. |
| **Published** | Live on the site. |
| **Unpublished** | Taken down, but kept — content, blocks and address stay reserved, so you can put it back. |
| **Preview** | A private, time-limited link that renders the draft exactly as the site would. |
| **Site settings** | Header menus, footer, and legal links — shared by every page in one language. |
Everything you edit is a draft. A live page keeps serving the old version until you press Publish,
so an unfinished edit can never reach visitors. Nothing is deleted unless you press Delete.
## Page Lifecycle
```txt
create publish
(none) ────────► draft ───────────► published ◄──────┐
│ │ │
discard│ │unpublish │publish
▼ ▼ │
deleted unpublished ───────┘
published + open draft clone: visitors see the published row, the editor edits the clone
```
Every write happens in one transaction with rows locked `FOR UPDATE`; routes and permissions are in [backend.md](./backend.md), purge behavior in [integration.md](./integration.md#caching-and-revalidation).
- **Create** — `createDraft` validates the payload, asserts the `locale` + `path` is free across every root row (any status), and inserts the page with `status = draft` plus its components. Nothing public yet.
- **Save a draft** — `ensureDraft` edits a `draft` or `unpublished` page in place; for a `published` page it reuses or creates the draft clone. Components are replaced wholesale in the submitted order; a clone inherits its original's address.
- **Edit a published page** — the clone is a second row with `draft_of_id` pointing at the original (one clone per page, enforced by a partial unique index). Edit always opens the clone; the original keeps serving until the clone is published.
- **Preview** — a signed token (HMAC over `APP_KEY`, TTL 900s clamped to 60–3600s) lets the web app render the draft in an iframe sheet: `force-dynamic`, `noindex`, disallowed in robots. Absent, tampered, or expired tokens render "preview unavailable".
- **Publish** — publishes the **last saved** draft (the confirmation dialog says so). A pending clone is always preferred, so publishing can never drop the editor's work: a root draft goes live directly; a clone is folded into its original and deleted. A _structural_ change (first publish, republish, or changed locale/path/title/group key/page type) purges the whole cache tag; anything else purges only the affected page tags.
- **Unpublish** — same permission as publish; only valid on a published page (else 409). Sets `status = unpublished`, keeping content, components, and any open clone; the address stays reserved. Full purge — the URL now 404s.
- **Republish** — publish again; an unpublished page is edited in place, and republishing counts as structural.
- **Discard a draft** — deletes the draft row and its components; a clone's original is untouched. No purge.
- **Delete** — the only destructive action, behind a confirmation dialog. Components and any open clone cascade with the row; full purge if it was published.
+79
View File
@@ -0,0 +1,79 @@
# Web Manager — Admin (Web Studio)
Admin side of [Web Manager](./Readme.md). Source: `apps/admin/src/core/WebStudio/`.
## Routes
```txt
/web-studio/pages/listing list, search, filters, row actions
/web-studio/pages/-/create new draft
/web-studio/pages/-/:id editor (page or draft clone)
/web-studio/settings/:section header | footer | legal
```
The list shows title with full path, locale, slug, group key, page type, status, draft marker and
updated time; search is debounced and filters, pagination and locale live in the query string
(`useWebStudioQueryParams`), so links and the back arrow keep the editor's context. Row actions are
Edit, Preview, Open in web app, Publish, Unpublish, Discard and Delete — Edit opens the draft clone
when `draftId` is present, so an editor always lands on the working copy.
## Editor
The editor is a flat two-column layout: a sticky ~320px Page details column, and Hero / Blocks / SEO
as line tabs beside it. One React Hook Form instance (`PAGE_EDITOR_FORM_ID`) owns everything;
the header's Save button submits it by form id, and submitting disables the whole fieldset.
Actions sit in a split button: Save draft, then Preview / Publish / Unpublish / Discard / Delete in
the dropdown, each behind its own control component in `view/controls/` and its own permission.
Publish is offered whenever the page is not already live, so it doubles as republish.
Blocks are added from a sheet filtered to the page type's `allowedBlockTypes`, reordered by drag
(`view/components/SortableList.tsx`), and duplicated or removed from their own header. Nested
collections — content columns, CTAs, FAQ items, navigation links — behave the same way one level
down.
## Key Files
| Concern | File |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| Module registration | `config/module.ts`, `config/routing.tsx`, `config/menu.ts` |
| Languages | `config/languages.ts` — code, labels, direction, public prefix |
| Page types | `config/pages.ts` — allowed blocks and default path per type |
| Block catalogue | `config/blocks.ts` — label, description, preview image, factory |
| Settings shape | `config/siteSettings.ts` |
| HTTP + multipart | `data/api/WebStudioAPI.ts`, `data/api/PagePayloadSerializer.ts` |
| Domain types | `entities/` — `PageType`, `PageStatus`, `BlockType`, `HeroType`, block and field shapes |
| Form ↔ DTO | `pages/PageEditPage/helpers/toEditorValues.ts`, `view/forms/PageEditorForm/helpers/toCreatePayload.ts` |
Page types decide what an editor can build:
| Type | Layout | Blocks offered | Default path |
| ----------- | ---------- | --------------------------------------------------- | ------------ |
| `landing` | Standard | all six | `/` |
| `blog` | Blog index | none — the listing is rendered from published posts | `/blog` |
| `blog-item` | Editorial | all except `articles` | `/` |
## UI Conventions
The module is a content tool, not a dashboard of cards. Flat surfaces, few separators, one primary
action per screen.
| Element | Treatment |
| ------------------- | -------------------------------------------------------- |
| Screen heading | `text-mono text-xl font-semibold` |
| Supporting copy | `text-muted-foreground text-sm` |
| Editor settings | `bg-muted/40 space-y-5 p-5` |
| Top-level block | `bg-muted/35`, compact header, one `border-b` while open |
| Nested field group | `border-l-2 pl-4` (`view/components/FieldCard.tsx`) |
| Inline actions | small ghost icon buttons |
| Destructive trigger | ghost button, destructive text, confirmation dialog |
| Empty collection | `bg-muted/35`, generous padding, short copy |
`view/components/BlockCard.tsx` is named for compatibility but is a flat section, not a card — no
shadow, no hover lift, no bordered container nested inside every block. The block picker stays a
sheet with a divided list, because picking is a temporary task. Shared visual principles live in the
[Admin UI Style Guide](../../../apps/admin/ui-style-guide.md); style Web Studio from Web Studio and do
not edit `apps/admin/src/shared` for a module-only visual change.
A visual change must not alter routes, `locale` / `group_key` query behavior, list filtering or DTO
mapping, draft-clone semantics, form values or validation, block factory output, settings payload
shape, or the toast / error / loading states. Anything that does is a feature change.
+127
View File
@@ -0,0 +1,127 @@
# Web Manager — Backend
Backend of [Web Manager](./Readme.md). Source: `apps/backend/app/base_modules/WebManager/`.
Lifecycle mechanics: [Page Lifecycle](./Readme.md#page-lifecycle). Cross-app flows: [Integration](./integration.md).
## Data Model
### `web_pages`
```txt
id
status -- draft | published | unpublished
draft_of_id -- nullable FK to web_pages, ON DELETE CASCADE
locale -- en, uk, de
path -- '' for home, else 'about-us' or 'blog/post/about-me' (no leading slash)
group_key -- links localized versions of the same page
page_type -- landing | blog | blog-item
title
hero -- jsonb, nullable
seo_title -- defaults to ''
seo_description
published_at -- nullable
created_at / updated_at
```
Indexes: `(group_key, status)`, `(locale, path, status)`, a partial unique index on
`(locale, path) WHERE status = 'published'`, and a partial unique index on
`(draft_of_id) WHERE draft_of_id IS NOT NULL` — one draft clone per page, enforced by the database.
Rules:
- `path` is the full route after the locale, without leading or trailing slashes: `/en/blog/post/x`
is `locale = en`, `path = blog/post/x`. The editor form shows it with a leading slash.
- Public reads only ever match `status = published`.
- A root row is `draft_of_id = null`; a draft clone points at its published original.
- Route uniqueness is enforced twice: the partial index for live collisions, and
`assertNewRouteAvailable` for root rows of any status, so an unpublished page keeps its address.
### `web_components`
```txt
id
page_id -- FK to web_pages, ON DELETE CASCADE
type -- content | faq | media-content | articles | banner | media
data -- jsonb, block-specific shape validated by validator/web_content_validator.ts
sort_order -- position in the page, index of the submitted array
created_at / updated_at
```
Indexed on `(page_id, sort_order)`. Saving a draft deletes and reinserts the whole set rather than
diffing it.
### `web_settings`
```txt
id
locale -- unique
header -- jsonb { menus: [{ title, items }], ctaItems }
footer -- jsonb { copyright, menus }
legal -- jsonb { privacyPath, termsPath }
created_at / updated_at
```
One row per locale, upserted by locale. Shape lives in `WebStudio/config/siteSettings.ts`.
## HTTP API
Admin routes are under `/web-manager` behind `middleware.auth()`; every action authorizes through
`WebManagerPolicy`.
| Method | Route | Permission | Purpose |
| ------ | -------------------------------------- | ---------------------- | ------------------------------------------- |
| GET | `/web-manager/pages` | `web_manager.view` | Paginated root rows with a `draft` relation |
| GET | `/web-manager/pages/group-keys` | `web_manager.view` | Distinct group keys, alphabetized |
| POST | `/web-manager/pages` | `web_manager.create` | Create a draft (multipart) |
| GET | `/web-manager/pages/:id` | `web_manager.view` | Page + components |
| PUT | `/web-manager/pages/:id/draft` | `web_manager.update` | Save a draft (multipart) |
| POST | `/web-manager/pages/:id/publish` | `web_manager.publish` | Publish, returns `meta.revalidation` |
| POST | `/web-manager/pages/:id/unpublish` | `web_manager.publish` | Take down, returns `meta.revalidation` |
| POST | `/web-manager/pages/:id/preview-token` | `web_manager.view` | Signed preview token |
| DELETE | `/web-manager/pages/:id` | `web_manager.delete` | Delete page or discard draft |
| GET | `/web-manager/settings` | `web_manager.view` | Settings, all locales or one |
| PUT | `/web-manager/settings` | `web_manager.settings` | Upsert settings for a locale |
Public routes are under `/web-manager/public`, unauthenticated, and return published content only —
except `preview`, which is gated by the signed token.
| Route | Returns |
| ------------------------------------ | ----------------------------------------------------------------------------------- |
| `/pages/resolve?locale=&path=` | Page, components, settings, breadcrumbs, `languageAlternatives` |
| `/pages/preview?token=` | The same shape for any status, for a valid token |
| `/pages/sitemap?page=&perPage=` | `locale`, `path`, `groupKey`, `pageType`, `publishedAt`, `updatedAt` + `pagination` |
| `/blog/pages?locale=&page=&perPage=` | Published `blog-item` pages, newest first, with hero media |
| `/settings?locale=` | Header, footer, legal for one locale |
The sitemap is the one public read proportional to the site, so it is paginated
(`SITEMAP_PAGE_SIZE` 1000, capped at `SITEMAP_MAX_PAGE_SIZE` 5000) and the caller walks it;
`groupKey` is included so hreflang alternates need no second lookup. The resolve payload carries
`publishedAt` / `updatedAt`, which the web app emits as `datePublished` / `dateModified`.
Permissions are seeded from `WebManager/config/permissions.ts`: `web_manager.view`, `.create`,
`.update`, `.publish`, `.delete`, `.settings`.
## Public Resolve Query Budget
| Query | Bounded by |
| --------------------- | ----------------------------------------------------------- |
| the page row | route lookup |
| its components | blocks on the page |
| site settings | one row per locale |
| language alternatives | locales sharing the `group_key` |
| breadcrumbs | path depth (~4–6 rows), plus one query per group-key jump |
| internal page links | internal links in the page's own content, skipped when none |
Two details keep that true. **Link map by id**: `collectInternalPageIds` walks hero, components and
settings first, then `createPageLinkMap` fetches only those ids — unknown or unpublished ids are
absent and render as an empty href. **Breadcrumbs by candidate**: the walk can only climb to a
shorter prefix of the current path, the home page, or the group key in
`BREADCRUMB_PAGE_TYPE_PARENTS`, so `buildBreadcrumbs` asks for precisely those rows; a second query
happens only when a group-key jump lands on a branch whose ancestors were not already loaded
(`ancestorsAlreadyLoaded`). Both are served by the existing indexes.
## Tuning Constants
Backend tuning constants live in `WebManager/config/general.ts`: media upload path and 10 MB size
limit, preview token TTLs, sitemap page sizes, and `BREADCRUMB_PAGE_TYPE_PARENTS` (which makes
`blog` the breadcrumb parent of every `blog-item`).
+185
View File
@@ -0,0 +1,185 @@
# Web Manager — Integration
How the three apps of [Web Manager](./Readme.md) connect: contracts, flows, caching, env wiring, and cross-app extension checklists.
## How the Three Apps Connect
```txt
┌───────────────────────┐ ┌──────────────────────────┐
│ Admin (Web Studio) │───────►│ Backend (AdonisJS) │
│ editor, drafts, │◄───────│ pages, components, │
│ publish, preview │ DTOs │ settings, transactions │
└───────────┬───────────┘ └────────────┬─────────────┘
│ │
│ iframe /preview?token=… │ POST /api/revalidate (tag purge)
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ Public site (Next.js) │
│ server-renders GET /web-manager/public/*, tagged 60s cache │
└─────────────────────────────────────────────────────────────┘
```
The three never share code, only contracts:
| Contract | Defined by | Consumed by |
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------- |
| Page / block payloads | `types/web_content.ts`, `validator/web_content_validator.ts` | Admin DTOs, `web-runtime/types.ts` |
| Public read API | `WebManager/routes.ts` (`/web-manager/public/*`) | `web-runtime/api/web-runtime-api.ts` |
| Cache tag names | `services/web_revalidation_service.ts` | `web-runtime/api/cache-tags.ts` — **must stay in sync** |
| Preview token | `services/preview_token_service.ts` | Admin preview sheet, `app/[locale]/preview/` |
| Block / page types | `types/web_content.ts` | `WebStudio/config/blocks.ts`, `web-runtime/*/registry` |
## Save Flow (admin → backend)
```txt
editor submits React Hook Form values
PagePayloadSerializer → FormData: `payload` JSON string + one binary part per new file
POST /web-manager/pages | PUT /web-manager/pages/:id/draft (multipart/form-data)
PagePayloadService parses `payload`, validates, stores files through FileManager,
rewrites { name, fileKey } media values into { name, ref }
WebManagerService writes page + components in one transaction
```
Media inside `hero` and block `data` is either `{ name, ref }` (already stored) or
`{ name, fileKey }` (new upload); only `ref` values are ever persisted. Part names are the
`fileKey`s (`file-0`, `file-1`, …). A request without a `payload` part is read as a plain JSON body,
so the endpoints stay usable from tests and scripts. The payload travels as one JSON string so
booleans, numbers, nulls and empty arrays survive the multipart transport.
## Publish Flow (backend → public site)
```txt
publish/unpublish/settings commit
WebRevalidationService POSTs { tags } to WEB_APP_URL/api/revalidate with X-Revalidate-Secret
Next.js revalidateTag(tag, 'max') per tag
response meta.revalidation = purged | retrying | disabled → admin toast
```
## Public Request Flow (visitor → web → backend)
```txt
GET /en/about-us
next-intl middleware resolves the locale segment
app/[locale]/[[...path]]/page.tsx → resolvePageRenderData(locale, path)
GET /web-manager/public/pages/resolve?locale=en&path=about-us (next: revalidate 60 + tags)
serializePublicPage returns page, components, settings, breadcrumbs, languageAlternatives
renderer.tsx → header + RegisteredPageLayout + footer; blocks via the block registry
```
Before it leaves the backend, `PublicContentService.prepare` resolves internal links to hrefs,
turns stored file refs into URLs, and sanitizes rich text (`content_sanitizer.ts`: allowlisted tags,
`http/https/mailto/tel` only, `rel` limited to `nofollow`/`noopener`/`noreferrer`). The web app
renders rich text as HTML and trusts that sanitization.
## Caching and Revalidation
Every public fetch is tagged and time-boxed, so a purge that never lands costs about a minute of
stale content rather than correctness.
| Fetch | `revalidate` | Tags |
| --------- | ------------ | --------------------------------------------------------------- |
| page | 60s | `web-manager`, `web-manager:page:{locale}:{path}`, settings tag |
| settings | 60s | `web-manager`, `web-manager:settings:{locale}` |
| blog list | 60s | `web-manager`, `web-manager:blog:{locale}` |
| sitemap | 3600s | `web-manager` only — it spans every locale and page |
Tag builders are duplicated in `web_revalidation_service.ts` (backend) and `cache-tags.ts` (web) and
must stay in sync; both sides have tests over the names, because a drift stops purges from landing
without anything failing.
Delivery is one attempt inline with the request (so the outcome can be reported to the editor) and
attempts 2–3 in the background with exponential backoff and jitter. Publish, unpublish and settings
save therefore return `meta.revalidation`:
| `status` | Meaning |
| ---------- | ----------------------------------------------------------------------- |
| `purged` | The web app confirmed the purge. |
| `retrying` | The first attempt failed; background retries are running. |
| `disabled` | `WEB_APP_URL` or `WEB_REVALIDATE_SECRET` is unset, so nothing was sent. |
The admin shows a warning toast on anything but `purged`, so an editor is never told the site is up
to date when it is not. Retries live in memory only — a restart drops them onto the same 60s floor a
failed purge already has. Exhausting all attempts logs at `error`; publishing again sends a fresh
purge. A deployment missing either variable logs each skipped purge at `warn` in production.
## Configuration
| App | Variable | Purpose |
| ------- | ----------------------- | ----------------------------------------------------------------------------- |
| backend | `APP_KEY` | Signs preview tokens. |
| backend | `WEB_APP_URL` | Where to POST cache purges. Server-side URL (compose service name in Docker). |
| backend | `WEB_REVALIDATE_SECRET` | Must equal the web app's `REVALIDATE_SECRET`. |
| web | `BACKEND_API_URL` | Backend base URL, without the `/web-manager/public/...` part. Required. |
| web | `REVALIDATE_SECRET` | Guards `/api/revalidate`. |
| web | `SITE_URL` | Canonical origin for metadata. Required in production. |
| admin | `VITE_WEB_URL` | Public site origin used to build preview and "view page" links. |
Backend tuning constants live in `WebManager/config/general.ts` — see
[Backend → Tuning Constants](./backend.md#tuning-constants).
## Extending
### Add a block type
1. Backend: add the value to `BLOCK_TYPES` and its data type in `WebManager/types/web_content.ts`,
and its shape to the component union in `validator/web_content_validator.ts`, so the payload is
validated server-side.
2. Admin: add the enum value to `entities/BlockType.ts`, the block shape under `entities/block/`,
an editor under `view/forms/blocks/<Name>Block/` with its `schema.ts`, and an entry in
`config/blocks.ts` (label, description, preview image, `createBlock` factory). Add it to the
`allowedBlockTypes` of every page type that should offer it in `config/pages.ts`.
3. Web: add the renderer under `web-runtime/blocks/components/` and register it in
`web-runtime/blocks/registry.tsx`, plus the type in `web-runtime/types.ts`.
4. Translations: add the label and description keys to both `config/translation/en.json` and
`uk.json` — the two files are kept at exact key parity.
`apps/web/tests/registry.test.ts` keeps a hand-mirrored copy of `BLOCK_TYPES` and `PAGE_TYPES` and
asserts the registries match it exactly — add the type there too, and it becomes the tripwire for a
block the backend can publish but the site cannot draw.
### Block preview images
Wireframe SVGs in `apps/admin/public/media/web-studio/blocks/<type>.svg`, referenced by
`previewImage` in `config/blocks.ts` and rendered by `BlockPickerSheet` as a 4:3 thumbnail. They are
schematic, not screenshots: `viewBox="0 0 400 300"`, no `width`/`height`, content inside
`x: 40–360` / `y: 32–268`, and no text — everything reads as bars and shapes at 112px wide.
| Token | Hex | Used for |
| -------------- | --------- | --------------------------------------------- |
| background | `#f7f4ec` | full-canvas rect, always the first element |
| surface | `#fffdf8` | cards, accordion rows, banner strip |
| surface border | `#d9dacf` | 1px stroke on surfaces and image placeholders |
| image fill | `#e7e9dc` | image placeholder rects |
| ink | `#1f291f` | titles, button inner bars, icons |
| text line | `#ccd1c0` | body copy bars |
| muted line | `#b9bfab` | subtitles, captions, image glyphs |
| accent | `#c7ff3f` | primary CTA pills, sun circle, active state |
| accent border | `#a8c930` | stroke marking an active or open element |
Reuse the shared vocabulary so previews read as one family: title bar (dark rect, `height 14–18`,
`rx = height/2`), body line (`height 9–10`, last line ~70% width), card (surface + border,
`rx 10–14`), CTA pill (accent, `height 24–28`, dark label bar inside), image placeholder (mountain
polygon plus accent sun). Draw the distinguishing structure only — three cards means "a list",
2–4 lines mean "a paragraph" — and keep at most one accent element per preview.
### Add a page type
Add the value to `PAGE_TYPES` in `WebManager/types/web_content.ts` and `entities/PageType.ts`, a
`WebStudioPageConfig` in `config/pages.ts` (allowed blocks, default path, label keys), a layout
component in `web-runtime/layouts/components/`, its entry in `PAGE_LAYOUT_REGISTRY`, and the
mirrored list in `registry.test.ts`. An unregistered type still renders through the standard
layout, so the site cannot break mid-rollout.
## Tests
| Suite | Command | Location | Covers |
| ------- | ---------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Backend | `npm run test:backend` | `apps/backend/tests/functional/web_manager/` | Preview token, revalidation, public content, sanitizer, breadcrumbs; page lifecycle (`web_manager_service.spec.ts`: draft/clone/publish/unpublish, route reservation, purge tags); HTTP surface (`pages_api.spec.ts`: 401/403 per permission, multipart, public endpoints). |
| Admin | `npm run test:admin` | `apps/admin/tests/unit/core/WebStudio/` | Menu, SEO helpers, list mapper, payload serializer, editor-value mapping and validation, `toCreatePayload`, block factory↔schema round-trips, field schemas, page paths, permission helper. |
| Web | `npm run test:web` | `apps/web/tests/` | Normalization, cache tags, block/layout registries, page metadata, sitemap, site URL, structured data, `safeHref`, published-only gate, `/api/revalidate`, API client, blog pager. |
Notes:
- DB-backed backend specs run against a dedicated `adonis_test` database (`.env.test`): the runner migrates up/down and each test runs in a global transaction (fixtures in `tests/utils/web_manager.ts`). `.env.test` leaves `WEB_REVALIDATE_SECRET` empty, so revalidation reports `disabled` while still exposing computed tags for assertions.
- `apps/web/vitest.config.ts` maps `#*` to the app root (Vitest ignores `tsconfig` `paths`) and stubs `server-only`, which only resolves inside a Next build.
- Not covered: rendering the block and layout components — needs jsdom and `@testing-library/react`, neither a dependency yet.
+71
View File
@@ -0,0 +1,71 @@
# Web Manager — Public Site (Next.js)
How the public site renders what [Web Manager](./Readme.md) publishes. Source: `apps/web/web-runtime/`, `apps/web/app/`.
## Structure
```txt
app/[locale]/[[...path]]/page.tsx localized catch-all
app/[locale]/preview/page.tsx token-gated draft rendering, noindex, force-dynamic
app/api/revalidate/route.ts secret-guarded tag purge
app/sitemap.ts, app/robots.ts generated from the public API
web-runtime/api/ HTTP client, render-data resolution, cache tags
web-runtime/data/ transport normalization
web-runtime/blocks/ block renderers + registry
web-runtime/layouts/ page layouts and hero
web-runtime/chrome/ header and footer
web-runtime/components/ link, rich text and media primitives
web-runtime/lib/ metadata, site URL, sitemap, structured data
web-runtime/types.ts normalized public contract
web-runtime/renderer.tsx header + layout + footer
```
Locales come from `site.config.ts` (`en`, `uk`, `de`, default `en`); with more than one locale
next-intl always prefixes the URL.
Everything degrades rather than fails. An unknown `page_type` falls back to the standard layout; an
unknown block type renders a dev-only "renderer missing" banner and nothing at all in production;
the normalizer degrades malformed payloads instead of throwing. Publishing a block type the web app
does not know yet can therefore never take the site down.
## Hero
Hero impact is an intensity choice, not a block type:
| Impact | Use for | Rendering |
| -------- | ------------------------------- | ------------------------------------------------------- |
| `none` | pages that start with content | hero omitted |
| `low` | legal, utility, index pages | compact padding, smallest title scale |
| `middle` | default marketing and content | medium padding, balanced scale |
| `high` | campaigns, launches, conversion | near-viewport height, largest scale, radial accent wash |
Media beside the hero switches it to a two-column grid at every impact. A page with no hero object
falls back to a page-type eyebrow plus the title.
## SEO
`generateMetadata` delegates to `pageMetadata` (`web-runtime/lib/page-metadata.ts`), a pure function
of the resolved render data:
- **title / description** — the SEO fields, falling back to the page title; an empty description is
omitted rather than sent blank.
- **canonical** — the page's own locale-prefixed path.
- **hreflang** — one alternate per locale in `languageAlternatives`, plus `x-default` pointing at
the default locale, or at this page when the default locale has no version. Omitted for a page
with no translations.
- **Open Graph / Twitter** — the hero image becomes the share card with width, height and alt when
supplied; `summary_large_image` with an image, `summary` without; `og:type` is `article` for
`blog-item`, `website` otherwise.
`app/sitemap.ts` walks the paginated sitemap endpoint (capped at 50 requests — the protocol's
50,000-URL limit) and hands entries to `buildSitemap`; pages sharing a `groupKey` link to each other
through `alternates.languages`, and `lastModified` uses `updatedAt` falling back to `publishedAt`.
`app/robots.ts` allows everything except `/*/preview` and advertises the sitemap.
`web-runtime/lib/structured-data.tsx` emits `BreadcrumbList` on every page with a trail and
`Article` on editorial pages — skipped entirely without a publication date rather than shipping
markup a crawler discards.
**`SITE_URL` must be set.** `metadataBase` comes from `siteUrl()`, and Next resolves every relative
metadata URL against it. Without it Next falls back to `http://localhost:3000`: the build succeeds
and pages look fine, but canonicals and share cards point at localhost. A production build without
it logs an error; a malformed value throws.
+7
View File
@@ -0,0 +1,7 @@
# Project Modules
Documentation for project-specific modules
| Module | Backend | Frontend | Documentation |
| ------ | ------- | -------- | ------------- |
| - | - | - | - |
+14
View File
@@ -0,0 +1,14 @@
# Crux Kit Packages
Reusable packages shared across CRUX applications. Each package documents itself in its own `Readme.md`.
| Package | Description |
| -------------------------------------------------------------------------- | ----------------------------------------- |
| [@crux-kit/hooks](../../packages/hooks/Readme.md) | Reusable React hooks |
| [@crux-kit/utils](../../packages/utils/Readme.md) | Common utilities for frontend and backend |
| [@crux-kit/intl-date](../../packages/services/intl-date/Readme.md) | Date formatting and localization |
| [@crux-kit/intl-name](../../packages/services/intl-name/Readme.md) | Person and organization name formatting |
| [@crux-kit/local-storage](../../packages/services/local-storage/Readme.md) | Local storage abstraction |
| [@crux-kit/keenicon](../../packages/design-system/keenicon/Readme.md) | KeenIcons integration and icon components |
New package: add a `Readme.md` in the package root (installation, usage, API) and list it here.