Scaffold pnpm monorepo with lint, format, and test tooling
pnpm workspace with apps/web, apps/api, apps/collab, and packages/shared; strict TypeScript base config, repo-wide ESLint (flat) + Prettier, Vitest per package, and root scripts lint/typecheck/test/ build. @dorfteich/shared ships a first health-response helper consumed by apps/api to prove workspace linking. Existing markdown docs are reformatted once by the new Prettier setup. Closes #1 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6517c8fb71
commit
b16d23297e
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.DS_Store
|
||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
coverage/
|
||||||
|
pnpm-lock.yaml
|
||||||
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
20
README.md
20
README.md
@ -1,7 +1,7 @@
|
|||||||
# Dorfteich
|
# Dorfteich
|
||||||
|
|
||||||
Dorfteich is an open-source wiki system built around **ponds** (German:
|
Dorfteich is an open-source wiki system built around **ponds** (German:
|
||||||
*Teiche*) — self-contained wiki spaces that people and teams organize freely
|
_Teiche_) — self-contained wiki spaces that people and teams organize freely
|
||||||
with hierarchical labels, directories, and Obsidian-style page relations.
|
with hierarchical labels, directories, and Obsidian-style page relations.
|
||||||
Pages are edited in a collaborative WYSIWYG editor with live cursors and
|
Pages are edited in a collaborative WYSIWYG editor with live cursors and
|
||||||
offline support.
|
offline support.
|
||||||
@ -34,12 +34,28 @@ offline support.
|
|||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
| Path | Contents |
|
| Path | Contents |
|
||||||
| --- | --- |
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `docs/architecture/` | Architecture documentation: ADRs, data model, permission model, collaboration and plugin concepts, deployment and operations |
|
| `docs/architecture/` | Architecture documentation: ADRs, data model, permission model, collaboration and plugin concepts, deployment and operations |
|
||||||
| `apps/` | Application packages (web frontend, API server, collaboration server) — created as implementation proceeds |
|
| `apps/` | Application packages (web frontend, API server, collaboration server) — created as implementation proceeds |
|
||||||
| `packages/` | Shared packages (types, permission logic, plugin SDK) |
|
| `packages/` | Shared packages (types, permission logic, plugin SDK) |
|
||||||
| `deploy/` | Docker Compose stacks and deployment tooling |
|
| `deploy/` | Docker Compose stacks and deployment tooling |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Requirements: Node.js ≥ 22 and [pnpm](https://pnpm.io) (`npm install -g pnpm`).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install # install all workspace dependencies
|
||||||
|
pnpm lint # ESLint + Prettier check across the repo
|
||||||
|
pnpm typecheck # TypeScript --noEmit in every package
|
||||||
|
pnpm test # Vitest in every package
|
||||||
|
pnpm build # build every package (dependency order)
|
||||||
|
```
|
||||||
|
|
||||||
|
The workspace packages live under `apps/` (web, api, collab) and
|
||||||
|
`packages/` (shared). Shared logic goes into `packages/shared` and is
|
||||||
|
imported as `@dorfteich/shared` — never copy code between apps.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
The project is in the architecture and backlog phase. Implementation stories
|
The project is in the architecture and backlog phase. Implementation stories
|
||||||
|
|||||||
18
apps/api/package.json
Normal file
18
apps/api/package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@dorfteich/api",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Dorfteich REST API server",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run --passWithNoTests"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dorfteich/shared": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
apps/api/src/index.test.ts
Normal file
9
apps/api/src/index.test.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { apiHealth } from './index';
|
||||||
|
|
||||||
|
describe('workspace linking', () => {
|
||||||
|
it('api consumes @dorfteich/shared', () => {
|
||||||
|
expect(apiHealth().service).toBe('api');
|
||||||
|
});
|
||||||
|
});
|
||||||
7
apps/api/src/index.ts
Normal file
7
apps/api/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Placeholder entry point; replaced by the NestJS bootstrap in issue #2.
|
||||||
|
// It already imports from @dorfteich/shared to prove workspace linking.
|
||||||
|
import { healthResponse } from '@dorfteich/shared';
|
||||||
|
|
||||||
|
export function apiHealth(): ReturnType<typeof healthResponse> {
|
||||||
|
return healthResponse('api', '0.0.0');
|
||||||
|
}
|
||||||
10
apps/api/tsconfig.json
Normal file
10
apps/api/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["src/**/*.test.ts"]
|
||||||
|
}
|
||||||
15
apps/collab/package.json
Normal file
15
apps/collab/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@dorfteich/collab",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Dorfteich collaboration server (Yjs/Hocuspocus)",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run --passWithNoTests"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
apps/collab/src/index.ts
Normal file
3
apps/collab/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// Placeholder entry point; the Hocuspocus server arrives with milestone M3
|
||||||
|
// (issue #33). The package exists so workspace tooling covers it from day one.
|
||||||
|
export const SERVICE_NAME = 'collab';
|
||||||
9
apps/collab/tsconfig.json
Normal file
9
apps/collab/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
15
apps/web/package.json
Normal file
15
apps/web/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "@dorfteich/web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Dorfteich single-page application",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run --passWithNoTests"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
apps/web/src/index.ts
Normal file
2
apps/web/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Placeholder entry point; replaced by the Vite + React app in issue #4.
|
||||||
|
export const APP_NAME = 'Dorfteich';
|
||||||
9
apps/web/tsconfig.json
Normal file
9
apps/web/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@ -53,7 +53,7 @@ flowchart LR
|
|||||||
### Architecture Decision Records (`adr/`)
|
### Architecture Decision Records (`adr/`)
|
||||||
|
|
||||||
| ADR | Decision |
|
| ADR | Decision |
|
||||||
| --- | --- |
|
| -------------------------------------------- | ----------------------------------------------------------------- |
|
||||||
| [0001](adr/0001-typescript-monorepo.md) | TypeScript everywhere, pnpm monorepo |
|
| [0001](adr/0001-typescript-monorepo.md) | TypeScript everywhere, pnpm monorepo |
|
||||||
| [0002](adr/0002-postgresql.md) | PostgreSQL as the only database |
|
| [0002](adr/0002-postgresql.md) | PostgreSQL as the only database |
|
||||||
| [0003](adr/0003-yjs-crdt-collaboration.md) | Yjs CRDT + Hocuspocus for real-time and offline collaboration |
|
| [0003](adr/0003-yjs-crdt-collaboration.md) | Yjs CRDT + Hocuspocus for real-time and offline collaboration |
|
||||||
@ -74,7 +74,7 @@ flowchart LR
|
|||||||
### Concept documents
|
### Concept documents
|
||||||
|
|
||||||
| Document | Contents |
|
| Document | Contents |
|
||||||
| --- | --- |
|
| ------------------------------------------------------ | ---------------------------------------------------------------------------- |
|
||||||
| [data-model.md](data-model.md) | Entities and relations: users, ponds, pages, labels, grants, quotas, plugins |
|
| [data-model.md](data-model.md) | Entities and relations: users, ponds, pages, labels, grants, quotas, plugins |
|
||||||
| [permissions.md](permissions.md) | Role model and the "most specific setting wins" resolution algorithm |
|
| [permissions.md](permissions.md) | Role model and the "most specific setting wins" resolution algorithm |
|
||||||
| [realtime-collaboration.md](realtime-collaboration.md) | CRDT document lifecycle, cursor sync, offline behavior, versioning hooks |
|
| [realtime-collaboration.md](realtime-collaboration.md) | CRDT document lifecycle, cursor sync, offline behavior, versioning hooks |
|
||||||
@ -87,7 +87,7 @@ flowchart LR
|
|||||||
## Terminology
|
## Terminology
|
||||||
|
|
||||||
| German (product vision) | English (code, docs, issues) |
|
| German (product vision) | English (code, docs, issues) |
|
||||||
| --- | --- |
|
| ----------------------- | ---------------------------- |
|
||||||
| Teich | pond |
|
| Teich | pond |
|
||||||
| Seite | page |
|
| Seite | page |
|
||||||
| Teich-Admin | Pond Admin |
|
| Teich-Admin | Pond Admin |
|
||||||
|
|||||||
@ -29,7 +29,7 @@ natural hooks for both.
|
|||||||
the current version (diff computed on the derived plain/Markdown
|
the current version (diff computed on the derived plain/Markdown
|
||||||
representation — good enough for "what changed", no structural diff UI in
|
representation — good enough for "what changed", no structural diff UI in
|
||||||
v1).
|
v1).
|
||||||
- **Restore**: restoring creates a *new* state on top of history (the
|
- **Restore**: restoring creates a _new_ state on top of history (the
|
||||||
restored content is applied as a regular update, preceded by an automatic
|
restored content is applied as a regular update, preceded by an automatic
|
||||||
"pre-restore" snapshot). History is append-only; nothing is rewritten.
|
"pre-restore" snapshot). History is append-only; nothing is rewritten.
|
||||||
- **Compaction**: to bound Yjs update-log growth, the persistence layer
|
- **Compaction**: to bound Yjs update-log growth, the persistence layer
|
||||||
@ -64,5 +64,5 @@ natural hooks for both.
|
|||||||
rejected.
|
rejected.
|
||||||
- **Storing Markdown snapshots instead of Yjs states**: smaller, but restore
|
- **Storing Markdown snapshots instead of Yjs states**: smaller, but restore
|
||||||
would lose structure/plugin blocks and break the CRDT continuity;
|
would lose structure/plugin blocks and break the CRDT continuity;
|
||||||
rejected (Markdown diffs are still used for the *display* layer).
|
rejected (Markdown diffs are still used for the _display_ layer).
|
||||||
- **Hard delete only**: data-loss risk contradicts kickoff decision.
|
- **Hard delete only**: data-loss risk contradicts kickoff decision.
|
||||||
|
|||||||
@ -32,8 +32,9 @@ erDiagram
|
|||||||
## Identity and access
|
## Identity and access
|
||||||
|
|
||||||
### `users`
|
### `users`
|
||||||
|
|
||||||
| Column | Notes |
|
| Column | Notes |
|
||||||
| --- | --- |
|
| ----------------------------- | ----------------------------------------------------------- |
|
||||||
| `id` (uuid) | |
|
| `id` (uuid) | |
|
||||||
| `username` | unique, URL-safe |
|
| `username` | unique, URL-safe |
|
||||||
| `email` | unique, stored verified/unverified with `email_verified_at` |
|
| `email` | unique, stored verified/unverified with `email_verified_at` |
|
||||||
@ -44,21 +45,25 @@ erDiagram
|
|||||||
| `created_at`, `last_login_at` | |
|
| `created_at`, `last_login_at` | |
|
||||||
|
|
||||||
### `user_identities` (ADR 0007)
|
### `user_identities` (ADR 0007)
|
||||||
|
|
||||||
`user_id`, `provider` (`password` now; `oidc:<issuer>` later), `subject`,
|
`user_id`, `provider` (`password` now; `oidc:<issuer>` later), `subject`,
|
||||||
`credential` (Argon2id hash for `password`), unique on
|
`credential` (Argon2id hash for `password`), unique on
|
||||||
(`provider`, `subject`).
|
(`provider`, `subject`).
|
||||||
|
|
||||||
### `sessions`
|
### `sessions`
|
||||||
|
|
||||||
Opaque id (hashed), `user_id`, `created_at`, `expires_at`, `last_seen_at`,
|
Opaque id (hashed), `user_id`, `created_at`, `expires_at`, `last_seen_at`,
|
||||||
user-agent summary (for "active sessions" UI).
|
user-agent summary (for "active sessions" UI).
|
||||||
|
|
||||||
### `auth_tokens`
|
### `auth_tokens`
|
||||||
|
|
||||||
Single-use tokens for e-mail verification and password reset: hashed token,
|
Single-use tokens for e-mail verification and password reset: hashed token,
|
||||||
`purpose`, `expires_at`, `consumed_at`.
|
`purpose`, `expires_at`, `consumed_at`.
|
||||||
|
|
||||||
### `role_grants` — the permission table (see `permissions.md`)
|
### `role_grants` — the permission table (see `permissions.md`)
|
||||||
|
|
||||||
| Column | Notes |
|
| Column | Notes |
|
||||||
| --- | --- |
|
| -------------------------- | --------------------------------------------- |
|
||||||
| `id` | |
|
| `id` | |
|
||||||
| `pond_id` | every grant belongs to exactly one pond |
|
| `pond_id` | every grant belongs to exactly one pond |
|
||||||
| `subject_type` | `user` / `authenticated` / `public` |
|
| `subject_type` | `user` / `authenticated` / `public` |
|
||||||
@ -76,8 +81,9 @@ and `subject_type = user`.
|
|||||||
## Content
|
## Content
|
||||||
|
|
||||||
### `ponds`
|
### `ponds`
|
||||||
|
|
||||||
| Column | Notes |
|
| Column | Notes |
|
||||||
| --- | --- |
|
| -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
| `id`, `slug` (unique), `name`, `description` | |
|
| `id`, `slug` (unique), `name`, `description` | |
|
||||||
| `type` | `personal` (one per user, from self-signup) / `shared` |
|
| `type` | `personal` (one per user, from self-signup) / `shared` |
|
||||||
| `owner_id` | creator; personal ponds: the user it belongs to |
|
| `owner_id` | creator; personal ponds: the user it belongs to |
|
||||||
@ -85,8 +91,9 @@ and `subject_type = user`.
|
|||||||
| `deleted_at`, `deleted_by` | pond-level trash (ADR 0013) |
|
| `deleted_at`, `deleted_by` | pond-level trash (ADR 0013) |
|
||||||
|
|
||||||
### `pages`
|
### `pages`
|
||||||
|
|
||||||
| Column | Notes |
|
| Column | Notes |
|
||||||
| --- | --- |
|
| ---------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
| `id`, `pond_id` | |
|
| `id`, `pond_id` | |
|
||||||
| `title` | also indexed for search weight A |
|
| `title` | also indexed for search weight A |
|
||||||
| `slug` | unique per pond, for stable URLs and wikilink resolution |
|
| `slug` | unique per pond, for stable URLs and wikilink resolution |
|
||||||
@ -97,27 +104,32 @@ and `subject_type = user`.
|
|||||||
| `deleted_at`, `deleted_by` | trash |
|
| `deleted_at`, `deleted_by` | trash |
|
||||||
|
|
||||||
### `page_content_cache`
|
### `page_content_cache`
|
||||||
|
|
||||||
One row per page, refreshed on persistence: `plain_text`, `markdown`,
|
One row per page, refreshed on persistence: `plain_text`, `markdown`,
|
||||||
`html`, `outline` (headings JSON, for TOC plugins), generated `tsvector`
|
`html`, `outline` (headings JSON, for TOC plugins), generated `tsvector`
|
||||||
column with GIN index (ADR 0010).
|
column with GIN index (ADR 0010).
|
||||||
|
|
||||||
### `page_versions` (ADR 0013)
|
### `page_versions` (ADR 0013)
|
||||||
|
|
||||||
`page_id`, `ydoc_snapshot` (bytea, self-contained), `trigger`
|
`page_id`, `ydoc_snapshot` (bytea, self-contained), `trigger`
|
||||||
(`auto` / `manual` / `pre_restore`), `label`, `contributor_ids`,
|
(`auto` / `manual` / `pre_restore`), `label`, `contributor_ids`,
|
||||||
`created_at`.
|
`created_at`.
|
||||||
|
|
||||||
### `labels`
|
### `labels`
|
||||||
|
|
||||||
`pond_id`, `name`, `parent_id` (nullable — hierarchy), `color`, unique on
|
`pond_id`, `name`, `parent_id` (nullable — hierarchy), `color`, unique on
|
||||||
(`pond_id`, `parent_id`, `name`). Cycles are rejected at write time.
|
(`pond_id`, `parent_id`, `name`). Cycles are rejected at write time.
|
||||||
`page_labels(page_id, label_id)` is the assignment table.
|
`page_labels(page_id, label_id)` is the assignment table.
|
||||||
|
|
||||||
### `page_links`
|
### `page_links`
|
||||||
|
|
||||||
Wikilink index maintained on persistence: `from_page_id`, `to_page_id`
|
Wikilink index maintained on persistence: `from_page_id`, `to_page_id`
|
||||||
(nullable when target does not exist yet — "phantom" links), `target_slug`.
|
(nullable when target does not exist yet — "phantom" links), `target_slug`.
|
||||||
Backlinks = query by `to_page_id`. Creating a page with a phantom-linked
|
Backlinks = query by `to_page_id`. Creating a page with a phantom-linked
|
||||||
slug resolves those rows.
|
slug resolves those rows.
|
||||||
|
|
||||||
### `attachments` (ADR 0011)
|
### `attachments` (ADR 0011)
|
||||||
|
|
||||||
`id`, `pond_id`, `page_id` (nullable — pond-level files), `file_name`,
|
`id`, `pond_id`, `page_id` (nullable — pond-level files), `file_name`,
|
||||||
`mime_type`, `size_bytes`, `storage_path`, `uploaded_by`, `created_at`,
|
`mime_type`, `size_bytes`, `storage_path`, `uploaded_by`, `created_at`,
|
||||||
`deleted_at`.
|
`deleted_at`.
|
||||||
@ -125,23 +137,27 @@ slug resolves those rows.
|
|||||||
## Plugins (ADR 0008)
|
## Plugins (ADR 0008)
|
||||||
|
|
||||||
### `plugins`
|
### `plugins`
|
||||||
|
|
||||||
`id` (manifest id), `version`, `manifest` (jsonb), `storage_path`,
|
`id` (manifest id), `version`, `manifest` (jsonb), `storage_path`,
|
||||||
`kind` (`code` / `section_style`), `instance_mode`
|
`kind` (`code` / `section_style`), `instance_mode`
|
||||||
(`disabled` / `optional` / `required`), `installed_by`, `installed_at`.
|
(`disabled` / `optional` / `required`), `installed_by`, `installed_at`.
|
||||||
|
|
||||||
### `pond_plugins`
|
### `pond_plugins`
|
||||||
|
|
||||||
(`pond_id`, `plugin_id`, `enabled`) — only meaningful for `optional`
|
(`pond_id`, `plugin_id`, `enabled`) — only meaningful for `optional`
|
||||||
plugins; `required` plugins are active everywhere.
|
plugins; `required` plugins are active everywhere.
|
||||||
|
|
||||||
## Quotas and settings
|
## Quotas and settings
|
||||||
|
|
||||||
### `instance_settings`
|
### `instance_settings`
|
||||||
|
|
||||||
Key-value (typed JSON) singleton set: registration mode, SMTP config
|
Key-value (typed JSON) singleton set: registration mode, SMTP config
|
||||||
(secrets referenced from env, not stored plaintext — see `security.md`),
|
(secrets referenced from env, not stored plaintext — see `security.md`),
|
||||||
default quotas, upload allowlist, legal pages content (imprint, privacy),
|
default quotas, upload allowlist, legal pages content (imprint, privacy),
|
||||||
instance default locale.
|
instance default locale.
|
||||||
|
|
||||||
### `quota_overrides`
|
### `quota_overrides`
|
||||||
|
|
||||||
`subject_type` (`user` / `pond`), `subject_id`, `quota_key`
|
`subject_type` (`user` / `pond`), `subject_id`, `quota_key`
|
||||||
(`editors_per_pond`, `readers_per_pond`, `additional_ponds`,
|
(`editors_per_pond`, `readers_per_pond`, `additional_ponds`,
|
||||||
`storage_bytes`, `max_file_bytes`), `value`. Resolution: pond override →
|
`storage_bytes`, `max_file_bytes`), `value`. Resolution: pond override →
|
||||||
@ -149,6 +165,7 @@ user override → instance default (most specific wins, mirroring the
|
|||||||
permission philosophy).
|
permission philosophy).
|
||||||
|
|
||||||
### `pond_usage`
|
### `pond_usage`
|
||||||
|
|
||||||
Cached counters per pond: `storage_bytes_used`, `editor_count`,
|
Cached counters per pond: `storage_bytes_used`, `editor_count`,
|
||||||
`reader_count` — updated transactionally, reconciled nightly.
|
`reader_count` — updated transactionally, reconciled nightly.
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ choice and any later move cheap). DNS status: `*.dorfteich.online` and
|
|||||||
Every stage (and every self-hosted instance) runs the same services:
|
Every stage (and every self-hosted instance) runs the same services:
|
||||||
|
|
||||||
| Service | Image | Notes |
|
| Service | Image | Notes |
|
||||||
| --- | --- | --- |
|
| ----------- | ------------------------------------ | --------------------------------------------------------------- |
|
||||||
| `web` | `dorfteich-web` | nginx: SPA assets, fonts; SPA fallback routing |
|
| `web` | `dorfteich-web` | nginx: SPA assets, fonts; SPA fallback routing |
|
||||||
| `api` | `dorfteich-api` | NestJS; runs `prisma migrate deploy` on start |
|
| `api` | `dorfteich-api` | NestJS; runs `prisma migrate deploy` on start |
|
||||||
| `collab` | `dorfteich-collab` | Hocuspocus WebSocket server |
|
| `collab` | `dorfteich-collab` | Hocuspocus WebSocket server |
|
||||||
@ -41,7 +41,7 @@ Self-hosters without a proxy can enable the optional `caddy` Compose profile
|
|||||||
## Stages
|
## Stages
|
||||||
|
|
||||||
| Stage | Where | Domain | Purpose | Data |
|
| Stage | Where | Domain | Purpose | Data |
|
||||||
| --- | --- | --- | --- | --- |
|
| -------- | ----------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------- |
|
||||||
| **Dev** | contributor machine (e.g. the operator's MacBook), Docker Desktop | `localhost` | feature work; hot reload via `compose.dev.yml` overlay (source mounts, vite dev server) | fixtures/seed script |
|
| **Dev** | contributor machine (e.g. the operator's MacBook), Docker Desktop | `localhost` | feature work; hot reload via `compose.dev.yml` overlay (source mounts, vite dev server) | fixtures/seed script |
|
||||||
| **Test** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-test/` | `test.dorfteich.cloud` | auto-deploy target of `main`; e2e suite runs here | reset-able; seeded |
|
| **Test** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-test/` | `test.dorfteich.cloud` | auto-deploy target of `main`; e2e suite runs here | reset-able; seeded |
|
||||||
| **Int** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-int/` | `int.dorfteich.cloud` | stable preview; manual/exploratory testing; release candidates | persistent test data |
|
| **Int** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-int/` | `int.dorfteich.cloud` | stable preview; manual/exploratory testing; release candidates | persistent test data |
|
||||||
|
|||||||
@ -54,7 +54,7 @@ monitoring, structured logs, backup alerting — no dedicated metrics stack.
|
|||||||
## Maintenance jobs (in-app scheduler, `jobs` table)
|
## Maintenance jobs (in-app scheduler, `jobs` table)
|
||||||
|
|
||||||
| Job | Cadence | Purpose |
|
| Job | Cadence | Purpose |
|
||||||
| --- | --- | --- |
|
| --------------------- | ----------------------- | -------------------------------------------------- |
|
||||||
| trash purge | daily | delete pages/ponds past trash retention (ADR 0013) |
|
| trash purge | daily | delete pages/ponds past trash retention (ADR 0013) |
|
||||||
| version thinning | daily | auto-version retention policy (ADR 0013) |
|
| version thinning | daily | auto-version retention policy (ADR 0013) |
|
||||||
| update-log compaction | hourly, idle pages only | bound Yjs log growth |
|
| update-log compaction | hourly, idle pages only | bound Yjs log growth |
|
||||||
@ -79,7 +79,7 @@ panel is the operator's single glance for instance health.
|
|||||||
## Capacity & limits (initial values, instance-tunable)
|
## Capacity & limits (initial values, instance-tunable)
|
||||||
|
|
||||||
| Limit | Default |
|
| Limit | Default |
|
||||||
| --- | --- |
|
| ------------------------------- | ------------------------------------------------ |
|
||||||
| max page document size | 5 MiB Yjs state |
|
| max page document size | 5 MiB Yjs state |
|
||||||
| max upload size | 25 MiB (quota ladder, ADR 0011) |
|
| max upload size | 25 MiB (quota ladder, ADR 0011) |
|
||||||
| collab connections per instance | 500 concurrent |
|
| collab connections per instance | 500 concurrent |
|
||||||
|
|||||||
@ -9,7 +9,7 @@ never enforces security).
|
|||||||
## Roles
|
## Roles
|
||||||
|
|
||||||
| Role | Scope of existence | Capabilities |
|
| Role | Scope of existence | Capabilities |
|
||||||
| --- | --- | --- |
|
| -------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Site Admin** | instance (user flag) | everything: instance settings, users, quotas, plugins (install + set `disabled`/`optional`/`required`), all ponds' administration, legal pages, backups |
|
| **Site Admin** | instance (user flag) | everything: instance settings, users, quotas, plugins (install + set `disabled`/`optional`/`required`), all ponds' administration, legal pages, backups |
|
||||||
| **Pond Admin** | per pond (grant) | manage the pond: settings/fonts, labels, members and their grants, enable/disable optional plugins, trash, quotas view; implies Editor everywhere in the pond |
|
| **Pond Admin** | per pond (grant) | manage the pond: settings/fonts, labels, members and their grants, enable/disable optional plugins, trash, quotas view; implies Editor everywhere in the pond |
|
||||||
| **Editor** | per pond/label/page (grant) | create/edit/delete pages within the granted scope; view history, restore versions, use trash for pages they may edit |
|
| **Editor** | per pond/label/page (grant) | create/edit/delete pages within the granted scope; view history, restore versions, use trash for pages they may edit |
|
||||||
@ -34,11 +34,11 @@ A grant is `(subject, role, scope, effect)` inside one pond
|
|||||||
`public` (everyone, including anonymous visitors).
|
`public` (everyone, including anonymous visitors).
|
||||||
- **scope**: the whole pond, one label, or one page.
|
- **scope**: the whole pond, one label, or one page.
|
||||||
- **effect**: `allow` or `deny`. `deny` expresses the vision's "all pages
|
- **effect**: `allow` or `deny`. `deny` expresses the vision's "all pages
|
||||||
*except* label X" (pond-scope allow + label-scope deny).
|
_except_ label X" (pond-scope allow + label-scope deny).
|
||||||
|
|
||||||
## Resolution algorithm
|
## Resolution algorithm
|
||||||
|
|
||||||
Question: *may user U perform action A (read / write) on page P?*
|
Question: _may user U perform action A (read / write) on page P?_
|
||||||
|
|
||||||
1. If U is Site Admin → **allow**.
|
1. If U is Site Admin → **allow**.
|
||||||
2. If the page or its pond is in trash → only roles that could edit it may
|
2. If the page or its pond is in trash → only roles that could edit it may
|
||||||
@ -65,7 +65,7 @@ Question: *may user U perform action A (read / write) on page P?*
|
|||||||
Pond "Handbook", user Uma has pond-scope `editor` (allow):
|
Pond "Handbook", user Uma has pond-scope `editor` (allow):
|
||||||
|
|
||||||
| Extra grants | Uma edits page "Salaries"? | Why |
|
| Extra grants | Uma edits page "Salaries"? | Why |
|
||||||
| --- | --- | --- |
|
| ------------------------------------------------------------------------ | -------------------------- | -------------------------------------------- |
|
||||||
| — | yes | pond-scope allow |
|
| — | yes | pond-scope allow |
|
||||||
| label `confidential` on the page + label-scope `editor` **deny** for Uma | no | label level is more specific than pond level |
|
| label `confidential` on the page + label-scope `editor` **deny** for Uma | no | label level is more specific than pond level |
|
||||||
| additionally page-scope `editor` **allow** for Uma on "Salaries" | yes | page level beats label level |
|
| additionally page-scope `editor` **allow** for Uma on "Salaries" | yes | page level beats label level |
|
||||||
|
|||||||
@ -26,7 +26,11 @@ my-plugin.zip
|
|||||||
"apiVersion": "1",
|
"apiVersion": "1",
|
||||||
"kind": "code",
|
"kind": "code",
|
||||||
"extensionPoints": [
|
"extensionPoints": [
|
||||||
{ "type": "pageTool", "id": "toc", "title": { "de": "Inhaltsverzeichnis", "en": "Table of contents" } }
|
{
|
||||||
|
"type": "pageTool",
|
||||||
|
"id": "toc",
|
||||||
|
"title": { "de": "Inhaltsverzeichnis", "en": "Table of contents" }
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"permissions": ["readCurrentPage"],
|
"permissions": ["readCurrentPage"],
|
||||||
"fallback": { "type": "text", "value": "[Table of contents]" },
|
"fallback": { "type": "text", "value": "[Table of contents]" },
|
||||||
@ -45,7 +49,7 @@ my-plugin.zip
|
|||||||
## Kinds and extension points
|
## Kinds and extension points
|
||||||
|
|
||||||
| Kind | Extension point | What it does | Sandbox |
|
| Kind | Extension point | What it does | Sandbox |
|
||||||
| --- | --- | --- | --- |
|
| --------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `section_style` | `sectionStyle` | declares named styles (name, i18n label, CSS class body) applicable to container blocks — e.g. colored background boxes | none needed: CSS is sanitized (no `@import`, no `url()` to external hosts) and scoped under `.dt-style-<pluginId>-<styleId>` |
|
| `section_style` | `sectionStyle` | declares named styles (name, i18n label, CSS class body) applicable to container blocks — e.g. colored background boxes | none needed: CSS is sanitized (no `@import`, no `url()` to external hosts) and scoped under `.dt-style-<pluginId>-<styleId>` |
|
||||||
| `code` | `block` | a custom editor block (diagram, embed, …); host registers a ProseMirror node `plugin_block` instance with `pluginId`, `blockType`, `data` attrs | sandboxed iframe per block |
|
| `code` | `block` | a custom editor block (diagram, embed, …); host registers a ProseMirror node `plugin_block` instance with `pluginId`, `blockType`, `data` attrs | sandboxed iframe per block |
|
||||||
| `code` | `pageTool` | read-only widget rendered in the page tools panel or embedded as a block (TOC, page index, cross-page block embed) | sandboxed iframe |
|
| `code` | `pageTool` | read-only widget rendered in the page tools panel or embedded as a block (TOC, page index, cross-page block embed) | sandboxed iframe |
|
||||||
@ -73,7 +77,7 @@ the **viewing user's** session — a plugin can never read more than the
|
|||||||
person looking at it could.
|
person looking at it could.
|
||||||
|
|
||||||
| Capability | Methods |
|
| Capability | Methods |
|
||||||
| --- | --- |
|
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `readCurrentPage` | `getOutline()`, `getContent()` (Markdown), `getMeta()` |
|
| `readCurrentPage` | `getOutline()`, `getContent()` (Markdown), `getMeta()` |
|
||||||
| `readPond` | `listPages()`, `getPageOutline(pageId)`, `getPageContent(pageId)` |
|
| `readPond` | `listPages()`, `getPageOutline(pageId)`, `getPageContent(pageId)` |
|
||||||
| `readBlock` | `getBlock(pageId, blockId)` — cross-page block embedding |
|
| `readBlock` | `getBlock(pageId, blockId)` — cross-page block embedding |
|
||||||
|
|||||||
@ -89,7 +89,7 @@ sequenceDiagram
|
|||||||
## Failure modes
|
## Failure modes
|
||||||
|
|
||||||
| Failure | Behavior |
|
| Failure | Behavior |
|
||||||
| --- | --- |
|
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||||
| collab container down | editing degrades to offline mode (local persistence); banner "reconnecting…"; REST reads unaffected |
|
| collab container down | editing degrades to offline mode (local persistence); banner "reconnecting…"; REST reads unaffected |
|
||||||
| WebSocket blocked (proxy) | same as above; deployment docs require WebSocket pass-through for `/collab` |
|
| WebSocket blocked (proxy) | same as above; deployment docs require WebSocket pass-through for `/collab` |
|
||||||
| stale collab token | client transparently re-fetches and reconnects |
|
| stale collab token | client transparently re-fetches and reconnects |
|
||||||
|
|||||||
@ -72,7 +72,7 @@ or sloppy plugin authors, compromised dependencies.
|
|||||||
entered in the setup wizard is written to the env-backed secret store,
|
entered in the setup wizard is written to the env-backed secret store,
|
||||||
not to a DB row).
|
not to a DB row).
|
||||||
- Key rotation: collab signing key and session pepper rotate via env change
|
- Key rotation: collab signing key and session pepper rotate via env change
|
||||||
+ rolling restart; procedure documented in `operations.md` runbooks.
|
plus rolling restart; procedure documented in `operations.md` runbooks.
|
||||||
- Dependencies: lockfile-pinned; monthly update batch; images pinned to
|
- Dependencies: lockfile-pinned; monthly update batch; images pinned to
|
||||||
digests in Prod.
|
digests in Prod.
|
||||||
|
|
||||||
|
|||||||
26
eslint.config.mjs
Normal file
26
eslint.config.mjs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Repo-wide ESLint flat config. Packages inherit these rules; add
|
||||||
|
// package-specific overrides here (scoped by `files`) rather than with
|
||||||
|
// per-package config files, so rules stay consistent across the monorepo.
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['**/dist/**', '**/node_modules/**', '**/coverage/**', '**/*.gen.ts'],
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Unused values are usually bugs; underscore-prefix marks intentional ones.
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
// `any` defeats the shared Zod/type contracts between client and server.
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "dorfteich",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Dorfteich — an open-source wiki system with real-time collaboration",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@11.9.0",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint . && prettier --check .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"typecheck": "pnpm -r run typecheck",
|
||||||
|
"test": "pnpm -r run test",
|
||||||
|
"build": "pnpm -r run build",
|
||||||
|
"i18n:check": "node scripts/i18n-check.mjs"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.20.0",
|
||||||
|
"eslint": "^9.20.0",
|
||||||
|
"eslint-config-prettier": "^10.0.0",
|
||||||
|
"prettier": "^3.5.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"typescript-eslint": "^8.24.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
packages/shared/package.json
Normal file
32
packages/shared/package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "@dorfteich/shared",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Types, schemas, and logic shared between web, api, and collab",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run --passWithNoTests"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.24.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"tsup": "^8.3.0",
|
||||||
|
"vitest": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/shared/src/health.test.ts
Normal file
13
packages/shared/src/health.test.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { healthResponse } from './health';
|
||||||
|
|
||||||
|
describe('healthResponse', () => {
|
||||||
|
it('reports ok with service, version, and a valid timestamp', () => {
|
||||||
|
const res = healthResponse('api', '1.2.3');
|
||||||
|
expect(res.status).toBe('ok');
|
||||||
|
expect(res.service).toBe('api');
|
||||||
|
expect(res.version).toBe('1.2.3');
|
||||||
|
expect(Number.isNaN(Date.parse(res.time))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
23
packages/shared/src/health.ts
Normal file
23
packages/shared/src/health.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Shape of the liveness responses served by every Dorfteich service
|
||||||
|
* (`/healthz` on web, api, and collab). Readiness (`/readyz`) has a richer,
|
||||||
|
* service-specific shape and lives with the api.
|
||||||
|
*/
|
||||||
|
export interface HealthResponse {
|
||||||
|
status: 'ok';
|
||||||
|
service: 'web' | 'api' | 'collab';
|
||||||
|
version: string;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function healthResponse(
|
||||||
|
service: HealthResponse['service'],
|
||||||
|
version: string,
|
||||||
|
): HealthResponse {
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
service,
|
||||||
|
version,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/shared/src/index.ts
Normal file
1
packages/shared/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './health';
|
||||||
9
packages/shared/tsconfig.json
Normal file
9
packages/shared/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
2472
pnpm-lock.yaml
generated
Normal file
2472
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
packages:
|
||||||
|
- apps/*
|
||||||
|
- packages/*
|
||||||
|
# Postinstall scripts are opt-in with pnpm; esbuild needs its binary install.
|
||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
19
tsconfig.base.json
Normal file
19
tsconfig.base.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user