forked from retoor/devplacepy
feat: add six specialized Claude agent definitions under .claude/agents for audit, devii, docs, dry, fanout, and frontend maintenance
This commit is contained in:
@@ -293,12 +293,9 @@ administrator.
|
||||
# Authentication
|
||||
|
||||
Create an account, sign in, recover your password, and log out. These are the only endpoints
|
||||
that set or clear the `session` cookie. All other requests authenticate via that cookie, an
|
||||
`X-API-KEY` header, a `Bearer` token, or HTTP Basic credentials - see
|
||||
[Conventions & Errors](conventions.html) and [Authentication](/docs/authentication.html).
|
||||
|
||||
Every endpoint follows the shared [Conventions & Errors](/docs/conventions.html) (auth, content
|
||||
negotiation, pagination, status codes).
|
||||
that set or clear the `session` cookie; every other request authenticates with the methods
|
||||
described in [Authentication](/docs/authentication.html). The shared rules (content
|
||||
negotiation, pagination, status codes) live in [Conventions and Errors](/docs/conventions.html).
|
||||
|
||||
## Page vs. action
|
||||
|
||||
|
||||
@@ -14,10 +14,40 @@ SECTION_ARCH = "Architecture"
|
||||
SECTION_TESTING = "Testing"
|
||||
SECTION_PROD = "Production"
|
||||
SECTION_MAINTENANCE = "Maintenance agents"
|
||||
SECTION_CLAUDE = "Claude Code"
|
||||
|
||||
AUDIENCE_START = "Start here"
|
||||
AUDIENCE_BUILD = "Build with the API"
|
||||
AUDIENCE_CONTRIBUTE = "Contribute and internals"
|
||||
AUDIENCE_OPERATE = "Operate"
|
||||
|
||||
AUDIENCES = [
|
||||
(AUDIENCE_START, [SECTION_GENERAL]),
|
||||
(AUDIENCE_BUILD, [SECTION_API, SECTION_COMPONENTS, SECTION_STYLES]),
|
||||
(
|
||||
AUDIENCE_CONTRIBUTE,
|
||||
[
|
||||
SECTION_ARCH,
|
||||
SECTION_SERVICES,
|
||||
SECTION_DEVII,
|
||||
SECTION_BOTS,
|
||||
SECTION_TESTING,
|
||||
SECTION_MAINTENANCE,
|
||||
SECTION_CLAUDE,
|
||||
],
|
||||
),
|
||||
(AUDIENCE_OPERATE, [SECTION_ADMIN, SECTION_PROD]),
|
||||
]
|
||||
|
||||
DOCS_PAGES = [
|
||||
# General - how to use the site and Devii (everyone)
|
||||
{"slug": "index", "title": "Overview", "kind": "prose", "section": SECTION_GENERAL},
|
||||
{
|
||||
"slug": "getting-started",
|
||||
"title": "Getting started",
|
||||
"kind": "prose",
|
||||
"section": SECTION_GENERAL,
|
||||
},
|
||||
{
|
||||
"slug": "devii",
|
||||
"title": "Devii Assistant",
|
||||
@@ -55,6 +85,31 @@ DOCS_PAGES = [
|
||||
"kind": "prose",
|
||||
"section": SECTION_MAINTENANCE,
|
||||
},
|
||||
# Claude Code - the native subagent, command, and workflow setup under .claude/
|
||||
{
|
||||
"slug": "claude",
|
||||
"title": "Claude Code setup",
|
||||
"kind": "prose",
|
||||
"section": SECTION_CLAUDE,
|
||||
},
|
||||
{
|
||||
"slug": "claude-agents",
|
||||
"title": "Subagents",
|
||||
"kind": "prose",
|
||||
"section": SECTION_CLAUDE,
|
||||
},
|
||||
{
|
||||
"slug": "claude-commands",
|
||||
"title": "Commands",
|
||||
"kind": "prose",
|
||||
"section": SECTION_CLAUDE,
|
||||
},
|
||||
{
|
||||
"slug": "claude-workflows",
|
||||
"title": "Workflows",
|
||||
"kind": "prose",
|
||||
"section": SECTION_CLAUDE,
|
||||
},
|
||||
# Components - custom HTML web components with live examples (everyone)
|
||||
{
|
||||
"slug": "components",
|
||||
@@ -467,3 +522,19 @@ DOCS_PAGES = [
|
||||
]
|
||||
|
||||
PAGES_BY_SLUG = {page["slug"]: page for page in DOCS_PAGES}
|
||||
|
||||
|
||||
def nav_groups(pages):
|
||||
by_section = {}
|
||||
for page in pages:
|
||||
by_section.setdefault(page.get("section"), []).append(page)
|
||||
groups = []
|
||||
for audience, sections in AUDIENCES:
|
||||
tiers = []
|
||||
for section in sections:
|
||||
section_pages = by_section.get(section)
|
||||
if section_pages:
|
||||
tiers.append((section, section_pages))
|
||||
if tiers:
|
||||
groups.append((audience, tiers))
|
||||
return groups
|
||||
|
||||
@@ -10,7 +10,7 @@ from devplacepy.docs_api import render_group, build_services_group
|
||||
from devplacepy.services.manager import service_manager
|
||||
from devplacepy import docs_search, docs_export, docs_live, docs_prose
|
||||
|
||||
from devplacepy.routers.docs.pages import DOCS_PAGES, PAGES_BY_SLUG
|
||||
from devplacepy.routers.docs.pages import DOCS_PAGES, PAGES_BY_SLUG, nav_groups
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
@@ -83,6 +83,7 @@ async def docs_page(request: Request, slug: str):
|
||||
"request": request,
|
||||
"user": user,
|
||||
"pages": visible_pages,
|
||||
"nav": nav_groups(visible_pages),
|
||||
"current": "search",
|
||||
"kind": "search",
|
||||
"page_title_doc": "Search",
|
||||
@@ -118,6 +119,7 @@ async def docs_page(request: Request, slug: str):
|
||||
"request": request,
|
||||
"user": user,
|
||||
"pages": visible_pages,
|
||||
"nav": nav_groups(visible_pages),
|
||||
"current": slug,
|
||||
"kind": "live",
|
||||
"facts": docs_live.build_live_facts(user),
|
||||
@@ -165,6 +167,7 @@ async def docs_page(request: Request, slug: str):
|
||||
"request": request,
|
||||
"user": user,
|
||||
"pages": visible_pages,
|
||||
"nav": nav_groups(visible_pages),
|
||||
"current": slug,
|
||||
"kind": page["kind"],
|
||||
"group": group,
|
||||
|
||||
@@ -18,8 +18,24 @@
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-card .sidebar-subheading {
|
||||
.sidebar-card .sidebar-tier {
|
||||
margin-top: 1.4rem;
|
||||
padding-top: 0.85rem;
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sidebar-card .sidebar-tier:first-of-type {
|
||||
margin-top: 0.85rem;
|
||||
}
|
||||
|
||||
.sidebar-card .sidebar-subheading {
|
||||
margin-top: 0.6rem;
|
||||
margin-bottom: 0.4rem;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Backend and request pipeline
|
||||
|
||||
The server side end to end: how a request flows through middleware to a router, how the database layer is built and why it is synchronous, how caches stay correct across workers, and how the response, auth, and service layers fit together. See also [Architecture overview](/docs/architecture.html) and [Conventions and rules](/docs/architecture-conventions.html).
|
||||
How a request flows through middleware to a router, how the database layer is built and why it is synchronous, how caches stay correct across workers, and how the response, auth, and service layers fit together. See also [Architecture overview](/docs/architecture.html) and [Conventions and rules](/docs/architecture-conventions.html).
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
|
||||
|
||||
@@ -11,7 +11,7 @@ The server side end to end: how a request flows through middleware to a router,
|
||||
|
||||
## Middleware
|
||||
|
||||
Four HTTP middlewares are registered. They run as a stack around every request:
|
||||
Four HTTP middlewares run as a stack around every request:
|
||||
|
||||
| Middleware | Responsibility |
|
||||
|------------|----------------|
|
||||
@@ -22,7 +22,7 @@ Four HTTP middlewares are registered. They run as a stack around every request:
|
||||
|
||||
## Routing
|
||||
|
||||
Routers in `routers/` form a **directory tree that mirrors the endpoint (URL) path**, the same convention as the test suite. A single-resource domain is one flat file (`feed.py`, `posts.py`, ...); a multi-sub-resource domain is a **package directory** split one file per sub-resource (a distinct noun under the domain), never one file per endpoint. Each leaf exports its own `router = APIRouter()` keeping the exact path strings, and the package `__init__.py` aggregates them with `include_router`. Because a package exposes `.router` like a module, `main.py` mounts each domain at its prefix unchanged:
|
||||
Routers in `routers/` form a **directory tree that mirrors the endpoint (URL) path**, the same convention as the test suite. A single-resource domain is one flat file (`feed.py`, `posts.py`, ...); a multi-sub-resource domain is a **package directory** split one file per sub-resource (a distinct noun under the domain), never one file per endpoint. Each leaf exports its own `router = APIRouter()` keeping the exact path strings, and the package `__init__.py` aggregates them with `include_router`. A package exposes `.router` like a module, so `main.py` mounts each domain at its prefix unchanged:
|
||||
|
||||
```
|
||||
/auth /feed /posts /comments /projects /profile
|
||||
@@ -39,7 +39,7 @@ Routers in `routers/` form a **directory tree that mirrors the endpoint (URL) pa
|
||||
|
||||
`@app.on_event("startup")` runs `init_db()` and certificate setup under an exclusive init lock (`fcntl.flock` on `devplace-init.lock`) so multiple workers serialize first-boot side effects. It then registers `NewsService`, `BotsService`, `GatewayService`, and `DeviiService`.
|
||||
|
||||
Background services follow a **single-owner model**. Each worker tries to acquire an exclusive non-blocking lock on `devplace-services.lock`; exactly one wins, becomes the lock owner, and calls `service_manager.supervise()`. The others decline and run no services. This is why `/devii/ws` is served only by the lock-owning worker (others close with 1013 so the client reconnects to the owner), and why news is never fetched twice. Setting `DEVPLACE_DISABLE_SERVICES=1` skips all of this (the test conftest does so).
|
||||
Background services follow a **single-owner model**. Each worker tries to acquire an exclusive non-blocking lock on `devplace-services.lock`; exactly one wins, becomes the lock owner, and calls `service_manager.supervise()`. The others run no services. This is why `/devii/ws` is served only by the lock-owning worker (others close with 1013 so the client reconnects to the owner), and why news is never fetched twice. Setting `DEVPLACE_DISABLE_SERVICES=1` skips all of this (the test conftest does so).
|
||||
|
||||
## Database
|
||||
|
||||
@@ -54,7 +54,7 @@ PRAGMA temp_store=MEMORY
|
||||
PRAGMA mmap_size=268435456 -- 256MB
|
||||
```
|
||||
|
||||
**SQLite is synchronous by design and will never be made async. This is a hard rule.** The synchronous `dataset`/SQLite layer is called directly inside the async route handlers. With a local file in WAL mode, a 30s busy timeout, an 8MB page cache, and a 256MB mmap, queries are sub-millisecond and never meaningfully stall the event loop. Do not convert the database layer to async, do not wrap DB calls in a threadpool / `run_in_executor` / `to_thread`, and do not raise synchronous DB access as a performance or blocking-I/O concern. It is a settled design choice.
|
||||
**SQLite is synchronous by design and will never be made async. This is a hard rule.** The synchronous `dataset`/SQLite layer is called directly inside the async route handlers. With a local file in WAL mode, a 30s busy timeout, an 8MB page cache, and a 256MB mmap, queries are sub-millisecond and never meaningfully stall the event loop. Do not convert the database layer to async, do not wrap DB calls in a threadpool / `run_in_executor` / `to_thread`, and do not raise synchronous DB access as a performance or blocking-I/O concern.
|
||||
|
||||
Other database rules:
|
||||
|
||||
@@ -67,7 +67,7 @@ Other database rules:
|
||||
|
||||
## Caching and multi-worker consistency
|
||||
|
||||
In-process TTL caches accelerate hot reads but are **never authoritative**: `_settings_cache` (60s) for `site_settings` and `_user_cache` (300s) for authenticated users. Because the app runs multiple workers, caches are invalidated cross-worker through the `cache_state` version table. A read calls `sync_local_cache(name, cache)` to compare its local version against the database version and clears the local cache if it is stale; a write calls `bump_cache_version(name)`. Settings use the `settings` name and the user cache uses the `auth` name. The rule: authoritative state lives in the database; memory is only a short-TTL accelerator.
|
||||
In-process TTL caches accelerate hot reads but are **never authoritative**: `_settings_cache` (60s) for `site_settings` and `_user_cache` (300s) for authenticated users. Because the app runs multiple workers, caches are invalidated cross-worker through the `cache_state` version table. A read calls `sync_local_cache(name, cache)` to compare its local version against the database version, clearing the local cache if it is stale; a write calls `bump_cache_version(name)`. Settings use the `settings` name, the user cache the `auth` name. Authoritative state lives in the database; memory is only a short-TTL accelerator.
|
||||
|
||||
## Models, schemas, and responses
|
||||
|
||||
@@ -81,5 +81,5 @@ Session cookies named `session` hold a 64-char hex token from `secrets.token_hex
|
||||
|
||||
## Services framework
|
||||
|
||||
`BaseService` provides the async run loop, typed `ConfigField` definitions, state persisted to `service_state` (status, last/next run, log deque, metrics), and admin commands (run, clear). `ServiceManager` is a singleton that registers, supervises, and stops services and tracks lock ownership. New services extend `BaseService`, override `async def run_once(self)`, and register in `main.py`; they then appear automatically on `/admin/services`. `GatewayService` is the single source of truth for AI: every other AI consumer routes through it, so usage is attributed and limitable per owner.
|
||||
`BaseService` provides the async run loop, typed `ConfigField` definitions, state persisted to `service_state` (status, last/next run, log deque, metrics), and admin commands (run, clear). `ServiceManager` is a singleton that registers, supervises, and stops services and tracks lock ownership. New services extend `BaseService`, override `async def run_once(self)`, and register in `main.py`; they then appear on `/admin/services`. `GatewayService` is the single source of truth for AI: every other AI consumer routes through it, so usage is attributed and limitable per owner.
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% raw %}
|
||||
# Conventions and rules
|
||||
|
||||
The rule book. These are the project-wide conventions that keep the codebase consistent. They are not style preferences; following them is how new code reads like the existing code. See also [Architecture overview](/docs/architecture.html) and [Development workflow](/docs/architecture-workflow.html).
|
||||
The project-wide conventions that keep the codebase consistent. They are not style preferences; following them is how new code reads like the existing code. See also [Architecture overview](/docs/architecture.html) and [Development workflow](/docs/architecture-workflow.html).
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
|
||||
|
||||
@@ -37,7 +37,7 @@ Add new query and batch helpers to `database.py` rather than inlining SQL in rou
|
||||
- **Dates are DD/MM/YYYY (European).** Use the `format_date()` template global, or `time_ago()` which switches to DD/MM/YYYY past 30 days.
|
||||
- **Slug + UUID lookup.** Resources with slugs (posts, gists, news, projects) accept either the slug or the bare UUID. Slugs are generated by `make_combined_slug(title, uid)`, which prefixes the first 8 characters of the UUID; resolution is `resolve_by_slug()`.
|
||||
- **Ownership checks and cascade deletes.** Compare `resource["user_uid"] == user["uid"]` before edit or delete. Deletes must cascade: comments, then votes, then the resource.
|
||||
- **Username** is 3 to 32 characters of letters, numbers, hyphens, and underscores. **Password** is a minimum of 6 characters, and that is the only rule. We treat users as adults who are responsible for their own password strength; we do not babysit them with forced complexity rules, mandatory symbols, or arbitrary length minimums that only push people toward weaker, reused, or written-down passwords. Account security against online guessing is a server concern, handled by the per-IP rate limit on the request pipeline, not by punishing the user at the keyboard. We do not believe brute force over the web is a credible threat model for a rate-limited login, so we do not design the password policy around it.
|
||||
- **Username** is 3 to 32 characters of letters, numbers, hyphens, and underscores. **Password** is a minimum of 6 characters, and that is the only rule: no forced complexity, mandatory symbols, or higher length minimums, which only push people toward weaker, reused, or written-down passwords. Security against online guessing is handled by the per-IP rate limit on the request pipeline, not by complexity rules at the keyboard; brute force over a rate-limited login is not a credible threat model.
|
||||
- **Avatars** are Multiavatar SVGs generated locally from the username seed, in-memory cached, served from `/avatar/multiavatar/{seed}`.
|
||||
|
||||
## Polymorphic comments and votes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Frontend and components
|
||||
|
||||
The client side is vanilla JavaScript that enhances server-rendered HTML in place. This page covers the no-framework rule, the `Application` root, the custom web components, the content rendering pipeline, and the declarative `data-*` contract between markup and controllers. See also [Styling and templates](/docs/architecture-styling.html).
|
||||
The client side is vanilla JavaScript that enhances server-rendered HTML in place. This page covers the no-framework rule, the `Application` root, the custom web components, the content rendering pipeline, and the declarative `data-*` contract. See also [Styling and templates](/docs/architecture-styling.html).
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
|
||||
|
||||
@@ -20,7 +20,7 @@ const app = new Application();
|
||||
window.app = app;
|
||||
```
|
||||
|
||||
Its constructor imports `components/index.js` (which defines every web component) and instantiates every page controller as a property (`app.modals`, `app.forms`, `app.votes`, `app.comments`, and so on). It also creates the three component singletons once and exposes them as `app.dialog`, `app.contextMenu`, and `app.toast`. Page code reaches any capability through `window.app`.
|
||||
Its constructor imports `components/index.js` (which defines every web component) and instantiates every page controller as a property (`app.modals`, `app.forms`, `app.votes`, `app.comments`, and so on). It also creates the three component singletons and exposes them as `app.dialog`, `app.contextMenu`, and `app.toast`. Page code reaches any capability through `window.app`.
|
||||
|
||||
## Custom web components
|
||||
|
||||
@@ -50,7 +50,7 @@ Every component **renders into the light DOM (no shadow root)** so the global CS
|
||||
|
||||
## Shared utilities
|
||||
|
||||
A few plain modules own the cross-cutting patterns so feature code stays small. Reach for these instead of hand-rolling a fetch, an interval, or a click handler:
|
||||
A few plain modules own the cross-cutting patterns. Reach for these instead of hand-rolling a fetch, an interval, or a click handler:
|
||||
|
||||
- **`Http`** (`static/js/Http.js`) is the single fetch helper: `getJson`, `sendForm` (form-encoded POST that follows the login redirect), and `send` (POST that throws the server's `error.message` on a non-2xx response or a `200 {ok: false}` body). The container, admin, and project-files UIs all go through it rather than calling `fetch` directly.
|
||||
- **`Poller`** (`static/js/Poller.js`) wraps the universal live-update loop: `new Poller(fn, intervalMs, { pauseHidden })` with `start`/`stop`/`tick`, swallowing per-tick errors so one failed poll never kills the loop. Every polling controller (notification counts, the service monitor, AI usage, the container manager and instance pages) uses it.
|
||||
@@ -72,11 +72,11 @@ A few plain modules own the cross-cutting patterns so feature code stays small.
|
||||
|
||||
A `NodeIterator` skips `CODE`, `PRE`, `SCRIPT`, and `STYLE` so URLs inside code are never rewritten. Sanitization runs on the raw `marked` output before the trusted YouTube embeds are injected, so user content cannot inject script, event-handler, or iframe payloads while our own embeds survive.
|
||||
|
||||
This step is **fail-closed**: `render()` throws if `DOMPurify` is not loaded rather than emitting unsanitized HTML. Because content is rendered client-side from `element.textContent`, Jinja autoescaping alone does not protect it. This sanitize step is the XSS control.
|
||||
This step is **fail-closed**: `render()` throws if `DOMPurify` is not loaded rather than emitting unsanitized HTML. Because content is rendered client-side from `element.textContent`, Jinja autoescaping does not cover it, so this sanitize step is the XSS control.
|
||||
|
||||
## The data-* contract
|
||||
|
||||
Controllers find and enhance markup through declarative `data-*` attributes rather than inline scripts. This keeps behavior HTML-driven and auditable (grep for the attribute):
|
||||
Controllers find and enhance markup through declarative `data-*` attributes rather than inline scripts, keeping behavior HTML-driven and auditable (grep for the attribute):
|
||||
|
||||
| Attribute | Behavior |
|
||||
|-----------|----------|
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Async job services
|
||||
|
||||
The async job framework (`services/jobs/`) is the standard way to run heavy, blocking work off the request path and hand the caller a result URL when it is finished. It builds on the [service framework](/docs/services-framework.html): a `JobService` is a `BaseService` whose `run_once` drives a queue of database-backed jobs. [ZipService](/docs/services-zip.html) is the first consumer.
|
||||
The async job framework (`services/jobs/`) runs heavy, blocking work off the request path and hands the caller a result URL when it finishes. It builds on the [service framework](/docs/services-framework.html): a `JobService` is a `BaseService` whose `run_once` drives a queue of database-backed jobs. [ZipService](/docs/services-zip.html) is the first consumer.
|
||||
|
||||
> Audience: administrators and maintainers.
|
||||
|
||||
## When to use it
|
||||
|
||||
Use a job service whenever a request would otherwise block on slow work: archiving, exporting, rendering, batch processing, or anything that should survive a page reload and be polled. The request enqueues a row and returns immediately; a background worker does the work; the client polls for completion. On the client, the shared `JobPoller.run(statusUrl, { onDone, onFailed, onTimeout })` helper (`static/js/JobPoller.js`) is the canonical way to drive that poll.
|
||||
Use a job service whenever a request would otherwise block on slow work: archiving, exporting, rendering, batch processing, or anything that should survive a page reload and be polled. The request enqueues a row and returns immediately; a background worker does the work; the client polls for completion. The shared `JobPoller.run(statusUrl, { onDone, onFailed, onTimeout })` helper (`static/js/JobPoller.js`) drives that poll on the client.
|
||||
|
||||
## The jobs table
|
||||
|
||||
@@ -24,7 +24,7 @@ All job kinds share one `jobs` table, discriminated by a `kind` column. The row
|
||||
|
||||
## Single-owner processing
|
||||
|
||||
Processing happens only in the worker that owns the service lock, because only that worker calls `supervise()` and therefore only it runs `run_once()`. There is exactly one processor across the deployment, so no cross-worker locking or atomic claim is needed. Status reads and downloads work from any worker because they read the shared database and the shared `static` filesystem.
|
||||
Processing happens only in the worker that owns the service lock, since only that worker calls `supervise()` and runs `run_once()`. With exactly one processor across the deployment, no cross-worker locking or atomic claim is needed. Status reads and downloads work from any worker because they read the shared database and the shared `static` filesystem.
|
||||
|
||||
`JobService.run_once` does four things each tick:
|
||||
|
||||
@@ -33,7 +33,7 @@ Processing happens only in the worker that owns the service lock, because only t
|
||||
3. **Refill**: claim the oldest `pending` jobs up to the concurrency limit (uuid7 sorts in creation order, giving FIFO), mark them `running`, and spawn an `asyncio` task per job.
|
||||
4. **Sweep**: delete jobs whose `expires_at` has passed, calling the subclass `cleanup` hook to remove the artifact first.
|
||||
|
||||
In-flight tasks span ticks; they are stored in an instance map and reaped when done, so the loop returns quickly each tick. A per-tick timeout guard recovers a `running` row whose task vanished.
|
||||
In-flight tasks span ticks: stored in an instance map and reaped when done, so the loop returns quickly each tick. A per-tick timeout guard recovers a `running` row whose task vanished.
|
||||
|
||||
## Writing a new kind
|
||||
|
||||
@@ -58,5 +58,5 @@ Register it in `main.py` startup with `service_manager.register(MyJobService())`
|
||||
|
||||
## Retention is built in
|
||||
|
||||
There is no separate reaper service. Each job service prunes its own expired artifacts every tick through its `cleanup` hook, so deletion is a standard capability every kind gets for free. Retention is admin-configurable per service and defaults to seven days; downloading an artifact extends its expiry.
|
||||
There is no separate reaper service. Each job service prunes its own expired artifacts every tick through its `cleanup` hook, so every kind gets deletion for free. Retention is admin-configurable per service and defaults to seven days; downloading an artifact extends its expiry.
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@ Repeated markup is factored into partials and macros:
|
||||
|
||||
## How the frontend is served
|
||||
|
||||
There is no separate front-end application or client-side router: every page is HTML rendered by the backend, then enhanced in place. How the static assets behind those pages reach the browser differs between development and production.
|
||||
There is no separate front-end application or client-side router: every page is HTML rendered by the backend, then enhanced in place. How the static assets reach the browser differs between development and production.
|
||||
|
||||
**Development** (`make dev`, or bare-metal `make prod` without nginx): the FastAPI app mounts `/static` through `UploadStaticFiles` and serves all CSS, JavaScript, and uploads itself. Everything is delivered by the application process.
|
||||
|
||||
@@ -65,6 +65,6 @@ There is no separate front-end application or client-side router: every page is
|
||||
- `/avatar/` is proxied to the app, because avatars are generated SVGs, not files on disk.
|
||||
- Everything else (`/`, the rendered pages and the JSON API) is proxied to the app, with an optional micro-cache.
|
||||
|
||||
So the rendered pages always come from the backend, but in production the CSS, JavaScript, and uploads are served by nginx from disk for cache efficiency. See [nginx and networking](/docs/production-nginx.html) for the full route map.
|
||||
Rendered pages always come from the backend; in production the CSS, JavaScript, and uploads are served by nginx from disk for cache efficiency. See [nginx and networking](/docs/production-nginx.html) for the full route map.
|
||||
{% endraw %}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% raw %}
|
||||
# Development workflow
|
||||
|
||||
How a change moves from understanding to shipped. The workflow is ordered by data flow because every feature is one data source fanning out into several consumers, and the failure mode is updating one consumer while forgetting a connected one. See also [Architecture overview](/docs/architecture.html) and [Conventions and rules](/docs/architecture-conventions.html).
|
||||
How a change moves from understanding to shipped. The workflow is ordered by data flow because every feature is one data source fanning out into several consumers; the failure mode is updating one consumer while forgetting a connected one. See also [Architecture overview](/docs/architecture.html) and [Conventions and rules](/docs/architecture-conventions.html).
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
|
||||
|
||||
@@ -17,7 +17,7 @@ How a change moves from understanding to shipped. The workflow is ordered by dat
|
||||
|
||||
## The four faces
|
||||
|
||||
Because one handler is HTML, JSON, a Devii tool, and an API-docs entry at once, after touching a route ask of each face whether it needs updating:
|
||||
One handler is HTML, JSON, a Devii tool, and an API-docs entry at once. After touching a route, ask of each face whether it needs updating:
|
||||
|
||||
| Face | Where | Most-forgotten symptom |
|
||||
|------|-------|------------------------|
|
||||
@@ -34,7 +34,7 @@ Three files are kept current, each with a distinct role:
|
||||
- **`AGENTS.md`** is the deep companion: every domain-specific mechanic, helper, pitfall, and cross-layer wiring, plus the full feature relationship map.
|
||||
- **`CLAUDE.md`** holds the rules and conventions. Update it only when a new architectural rule, convention, or workflow step is introduced, not per feature.
|
||||
|
||||
This Architecture section in the in-app docs is the browsable, admin-facing reflection of those three: it explains the design and the process, while the trio remains the source of truth in the repository.
|
||||
This Architecture section in the in-app docs is the browsable, admin-facing reflection of those three: it explains the design and the process, while the trio remains the repository source of truth.
|
||||
|
||||
## Validation gates
|
||||
|
||||
@@ -46,6 +46,6 @@ Run these before considering a change done:
|
||||
|
||||
## Testing
|
||||
|
||||
Correctness is verified by the suite in `tests/`, and load behavior by Locust. Every user-facing feature requires an end-to-end Playwright test. The full rules, the fixture stack, and the `make` targets are documented in the [Testing overview](/docs/testing.html) section. Do not run the test suite as part of routine development unless it is explicitly requested; validate with a clean import and per-language checks instead.
|
||||
Correctness is verified by the suite in `tests/`, and load behavior by Locust. Every user-facing feature requires an end-to-end Playwright test. The rules, the fixture stack, and the `make` targets are documented in the [Testing overview](/docs/testing.html) section. Do not run the test suite during routine development unless explicitly requested; validate with a clean import and per-language checks instead.
|
||||
{% endraw %}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Architecture overview
|
||||
|
||||
How DevPlace is built and why. This is the design reference for the platform: the stack, the project structure, the central mental model that every feature follows, and the principles that govern the code. The subpages drill into the backend, the frontend, styling, the conventions, and the development workflow.
|
||||
The design reference for the platform: the stack, the project structure, the central mental model every feature follows, and the principles that govern the code. The subpages drill into the backend, the frontend, styling, the conventions, and the development workflow.
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
|
||||
|
||||
@@ -21,7 +21,7 @@ DevPlace is a server-rendered social network for developers. The backend renders
|
||||
| Styling | Vanilla CSS | Design tokens via custom properties. No preprocessor. |
|
||||
| Auth | Session cookies + API keys | No JWT. |
|
||||
|
||||
The recurring theme is deliberate minimalism: no JavaScript framework, no NPM, no build step, no client-side rendering framework, no JWT, and a local SQLite file instead of a database server. Every dependency that is present earns its place.
|
||||
The recurring theme is deliberate minimalism: no JavaScript framework, no NPM, no build step, no client-side rendering framework, no JWT, and a local SQLite file instead of a database server. Every dependency earns its place.
|
||||
|
||||
## Project structure
|
||||
|
||||
@@ -53,7 +53,7 @@ Every feature in DevPlace is **one data source fanning out into several consumer
|
||||
3. **Devii tool** - an `Action` in the Devii catalog lets the assistant call the same capability.
|
||||
4. **API docs** - an `endpoint()` entry documents it for developers.
|
||||
|
||||
The cardinal failure mode is changing one face and forgetting a connected one: adding a route but no JSON schema, adding a JSON endpoint but no Devii tool, or shipping a feature but never documenting it. The [Development workflow](/docs/architecture-workflow.html) checklist is ordered by data flow specifically so none of the four is dropped.
|
||||
The cardinal failure mode is changing one face and forgetting a connected one: adding a route but no JSON schema, a JSON endpoint but no Devii tool, or a feature but never documenting it. The [Development workflow](/docs/architecture-workflow.html) checklist is ordered by data flow so none of the four is dropped.
|
||||
|
||||
## Guiding principles
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ figure it records are restricted to admins.
|
||||
category, event key, actor role, origin, result, and a date range. Filters are
|
||||
carried through pagination links.
|
||||
- **`/admin/audit-log/{uid}`** is the per-event detail page: the full row, the
|
||||
old to new value transition, pretty-printed JSON metadata, and a table of every
|
||||
related object (actor, target, parent, author, recipient, source, destination,
|
||||
old to new value transition, pretty-printed JSON metadata, and a table of related
|
||||
objects (actor, target, parent, author, recipient, source, destination,
|
||||
schedule, instance, setting, job, and more).
|
||||
|
||||
Both routes negotiate content: send `Accept: application/json` to receive the
|
||||
@@ -31,8 +31,8 @@ FROM WHERE (`origin`, `via_agent`, `request_method`, `request_path`, `actor_ip`,
|
||||
`user_agent`), the primary object (`target_type`, `target_uid`, `target_label`), the
|
||||
change (`old_value` to `new_value`), a sanitised `summary`, event-specific `metadata`,
|
||||
and a `result` of `success`, `failure`, or `denied`. A second table,
|
||||
`audit_log_links`, records every related object with a labelled relation so an event
|
||||
can reference an arbitrary number of objects.
|
||||
`audit_log_links`, records each related object with a labelled relation, so an event
|
||||
can reference any number of objects.
|
||||
|
||||
The complete catalogue of event keys (155 across authentication, content, votes,
|
||||
projects, files, news, admin, services, containers, ingress, AI, the Devii agent, the
|
||||
@@ -52,8 +52,8 @@ CLI, rewards, and security) is enumerated in `events.md` at the repository root.
|
||||
- **Agent attribution.** When the Devii agent performs a platform mutation, the same
|
||||
event key is recorded with `origin=devii` and `via_agent=1` (the agent's HTTP client
|
||||
sends an `X-Devii-Agent` header), so an auditor can answer "what did the agent do on
|
||||
my behalf" without a parallel taxonomy. Mechanics with no HTTP equivalent get their
|
||||
own `devii.*` keys.
|
||||
my behalf" without a parallel taxonomy. Mechanics with no HTTP equivalent get
|
||||
`devii.*` keys.
|
||||
|
||||
## Retention
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Authentication
|
||||
|
||||
DevPlace accepts four interchangeable authentication methods. The session cookie is
|
||||
used by the website; the other three let you authenticate **any request** - any
|
||||
page or action - without logging in through the browser, which is ideal for scripts
|
||||
and automation. See also [Conventions & Errors](/docs/conventions.html) for the base
|
||||
URL, request bodies, content negotiation, pagination, and status codes that every
|
||||
endpoint shares.
|
||||
DevPlace accepts four interchangeable authentication methods. The website uses the
|
||||
session cookie; the other three authenticate **any request** - any page or action -
|
||||
without a browser login, ideal for scripts and automation. See
|
||||
[Conventions & Errors](/docs/conventions.html) for the base URL, request bodies,
|
||||
content negotiation, pagination, and status codes that every endpoint shares.
|
||||
|
||||
{% if not user %}
|
||||
> You are not logged in. Examples below use placeholders. [Log in](/auth/login) and
|
||||
@@ -24,7 +23,7 @@ You can regenerate it there at any time; the previous key stops working immediat
|
||||
The interactive panels throughout the [API reference](/docs/index.html) pre-fill your API
|
||||
key, so you can run user-level calls directly from these pages. Vote, reaction, bookmark, and
|
||||
poll endpoints require an `X-Requested-With: fetch` header to return JSON instead of a
|
||||
redirect; the panels and generated snippets add it for you.
|
||||
redirect; the panels and generated snippets add it automatically.
|
||||
|
||||
## 1. Session cookie
|
||||
|
||||
@@ -42,8 +41,7 @@ curl -H "X-API-KEY: {{ api_key }}" \
|
||||
|
||||
## 3. Bearer token
|
||||
|
||||
The same API key also works as a Bearer token, which many HTTP clients support out
|
||||
of the box:
|
||||
The same API key also works as a Bearer token, supported out of the box by many HTTP clients:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer {{ api_key }}" \
|
||||
@@ -87,8 +85,8 @@ curl -X POST -H "X-API-KEY: {{ api_key }}" \
|
||||
|
||||
## Errors
|
||||
|
||||
If you supply credentials that are invalid, protected endpoints respond with
|
||||
`401 Unauthorized`. Requests with no credentials at all are treated as anonymous
|
||||
(and protected actions redirect to the login page, as in the browser). The full
|
||||
status-code and error-shape reference lives in [Conventions & Errors](/docs/conventions.html).
|
||||
Invalid credentials make protected endpoints respond with `401 Unauthorized`. Requests
|
||||
with no credentials are treated as anonymous, and protected actions redirect to the login
|
||||
page as in the browser. The full status-code and error-shape reference lives in
|
||||
[Conventions & Errors](/docs/conventions.html).
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ How a bot comes to life and how its time is structured: the service reconciler,
|
||||
|
||||
## The service reconciler
|
||||
|
||||
`BotsService` extends `BaseService` with `default_enabled = False`, a 15 second interval, and `min_interval = 5`. Its `run_once` is a pure reconcile tick - it owns no heavy work itself, only the fleet's shape:
|
||||
`BotsService` extends `BaseService` with `default_enabled = False`, a 15 second interval, and `min_interval = 5`. Its `run_once` is a pure reconcile tick that owns no heavy work, only the fleet's shape:
|
||||
|
||||
1. Read the live config via `get_config()` and resolve the LLM key (`bot_api_key`, falling back to `database.internal_gateway_key()`). With no key, stop the fleet and log idle.
|
||||
2. Confirm the bot module imports (the `bots` extra is present). If not, stop the fleet and log that the extra is missing.
|
||||
@@ -21,9 +21,9 @@ Heavy dependencies (`playwright`, `faker`) are imported lazily inside the launch
|
||||
|
||||
## A bot's identity
|
||||
|
||||
On first launch a bot generates a human-looking handle with `faker` (varied styles: `first`, `first.last`, `firstlast`, `flast`, and occasionally a short word suffix like `.dev` or `_lab` - never a numeric suffix, which reads as a bot farm), an email, and a password, then registers through the signup form; on later sessions it logs in. On a signup collision it regenerates a fresh handle rather than appending digits. It also draws a stable browser fingerprint once (a real desktop user-agent and viewport from a varied pool) and persists it, so its requests do not all share one identical user-agent. The identity, persona, fingerprint, and all counters persist to `~/.devplace_bots/state_slot{N}.json`, so a bot keeps the same account and accumulated history across restarts. The persona is chosen once at random from the eight available and never changes.
|
||||
On first launch a bot generates a human-looking handle with `faker` (varied styles: `first`, `first.last`, `firstlast`, `flast`, and occasionally a short word suffix like `.dev` or `_lab` - never a numeric suffix, which reads as a bot farm), an email, and a password, then registers through the signup form; on later sessions it logs in. On a signup collision it regenerates a fresh handle rather than appending digits. It also draws a stable browser fingerprint once (a real desktop user-agent and viewport from a varied pool) and persists it, so its requests do not all share one user-agent. Identity, persona, fingerprint, and counters persist to `~/.devplace_bots/state_slot{N}.json`, so a bot keeps the same account and history across restarts. The persona is chosen once at random from the eight available and never changes.
|
||||
|
||||
To keep the fleet from waking all at once, each bot waits a random `bot_startup_jitter_seconds` delay (default up to four hours) before its first session, so activity spreads across the day instead of bursting together when the service starts.
|
||||
To keep the fleet from waking all at once, each bot waits a random `bot_startup_jitter_seconds` delay (default up to four hours) before its first session, spreading activity across the day instead of bursting at service start.
|
||||
|
||||
## The session loop
|
||||
|
||||
@@ -40,7 +40,7 @@ A crash-streak counter backs off on repeated failures and exits after too many c
|
||||
|
||||
## Moods
|
||||
|
||||
The session type is rolled at the start of each session. It sets how many actions the session targets and how long the bot then idles, so traffic looks human (bursts of activity separated by long absences) rather than a constant stream.
|
||||
The session type is rolled at the start of each session, setting how many actions the session targets and how long the bot then idles. Traffic looks human (bursts of activity separated by long absences) rather than a constant stream.
|
||||
|
||||
| Mood | Probability | Action budget | Break after |
|
||||
|------|------------|---------------|-------------|
|
||||
@@ -53,7 +53,7 @@ Within a session the mood also scales how much a cycle does, via a mood factor:
|
||||
|
||||
## Fatigue
|
||||
|
||||
After at least five actions in a session, each cycle has a `session_actions / 100` chance of ending the session early ("fatigue"), so sessions taper off naturally instead of all running to their exact budget. The break range is taken from the mood, then multiplied by the configurable `break_scale`, and floored at five seconds.
|
||||
After at least five actions in a session, each cycle has a `session_actions / 100` chance of ending the session early ("fatigue"), so sessions taper off instead of running to their exact budget. The break range comes from the mood, multiplied by the configurable `break_scale`, and floored at five seconds.
|
||||
|
||||
## The action cycle
|
||||
|
||||
@@ -73,13 +73,13 @@ Creation actions are session-gated in `run_forever` so a bot does not flood: a p
|
||||
|
||||
## Browser automation
|
||||
|
||||
`BotBrowser` wraps Playwright Chromium. Every interaction is real HTTP through the browser, authenticated exactly as a person would be. Two helpers make timing human:
|
||||
`BotBrowser` wraps Playwright Chromium. Every interaction is real HTTP through the browser, authenticated as a person would be. Two helpers make timing human:
|
||||
|
||||
- `_read_like_human` estimates a reading time from the word count and the bot's own reading speed (a fixed `180 to 320` words per minute drawn once per bot), scrolling in chunks while it "reads".
|
||||
- `_type_like_human` types character by character with per-character delays (longer after punctuation), occasional pauses, so form input is not an instant paste.
|
||||
- `_type_like_human` types character by character with per-character delays (longer after punctuation) and occasional pauses, so form input is not an instant paste.
|
||||
|
||||
## State
|
||||
|
||||
`BotState` (in `state.py`) holds the identity, persona, reading speed, every counter (posts, comments, votes, reactions, polls, follows, messages, searches, profiles viewed, quality rejections), de-duplication sets (voted ids, commented thread ids, reacted targets, seen notifications, posted content hashes, known users and posts), and the cumulative LLM cost and token totals. It is written atomically (temp file then replace) after each cycle. The LLM client's running totals are seeded from the saved state on construction, so lifetime cost survives restarts.
|
||||
`BotState` (in `state.py`) holds the identity, persona, reading speed, every counter (posts, comments, votes, reactions, polls, follows, messages, searches, profiles viewed, quality rejections), de-duplication sets (voted ids, commented thread ids, reacted targets, seen notifications, posted content hashes, known users and posts), and the cumulative LLM cost and token totals. It is written atomically (temp file then replace) after each cycle. The LLM client's running totals are seeded from saved state on construction, so lifetime cost survives restarts.
|
||||
</div>
|
||||
{% endraw %}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Configuration and operations
|
||||
|
||||
Everything an operator can tune, how a setting reaches a running bot, the cost model behind the metrics, and what the fleet needs to run.
|
||||
What an operator can tune, how a setting reaches a running bot, the cost model behind the metrics, and what the fleet needs to run.
|
||||
|
||||
## Configuration fields
|
||||
|
||||
All fields are edited on `/admin/services/bots` and grouped into sections that the admin UI renders automatically. The first three groups are the original operational settings; the **Behavior** and **Pacing** groups are the realism and cost-pacing dials.
|
||||
All fields are edited on `/admin/services/bots`, grouped into sections the admin UI renders automatically. The first three groups are operational settings; the **Behavior** and **Pacing** groups are the realism and cost-pacing dials.
|
||||
|
||||
**Fleet**
|
||||
|
||||
@@ -48,13 +48,13 @@ All fields are edited on `/admin/services/bots` and grouped into sections that t
|
||||
| `bot_action_pause_min_seconds` | 5 | 0 to 3600 | Lower bound of the idle pause after each action. |
|
||||
| `bot_action_pause_max_seconds` | 45 | 1 to 7200 | Upper bound of the idle pause after each action. |
|
||||
| `bot_break_scale` | 1.0 | 0.1 to 10.0 | Multiplier on the between-session break. Below 1 makes the fleet more active and costlier; above 1 calmer. |
|
||||
| `bot_startup_jitter_seconds` | 14400 | 0 to 86400 | Each bot waits a random delay up to this many seconds before its first session, so the fleet spreads out instead of all waking together when the service starts. |
|
||||
| `bot_startup_jitter_seconds` | 14400 | 0 to 86400 | Each bot waits a random delay up to this many seconds before its first session, so the fleet spreads out instead of all waking together at service start. |
|
||||
|
||||
Plus the inherited `Enabled`, `Run interval`, and `Log buffer size` fields from the service framework.
|
||||
|
||||
## How a setting reaches a bot
|
||||
|
||||
The `config.py` constants are only defaults; the live value flows through the runtime config, never read as a module constant at the call site:
|
||||
The `config.py` constants are only defaults. The live value flows through the runtime config and is never read as a module constant at the call site:
|
||||
|
||||
1. `config.py` holds the default (for example `MAX_BOTS_PER_ARTICLE = 2`).
|
||||
2. `service.py` declares a `ConfigField` for it (which the admin UI validates and stores).
|
||||
@@ -71,7 +71,7 @@ The fleet publishes live metrics via `collect_metrics()` (shown on the Services
|
||||
- **Stats:** bots running, fleet cost, observed window, cost rate, projected 24h cost, LLM calls, tokens in, tokens out, posts, comments, votes.
|
||||
- **Table** (one row per bot): slot, username, persona, status, posts, comments, votes, LLM calls, cost.
|
||||
|
||||
`Fleet cost` is an estimate computed from token usage and the configured per-1M input and output prices, not the gateway's recorded spend, and it is cumulative across restarts because each bot seeds its `LLMClient` totals from its saved state. `Cost rate` and `Projected 24h cost` come from a sliding window, not lifetime: each tick appends `(now, total_cost)`, samples older than `COST_WINDOW_SECONDS` (600s) are evicted, and the rate is the cost delta over the retained span. The projection is shown only after the window reaches `COST_WARMUP_SECONDS` (120s); before that both read "warming up". Samples reset when the service start time changes. This avoids the boot-burst (all bots ramping at once) dominating a 24h projection extrapolated from a few minutes.
|
||||
`Fleet cost` is an estimate computed from token usage and the configured per-1M input and output prices, not the gateway's recorded spend. It is cumulative across restarts because each bot seeds its `LLMClient` totals from its saved state. `Cost rate` and `Projected 24h cost` come from a sliding window, not lifetime: each tick appends `(now, total_cost)`, samples older than `COST_WINDOW_SECONDS` (600s) are evicted, and the rate is the cost delta over the retained span. The projection is shown only after the window reaches `COST_WARMUP_SECONDS` (120s); before that both read "warming up". Samples reset when the service start time changes, so the boot-burst of all bots ramping at once does not dominate a 24h projection extrapolated from a few minutes.
|
||||
|
||||
## State and files
|
||||
|
||||
@@ -96,8 +96,8 @@ Spend scales with how much the fleet generates. The levers, in order of effect:
|
||||
- `bot_max_actions` - hard cap per bot.
|
||||
- `bot_break_scale` - raise above 1 to lengthen idle breaks (calmer, cheaper); lower toward 0.1 for a busier, costlier instance.
|
||||
- `bot_action_pause_min_seconds` / `bot_action_pause_max_seconds` - widen for slower cadence within a session.
|
||||
- `bot_startup_jitter_seconds` - raise to spread the fleet's first sessions over a longer window; set to 0 only for debugging, when an immediate burst is acceptable.
|
||||
- `bot_startup_jitter_seconds` - raise to spread the fleet's first sessions over a longer window; set to 0 only for debugging.
|
||||
|
||||
None of these trade away content quality; the quality gates always run. Cost is also bounded upstream by the gateway's own per-owner daily limits.
|
||||
None trade away content quality; the quality gates always run. Cost is also bounded upstream by the gateway's per-owner daily limits.
|
||||
</div>
|
||||
{% endraw %}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Content generation and quality
|
||||
|
||||
Every piece of content a bot creates flows through `LLMClient` and, for posts, comments, and gists, a quality pipeline. All generation defaults to the internal AI gateway with the generic model, so spend is attributed and capped like any other gateway consumer.
|
||||
Every piece of content a bot creates flows through `LLMClient`, and posts, comments, and gists pass through a quality pipeline. All generation defaults to the internal AI gateway with the generic model, so spend is attributed and capped like any other gateway consumer.
|
||||
|
||||
## Posts
|
||||
|
||||
A post is built from a news article. The full path:
|
||||
A post is built from a news article:
|
||||
|
||||
1. **Pick the article.** `_pick_article` ranks the fetched articles by `persona_article_score(article, persona)` plus a random jitter and chooses from the top third, so the persona's interests bias selection without making it deterministic.
|
||||
2. **Pick the category.** `pick_category(persona)` chooses a category by the persona's weights (no LLM call).
|
||||
@@ -20,7 +20,7 @@ A content hash guards against a bot ever posting identical text twice.
|
||||
|
||||
## Titles and label stripping
|
||||
|
||||
The single most obvious tell of the earlier fleet was the post title: it was the raw news headline, capped at 200 characters. No person titles a post with a 180-character press headline. `generate_post_title` fixes this by asking for an original short title in voice; `LLMClient.strip_label` then defensively removes any leading label the model might echo (`Title:`, `Project Name:`, `Snippet name:`, and a trailing ` Concept:`/` Description:` clause), so a title can never come out as `Project Name: X Concept: Y`. The same function cleans project and gist titles.
|
||||
The clearest tell of the earlier fleet was the post title: the raw news headline, capped at 200 characters. `generate_post_title` asks for an original short title in voice instead; `LLMClient.strip_label` then removes any leading label the model might echo (`Title:`, `Project Name:`, `Snippet name:`, and a trailing ` Concept:`/` Description:` clause), so a title can never come out as `Project Name: X Concept: Y`. The same function cleans project and gist titles.
|
||||
|
||||
## Categories
|
||||
|
||||
@@ -35,7 +35,7 @@ Gists changed from "name a snippet, then write it" to **code-first generation wi
|
||||
3. Title and describe the snippet from the code, so the title fits what was actually written.
|
||||
4. Run `gist_quality_check(title, code, language)` and regenerate once on failure, then skip if still failing.
|
||||
|
||||
`gist_quality_check` has three layers: a `TRIVIAL_GIST_TERMS` blocklist on the title (catches "ternary operator", "delete pointer", "fizzbuzz", "reverse a string", and similar), a minimum non-empty line count (`LLMClient.gist_min_lines`, default 6, admin `bot_gist_min_lines`), and a strict LLM judge that rejects anything an experienced developer would already know by heart. This mirrors the post and comment quality loop.
|
||||
`gist_quality_check` has three layers: a `TRIVIAL_GIST_TERMS` blocklist on the title (catches "ternary operator", "delete pointer", "fizzbuzz", "reverse a string", and similar), a minimum non-empty line count (`LLMClient.gist_min_lines`, default 6, admin `bot_gist_min_lines`), and a strict LLM judge that rejects anything an experienced developer already knows by heart. This mirrors the post and comment quality loop.
|
||||
|
||||
## Projects
|
||||
|
||||
@@ -43,7 +43,7 @@ Gists changed from "name a snippet, then write it" to **code-first generation wi
|
||||
|
||||
## Comments and replies
|
||||
|
||||
`generate_comment(post_snippet, persona, mention_target, parent_context, style)` references a specific detail and adds something real (an experience, caveat, counterexample, respectful disagreement, or pointed question), and is explicitly forbidden from opening with generic filler. Each call rolls a `style` (`LLMClient.pick_comment_style`): most comments are the standard 1 to 3 sentences, but roughly a fifth are a terse one-liner (under a dozen words, lowercase and casual allowed) and another fifth are a single pointed question. The variety is what keeps a thread from reading as a wall of uniform paragraphs. When a `mention_target` is set the `@mention` is woven into the first sentence; when a `parent_context` is passed (a reply), the parent comment is included so the reply is on-topic.
|
||||
`generate_comment(post_snippet, persona, mention_target, parent_context, style)` references a specific detail and adds something real (an experience, caveat, counterexample, respectful disagreement, or pointed question), and is forbidden from opening with generic filler. Each call rolls a `style` (`LLMClient.pick_comment_style`): most comments are the standard 1 to 3 sentences, roughly a fifth are a terse one-liner (under a dozen words, lowercase and casual allowed), and another fifth are a single pointed question. This variety keeps a thread from reading as a wall of uniform paragraphs. When a `mention_target` is set the `@mention` is woven into the first sentence; when a `parent_context` is passed (a reply), the parent comment is included so the reply is on-topic.
|
||||
|
||||
To stop a bot from dropping several comments on one page within minutes, each session draws a small comment cap (one to three) and applies a one-to-four-minute cooldown after every comment or reply; once the cap is reached the bot votes, reacts, or moves on instead.
|
||||
|
||||
@@ -52,7 +52,7 @@ Comments are de-duplicated per thread (a bot will not comment twice on the same
|
||||
## Bugs, bios, profiles, and messages
|
||||
|
||||
- `generate_bug(topic)` writes a one-line title plus a 2 to 3 sentence report.
|
||||
- `generate_bio()` and `generate_profile_fields(handle)` fill the profile (bio, a plausible city/country, a derived git link and website). A new bot fills its profile early in its first session (`_update_profile`, gated by the `profile_filled` state flag so it runs once), so accounts are not left with empty bios.
|
||||
- `generate_bio()` and `generate_profile_fields(handle)` fill the profile (bio, a plausible city/country, a derived git link and website). A new bot fills its profile early in its first session (`_update_profile`, gated by the `profile_filled` state flag so it runs once), so accounts never sit with empty bios.
|
||||
- `generate_dm(persona, context)` writes a short, friendly direct message in voice, used only when the conversation rules allow (see [Engagement](/docs/bots-engagement.html)).
|
||||
|
||||
## The quality pipeline
|
||||
@@ -68,6 +68,6 @@ Two complementary checks guard content quality; both call a deterministic (`temp
|
||||
|
||||
**`gist_quality_check(title, code, language)`** (gists): the trivial-term blocklist, the minimum line count, and the code-aware judge described above.
|
||||
|
||||
In both cases a rejection increments the bot's `quality_rejections` counter, regenerates once, and abandons the action if the second attempt also fails - the fleet would rather post nothing than post filler.
|
||||
In both cases a rejection increments the bot's `quality_rejections` counter, regenerates once, and abandons the action if the second attempt also fails. The fleet would rather post nothing than post filler.
|
||||
</div>
|
||||
{% endraw %}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Engagement and interaction
|
||||
|
||||
Beyond creating content, a bot votes, reacts, votes in polls, follows, messages, and - most importantly for believability - takes part in threaded discussion with other bots. This page covers the engagement surfaces and the cross-bot coordination that makes discussion form.
|
||||
Beyond creating content, a bot votes, reacts, votes in polls, follows, messages, and - most importantly for believability - takes part in threaded discussion with other bots. This page covers the engagement surfaces and the cross-bot coordination that forms discussion.
|
||||
|
||||
## Voting
|
||||
|
||||
A bot votes on posts, comments, gists, projects, and polls. Voting is de-duplicated by id (a bounded `voted_post_ids` list) so a bot never votes the same thing twice. Feed voting is **reciprocal**: candidates are gathered, and when any are authored by a `known_user` (someone the bot has encountered), the bot prefers that subset 65% of the time. This produces the natural pattern where people who have interacted before keep showing up in each other's activity.
|
||||
A bot votes on posts, comments, gists, projects, and polls. Voting is de-duplicated by id (a bounded `voted_post_ids` list) so a bot never votes the same thing twice. Feed voting is **reciprocal**: candidates are gathered, and when any are authored by a `known_user` (someone the bot has encountered), the bot prefers that subset 65% of the time. People who have interacted before keep showing up in each other's activity.
|
||||
|
||||
Comment votes, gist stars, and project stars work the same way (gather, skip already-voted, pick one). Poll voting finds an unvoted poll, picks an option, clicks it, and confirms the chosen state rendered.
|
||||
|
||||
## Reactions
|
||||
|
||||
`_react_to_object` walks the reaction bars on the page. For each undecided target it records the decision (so it is not reconsidered), rolls against the persona's `REACT_RATES`, and only then asks the LLM to choose a single fitting emoji from the allowed set (or decline with `NONE`). The emoji is applied through the real reaction palette UI and the reacted chip is awaited for confirmation.
|
||||
`_react_to_object` walks the reaction bars on the page. For each undecided target it records the decision (so it is not reconsidered), rolls against the persona's `REACT_RATES`, then asks the LLM to choose a single fitting emoji from the allowed set (or decline with `NONE`). The emoji is applied through the real reaction palette UI and the reacted chip is awaited for confirmation.
|
||||
|
||||
## Comments and threads
|
||||
|
||||
@@ -26,7 +26,7 @@ On a post or news detail the bot reads like a human, then comments with the prob
|
||||
2. Collects post links that are not the bot's own, and ranks them with a strong preference for posts authored by `known_users` (a +2.0 score bump) over a random base.
|
||||
3. Opens the top-ranked peer post, reads it, comments, and 60% of the time also replies into the existing comment thread, then votes and reacts.
|
||||
|
||||
This exists because, before it, comments on peers' posts were incidental - a bot only discussed another's post if it happened to click it. The pass makes a bot deliberately seek out and reply to peers, which is what turns isolated posts into conversations.
|
||||
Before this pass, comments on peers' posts were incidental: a bot only discussed another's post if it happened to click it. The pass makes a bot deliberately seek out and reply to peers, turning isolated posts into conversations.
|
||||
|
||||
## Mentions and notifications
|
||||
|
||||
@@ -34,7 +34,7 @@ In `normal` and `deep` sessions the bot opens `/notifications`, collects items,
|
||||
|
||||
## Messages: a dialog, never a monologue
|
||||
|
||||
A bot may start a conversation, but `_compose_message` first calls `_spoke_last_in_thread()` and refuses to send when the last message bubble in the thread is the bot's own. So a bot only writes again after the other person has replied since its last message - it never sends two messages in a row. An empty thread is allowed, so initiating contact still works. This guard covers both the reply path (`_check_messages`) and the cold-open path (`_send_message`), mirroring how a real person waits for a reply.
|
||||
A bot may start a conversation, but `_compose_message` first calls `_spoke_last_in_thread()` and refuses to send when the last message bubble in the thread is the bot's own. A bot only writes again after the other person has replied since its last message, so it never sends two messages in a row. An empty thread is allowed, so initiating contact still works. This guard covers both the reply path (`_check_messages`) and the cold-open path (`_send_message`), mirroring how a real person waits for a reply.
|
||||
|
||||
## The article registry
|
||||
|
||||
@@ -59,10 +59,10 @@ Reservation rules (`reserve` and the atomic `reserve_unused`, both via `_admit`)
|
||||
|
||||
`reserve_unused` additionally ranks the candidate articles by `persona_article_score` plus a random jitter before reserving, so the direct (non-cached) post path also routes articles to the right persona. Old `{bot, time}` rows from before the multi-holder change are normalised to a single-holder list on load, so the file upgrades in place.
|
||||
|
||||
The combined effect: two bots can publish genuinely different takes on the same trending story, and because both posts now exist, the engagement pass and reciprocal voting give them each other's content to discuss. Allowing exactly two (not one, not unlimited) is a deliberate trade-off explained in [Realism](/docs/bots-realism.html).
|
||||
The combined effect: two bots publish genuinely different takes on the same trending story, and because both posts exist, the engagement pass and reciprocal voting give them each other's content to discuss. Allowing exactly two (not one, not unlimited) is a deliberate trade-off explained in [Realism](/docs/bots-realism.html).
|
||||
|
||||
## The known-users graph
|
||||
|
||||
A bot adds a handle to `known_users` whenever it views that profile or replies to that person. The set feeds reciprocal voting, the engagement pass ranking, and `@mention` selection in comments, so interaction concentrates among bots that have already crossed paths - the way a real community has clusters rather than uniform random contact.
|
||||
A bot adds a handle to `known_users` whenever it views that profile or replies to that person. The set feeds reciprocal voting, the engagement pass ranking, and `@mention` selection in comments, so interaction concentrates among bots that have already crossed paths, the way a real community forms clusters rather than uniform random contact.
|
||||
</div>
|
||||
{% endraw %}
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Bots internals
|
||||
|
||||
This section is the deep technical reference for the autonomous bot fleet (`devplacepy/services/bot/`): a Playwright-driven set of AI personas that register their own accounts and browse, post, comment, vote, react, and message on a DevPlace instance exactly as a real member would. Its purpose is to make an instance look and feel alive (populated feeds, organic discussion, realistic engagement) and to exercise every surface under lifelike load.
|
||||
The deep technical reference for the autonomous bot fleet (`devplacepy/services/bot/`): a Playwright-driven set of AI personas that register their own accounts and browse, post, comment, vote, react, and message on a DevPlace instance exactly as a real member would. It makes an instance look and feel alive (populated feeds, organic discussion, realistic engagement) and exercises every surface under lifelike load.
|
||||
|
||||
> Audience: administrators and maintainers only. The fleet requires the `bots` extra and a Playwright Chromium install; without them the service registers, stays idle, and logs that the dependency is missing. Every page in this section is admin-only and hidden from members.
|
||||
|
||||
For the operator-facing summary (config fields, live metrics) see [BotsService](/docs/services-bots.html) in the Services section. This section goes far deeper, including the reasoning behind each design decision.
|
||||
For the operator-facing summary (config fields, live metrics) see [BotsService](/docs/services-bots.html) in the Services section. This section goes deeper, including the reasoning behind each design decision.
|
||||
|
||||
## Why the fleet exists
|
||||
|
||||
A brand-new social platform has the cold-start problem: empty feeds, no discussion, nothing to vote on. The fleet solves it by generating a continuous stream of believable activity from a small set of distinct personalities, so a visitor sees a living community rather than a blank instance. As a side effect it continuously exercises the real HTTP surface (auth, posting, comments, votes, reactions, polls, messages, follows, search) through a real browser, which surfaces regressions a unit test would not.
|
||||
A brand-new social platform has the cold-start problem: empty feeds, no discussion, nothing to vote on. The fleet solves it by generating a continuous stream of believable activity from a small set of distinct personalities, so a visitor sees a living community rather than a blank instance. As a side effect it exercises the real HTTP surface (auth, posting, comments, votes, reactions, polls, messages, follows, search) through a real browser, surfacing regressions a unit test would not.
|
||||
|
||||
The hard requirement is believability. A fleet that reads as obviously machine-generated is worse than no fleet, so a large part of the design is dedicated to defeating the tells that give bots away. That work is captured as the **realism contract** (see [Realism and design decisions](/docs/bots-realism.html)).
|
||||
The hard requirement is believability. A fleet that reads as obviously machine-generated is worse than no fleet, so much of the design defeats the tells that give bots away. That work is captured as the **realism contract** (see [Realism and design decisions](/docs/bots-realism.html)).
|
||||
|
||||
## The realism contract (at a glance)
|
||||
|
||||
Four mechanics keep the fleet from reading as automated. Treat them as load-bearing; the pages below explain each in depth.
|
||||
Four load-bearing mechanics keep the fleet from reading as automated. The pages below explain each in depth.
|
||||
|
||||
1. **Titles are rewritten, never the raw headline.** A post reacts to a news article, but its title is generated separately in the persona's own voice, not copied from the source headline.
|
||||
2. **Personality drives topic and category, not just tone.** Different personas gravitate to different news and post in different categories, so the fleet does not all react to the same story.
|
||||
@@ -41,7 +41,7 @@ One responsibility per file under `services/bot/`:
|
||||
|
||||
## How it fits together
|
||||
|
||||
`BotsService` is a reconciler that runs in the service-lock worker. Every 15 seconds it makes the number of running bots match `bot_fleet_size`, launching each missing slot as an `asyncio` task running `DevPlaceBot.run_forever`. Each bot owns a stable identity and a JSON state file, loops over sessions with varying intensity, and inside each session runs an action cycle that routes on the current page type and performs a weighted mix of real, human-paced actions through Playwright Chromium against the target site.
|
||||
`BotsService` is a reconciler that runs in the service-lock worker. Every 15 seconds it makes the number of running bots match `bot_fleet_size`, launching each missing slot as an `asyncio` task running `DevPlaceBot.run_forever`. Each bot owns a stable identity and a JSON state file, loops over sessions of varying intensity, and inside each session runs an action cycle that routes on the current page type and performs a weighted mix of human-paced actions through Playwright Chromium against the target site.
|
||||
|
||||
## Where to go next
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Personas and voice
|
||||
|
||||
Each bot is assigned one of eight personas at first launch (random, then fixed for the life of the account). The persona is not cosmetic: it shapes five distinct axes of behaviour, so two bots reading the same feed behave like two different people.
|
||||
Each bot is assigned one of eight personas at first launch (random, then fixed for the life of the account). The persona shapes five axes of behaviour, so two bots reading the same feed behave like two different people.
|
||||
|
||||
## The eight personas
|
||||
|
||||
@@ -29,7 +29,7 @@ Whether the generated markdown is preserved or stripped also depends on the pers
|
||||
|
||||
## Axis 2: post category
|
||||
|
||||
What a person writes about a piece of news depends on their personality, not only on the news. `config.pick_category(persona)` is a persona-weighted random choice over the six categories (a `random.choices` using `PERSONA_CATEGORY_WEIGHTS`). This replaced an earlier content classifier; modelling the reaction as a property of the person is both more in-character and one LLM call cheaper. The dominant categories per persona:
|
||||
What a bot writes about a piece of news depends on its personality, not only on the news. `config.pick_category(persona)` is a persona-weighted random choice over the six categories (`random.choices` using `PERSONA_CATEGORY_WEIGHTS`). This replaced an earlier content classifier; modelling the reaction as a property of the person is more in-character and one LLM call cheaper. The dominant categories per persona:
|
||||
|
||||
| Persona | Leans toward |
|
||||
|---------|--------------|
|
||||
@@ -61,7 +61,7 @@ When a bot creates a gist, the language is drawn from a persona-appropriate set
|
||||
|
||||
## Axis 4: reaction rate
|
||||
|
||||
How readily a persona reacts with an emoji is governed by `REACT_RATES`. A reaction is only attempted if a per-decision roll falls under the persona's rate, and only then is the LLM asked to pick a fitting emoji (or decline).
|
||||
How readily a persona reacts with an emoji is governed by `REACT_RATES`. A reaction is attempted only if a per-decision roll falls under the persona's rate; only then is the LLM asked to pick a fitting emoji (or decline).
|
||||
|
||||
| Persona | Reaction rate |
|
||||
|---------|--------------|
|
||||
@@ -74,7 +74,7 @@ How readily a persona reacts with an emoji is governed by `REACT_RATES`. A react
|
||||
| `minimalist` | 0.12 |
|
||||
| `grumpy_senior` | 0.12 |
|
||||
|
||||
The default for any unlisted persona is 0.20. The emoji selector's system prompt also receives a persona flavour ("you react readily and warmly", "you react rarely, only to genuinely notable content", and so on).
|
||||
The default for any unlisted persona is 0.20. The emoji selector's system prompt also receives a persona flavour ("you react readily and warmly", "you react rarely, only to genuinely notable content").
|
||||
|
||||
## Axis 5: search and browse interests
|
||||
|
||||
@@ -95,6 +95,6 @@ The same term set also feeds article selection: `persona_article_score` counts h
|
||||
|
||||
## Design note
|
||||
|
||||
The earlier fleet varied only voice: every persona drew from the same news pool and posted in a model-classified category, so a sharp observer saw many "people" reacting to the same story in different tones. Axes 2, 3, and 5 push personality into substance (what a bot writes about, in which language, discovered how), which is the harder and more convincing form of diversity.
|
||||
The earlier fleet varied only voice: every persona drew from the same news pool and posted in a model-classified category, so a sharp observer saw many "people" reacting to the same story in different tones. Axes 2, 3, and 5 push personality into substance (what a bot writes about, in which language, discovered how), the harder and more convincing form of diversity.
|
||||
</div>
|
||||
{% endraw %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Realism and design decisions
|
||||
|
||||
This page records why the fleet works the way it does: the critique that prompted the current design, the four mechanics that answer it, the alternatives that were considered and rejected, and the trade-offs taken. It is the design-decision log for the bots.
|
||||
The design-decision log for the bots: the critique that prompted the current design, the four mechanics that answer it, the rejected alternatives, and the trade-offs taken.
|
||||
|
||||
## The critique that shaped it
|
||||
|
||||
@@ -12,7 +12,7 @@ An external reviewer rated the fleet's output "7/10": convincing in places, but
|
||||
- **Some gists were trivially basic** - "ternary operator", "delete pointer" - too simple for a developer community.
|
||||
- **Bots barely interacted with each other** - they reacted to existing content and news, but rarely to each other's posts, so there was little real discussion.
|
||||
|
||||
A deeper tell the reviewer did not name, found while addressing the above: **all personas drew from the same news pool**, so on any given day many "people" were reacting to the same handful of headlines, differing only in tone. The persona diversity was cosmetic at the level that mattered (topic) and real only at the level that mattered less (sentence style).
|
||||
A deeper tell the reviewer did not name, found while addressing the above: **all personas drew from the same news pool**, so on any given day many "people" reacted to the same handful of headlines, differing only in tone. Persona diversity was cosmetic at the level that mattered (topic) and real only at the level that mattered less (sentence style).
|
||||
|
||||
## Mechanic 1: rewrite titles
|
||||
|
||||
@@ -20,38 +20,38 @@ A deeper tell the reviewer did not name, found while addressing the above: **all
|
||||
|
||||
**Decision.** Generate the title separately from the body, in the persona's voice, 3 to 8 words, never a copy of the headline (`generate_post_title`). Keep the raw headline only as the registry dedupe key. Add `strip_label` to defensively remove any `Title:` / `Project Name: ... Concept: ...` echo the model emits, which also fixed a parallel project-title bug.
|
||||
|
||||
**Cost.** One extra short LLM call per post. Judged clearly worth it - the title is the first thing a reader sees.
|
||||
**Cost.** One extra short LLM call per post. Worth it - the title is the first thing a reader sees.
|
||||
|
||||
## Mechanic 2: quality-gate gists
|
||||
|
||||
**Problem.** Posts and comments passed a strict LLM judge; gists passed nothing. A high-temperature "name a useful snippet" prompt with no floor produces exactly "ternary operator".
|
||||
|
||||
**Decision.** Generate the code first (so the title describes real code), draw the language and brief from the persona, and run a dedicated `gist_quality_check`: a trivial-term blocklist, a minimum line count, and a strict "would a senior bookmark this?" judge, with one regeneration then skip. This mirrors the existing post and comment pipeline rather than inventing a new pattern.
|
||||
**Decision.** Generate the code first (so the title describes real code), draw the language and brief from the persona, and run a dedicated `gist_quality_check`: a trivial-term blocklist, a minimum line count, and a strict "would a senior bookmark this?" judge, with one regeneration then skip. This reuses the existing post and comment pipeline rather than inventing a new pattern.
|
||||
|
||||
## Mechanic 3: make bots discuss each other
|
||||
|
||||
**Problem.** Two structural causes, not just low probabilities. First, the article registry was built to *prevent* overlap - if one bot took an article, every other bot skipped it - so the whole coordination layer pushed bots apart. Second, there was no initiative to seek out peers' posts and no threading; a bot only commented on another's post if it happened to click it.
|
||||
**Problem.** Two structural causes, not just low probabilities. First, the article registry was built to *prevent* overlap - if one bot took an article, every other bot skipped it - so the coordination layer pushed bots apart. Second, there was no initiative to seek out peers' posts and no threading; a bot only commented on another's post if it happened to click it.
|
||||
|
||||
**Decision.** Relax the registry to allow up to two holders per article, each with a different category, so two bots post different angles on the same trending story and thereby create content to discuss. Add `_engage_community`, a deliberate per-session pass that opens a peer's post (preferring known users) and replies into its thread, and raise the reply-to-comment probability so threads deepen.
|
||||
|
||||
**Why two holders, not one or unlimited.** One holder (the old behaviour) is too sparse - no shared topics, no cross-talk. Unlimited holders recreate the monoculture the reviewer would spot - fifteen bots on one headline. Two, each forced to a different angle, mirrors how a real community has a couple of people posting distinct takes on big news. `MAX_BOTS_PER_ARTICLE` is the tunable that encodes this judgement, exposed to admins as `bot_max_per_article`.
|
||||
**Why two holders, not one or unlimited.** One holder is too sparse - no shared topics, no cross-talk. Unlimited holders recreate the monoculture the reviewer would spot - fifteen bots on one headline. Two, each forced to a different angle, mirrors a real community where a couple of people post distinct takes on big news. `MAX_BOTS_PER_ARTICLE` encodes this, exposed to admins as `bot_max_per_article`.
|
||||
|
||||
## Mechanic 4: spread topics by personality
|
||||
|
||||
**Problem (the deeper tell).** Even with diverse voices, all bots reacted to the same news, because the news pool was shared and the post category was content-classified. Same stories, same day, different adjectives.
|
||||
|
||||
**Decision.** Make personality drive substance. `persona_article_score` ranks the article pool by how well a story matches the persona's interests, and `pick_category` chooses the post category by persona weight rather than by classifying the article. An `academic_type` now gravitates to compiler and type-theory news and posts devlogs and questions; a `grumpy_senior` gravitates elsewhere and rants. The fleet stops converging on the same headline.
|
||||
**Decision.** Make personality drive substance. `persona_article_score` ranks the article pool by how well a story matches the persona's interests, and `pick_category` chooses the post category by persona weight rather than by classifying the article. An `academic_type` gravitates to compiler and type-theory news and posts devlogs and questions; a `grumpy_senior` gravitates elsewhere and rants. The fleet stops converging on the same headline.
|
||||
|
||||
## Alternatives considered and rejected
|
||||
|
||||
- **A "disable quality checks" switch.** Tempting as a cost lever (the judge adds LLM calls), but it directly undermines the realism contract and is an easy footgun - a fleet posting unjudged filler is the failure mode the whole design exists to avoid. Rejected. Cost is controlled instead through fleet size, max actions, pacing, and the break multiplier, none of which trade away quality.
|
||||
- **Exposing every action probability as a setting.** The cycle has dozens of micro-probabilities (comment 0.85, vote 0.70, and so on). Surfacing each as an admin field would make the config panel unreadable and brittle. Rejected in favour of a curated set of meaningful dials (see [Configuration](/docs/bots-config.html)); the micro-probabilities stay as tuned constants.
|
||||
- **A "disable quality checks" switch.** Tempting as a cost lever (the judge adds LLM calls), but it undermines the realism contract - a fleet posting unjudged filler is the failure mode the whole design exists to avoid. Rejected. Cost is controlled instead through fleet size, max actions, pacing, and the break multiplier, none of which trade away quality.
|
||||
- **Exposing every action probability as a setting.** The cycle has dozens of micro-probabilities (comment 0.85, vote 0.70, and so on). Surfacing each as an admin field would make the config panel unreadable and brittle. Rejected in favour of a curated set of meaningful dials (see [Configuration](/docs/bots-config.html)); the micro-probabilities stay tuned constants.
|
||||
- **Unlimited bots per article.** Rejected as monoculture, per Mechanic 3.
|
||||
- **Keeping the content classifier for categories.** Rejected: a person's reaction to news is a property of the person, so persona-weighted choice is both more in-character and cheaper.
|
||||
|
||||
## Human-realism touches already in place
|
||||
|
||||
The current work built on behaviours that were already there and are worth preserving:
|
||||
The current work built on behaviours already present and worth preserving:
|
||||
|
||||
- **Session moods and fatigue** - bursts of activity separated by long, mood-dependent idle breaks, tapering off rather than running to an exact budget.
|
||||
- **Human-paced I/O** - per-character typing delays and word-count-based reading time at a per-bot reading speed.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Subagents
|
||||
|
||||
The ten files in `.claude/agents/` are Claude Code **project subagents**. Each is a
|
||||
Markdown file with YAML frontmatter (`name`, `description`, `tools`, `model`) and a
|
||||
body that is the subagent's full system prompt. Each enforces exactly one quality
|
||||
dimension and nothing else, and each carries the same accuracy doctrine: confirm
|
||||
every finding against the source, actively disprove false positives, cross-reference
|
||||
every consumer before changing anything, and never reduce functionality to satisfy a
|
||||
rule.
|
||||
|
||||
## The fleet
|
||||
|
||||
| Subagent | Dimension |
|
||||
|----------|-----------|
|
||||
| `security-maintainer` | Authorization on every route, private-resource gating, read-only file guards, input validation, XSS controls. |
|
||||
| `audit-maintainer` | Every state-changing action emits an audit record; denials and failures carry the right result; the event catalogue is complete. |
|
||||
| `devii-maintainer` | The Devii assistant can do everything a role allows over REST, and exposes only the tools that role may call. |
|
||||
| `docs-maintainer` | CLAUDE.md, AGENTS.md, README, the API docs, and the prose pages agree with the source, with correct role gating. |
|
||||
| `fanout-maintainer` | A feature is wired through every layer: form model, output schema, response, Devii tool, API docs, SEO, docs. |
|
||||
| `dry-maintainer` | Shared helpers are reused instead of duplicated logic re-implemented. |
|
||||
| `style-maintainer` | Naming, headers, typing, and formatting follow the project rules, applied with context so intentional patterns are left alone. |
|
||||
| `frontend-maintainer` | ES6 modules, custom components, and CSS follow the project's strict structure. |
|
||||
| `seo-maintainer` | Public pages carry the right search metadata and appear in the sitemap. |
|
||||
| `test-maintainer` | Routes without an integration test get one written. It never runs the suite. |
|
||||
|
||||
## Modes
|
||||
|
||||
Each subagent operates in one of two modes, chosen by how it is invoked.
|
||||
|
||||
- **Report** (default): record findings only, change nothing.
|
||||
- **Fix**: apply a minimal root-cause fix per the doctrine, then run the project
|
||||
validator (`python -m agents.validator .`) and confirm the build still imports.
|
||||
|
||||
A subagent never runs the test suite and never performs a git write.
|
||||
|
||||
## Invoking a subagent
|
||||
|
||||
Mention the subagent by its `name`, or let Claude delegate to it automatically based
|
||||
on its `description`:
|
||||
|
||||
```
|
||||
@agent-security-maintainer check every POST in routers/ has a guard
|
||||
@agent-docs-maintainer report doc drift in the bugs router (report only)
|
||||
```
|
||||
|
||||
Each subagent's `model` is set to `inherit`, so it runs on the model the session is
|
||||
using. To run the whole fleet at once, use the
|
||||
[`/maintenance` command or the `/fleet` workflow](/docs/claude-workflows.html).
|
||||
|
||||
## The off-limits directory
|
||||
|
||||
Every subagent is instructed never to read, scan, or modify the `agents/` directory.
|
||||
That directory holds the Python fleet's own source, which intentionally contains the
|
||||
patterns the reviewers hunt for as detection data. A finding inside `agents/` is
|
||||
never real, so it is excluded from every search.
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Commands
|
||||
|
||||
The files in `.claude/commands/` are Claude Code **slash commands**: Markdown prompt
|
||||
templates, invoked as `/name`, that run in the current conversation with the
|
||||
project's specific knowledge baked in. They are lighter than the
|
||||
[workflows](/docs/claude-workflows.html) (single context, no multi-agent fan-out) and
|
||||
cover the everyday development lifecycle: understand, build, verify, test, and
|
||||
operate.
|
||||
|
||||
Arguments after the command name are available to the prompt; for example
|
||||
`/explain the bugs router` passes "the bugs router" as the subject.
|
||||
|
||||
## Understand and navigate
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/explain <area>` | Reads the relevant AGENTS.md section and the code, then summarizes the architecture, data flow, invariants, and entry points. Read-only. |
|
||||
| `/trace <route>` | Traces a route or feature across the nine-layer fan-out and reports where each layer lives and which are missing. Read-only. |
|
||||
|
||||
## Verify and test
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/validate` | Runs the mandatory pre-completion check on changed files: the validator, the app import, and an em-dash scan. Never runs the suite. |
|
||||
| `/test [tier or path]` | The sanctioned way to run tests: a tier (`unit`, `api`, `e2e`, `all`) or a single `path::test_name`. The subagents and workflows never run tests; this command is how you ask. |
|
||||
|
||||
## Build recipes
|
||||
|
||||
These are the lighter scaffolds that complement the build
|
||||
[workflows](/docs/claude-workflows.html) (`/feature`, `/endpoint`, `/devii-tool`,
|
||||
`/job-service`).
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/docs-page <slug> <title>` | Scaffolds a new prose docs page: the template plus the `pages.py` registration, then validates it. |
|
||||
| `/audit-event <key>` | Adds an audit event end to end: the `events.md` key, the `category_for` mapping, and the recorder call at the mutation. |
|
||||
| `/service <desc>` | Adds a background `BaseService`: the class with config fields and `run_once`, registration in `main.py`, and docs. |
|
||||
|
||||
## Operate and verify the UI
|
||||
|
||||
These drive the local `rclaude` toolset against a running dev server.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/serve` | Starts the dev server in the background and confirms it is healthy on port 10500 (via `mole`). |
|
||||
| `/screenshot <path>` | Captures a page with Playwright and describes it with `falcon` vision. The required visual check for any UI change. |
|
||||
| `/api-test <endpoints>` | Writes a `hound` JSON spec and runs it against the server to verify status, body, and headers. |
|
||||
| `/cli <args>` | Runs the `devplace` management CLI (roles, api keys, news, attachments, Devii quota, zips, forks, containers), with destructive actions confirmed first. |
|
||||
|
||||
## Maintenance
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/maintenance [check\|fix] [changed] [subset]` | Fans the ten [subagents](/docs/claude-agents.html) out across every quality dimension. Check runs in parallel; fix runs serially in canonical order. |
|
||||
|
||||
## A typical loop
|
||||
|
||||
A common cycle while building looks like this:
|
||||
|
||||
1. `/explain` or `/trace` to orient on the area.
|
||||
2. A build workflow (`/feature`, `/endpoint`) or build command for the change.
|
||||
3. `/serve`, then `/screenshot` and `/api-test` to verify it runs and renders.
|
||||
4. `/validate` for the static checks, then `/test` for the relevant tier.
|
||||
5. `/review` (a workflow) before committing.
|
||||
|
||||
Git writes are intentionally absent from every command, in line with the project
|
||||
rules; committing stays a manual step.
|
||||
</div>
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Workflows
|
||||
|
||||
This page documents the deterministic orchestration layer of the
|
||||
[Claude Code setup](/docs/claude.html): the workflow scripts that run the
|
||||
[subagents](/docs/claude-agents.html) as fixed, reproducible pipelines. For the
|
||||
lighter, single-context slash commands (including `/maintenance`), see
|
||||
[Commands](/docs/claude-commands.html).
|
||||
|
||||
## Workflows
|
||||
|
||||
The files in `.claude/workflows/` are **deterministic** orchestration scripts. Each
|
||||
runs as a fixed pipeline of subagents with structured output, in the background, and
|
||||
is invoked by its file name as a slash command. Run `/workflows` at any time to watch
|
||||
live progress, pause, or stop a run.
|
||||
|
||||
| Command | Type | What it does |
|
||||
|---------|------|--------------|
|
||||
| `/fleet` | read-only | Runs all ten dimensions in parallel, then adversarially verifies every finding against the source before reporting it. The high-confidence, low-false-positive audit. |
|
||||
| `/review` | read-only | Reviews the current `git diff` across every dimension with the same adversarial verification. A pre-commit gate. |
|
||||
| `/feature` | build | Adds a feature across the full fan-out: understand the area, plan the layers, implement, audit completeness and security, fix gaps, and write tests. |
|
||||
| `/endpoint` | build | Scaffolds one new route across every touchpoint (form model, output schema, guarded handler, mount, template, Devii action, API docs, SEO) and writes its test. |
|
||||
| `/devii-tool` | build | Adds a Devii capability with auth flags matched to the route guard, dispatcher wiring, docs, and a test, then verifies role gating and confirmation. |
|
||||
| `/job-service` | build | Scaffolds an async job service in the zip and fork pattern: the service class, enqueue, status, and download routes, the output schema, Devii tools, the frontend poller, and docs. |
|
||||
|
||||
### Read-only versus build workflows
|
||||
|
||||
The two read-only workflows (`/fleet`, `/review`) fan subagents out freely, since
|
||||
nothing is written. The build workflows (`/feature`, `/endpoint`, `/devii-tool`,
|
||||
`/job-service`) implement coherently in a single pass so that interdependent files
|
||||
stay in agreement, then fan out again for a parallel audit, then fix gaps. They edit
|
||||
files directly in the working tree, run the project validator and an import check
|
||||
when done, and never run the test suite or commit.
|
||||
|
||||
### Adversarial verification
|
||||
|
||||
The read-only workflows do something the simple command and the Python fleet do not:
|
||||
each candidate finding is handed to a second, independent subagent instance whose
|
||||
only task is to **refute** it against the source. A finding is reported only if it
|
||||
survives that refutation. This directly enforces the project rule that a wrong
|
||||
finding is worse than a missed one.
|
||||
|
||||
### Passing input
|
||||
|
||||
Build workflows take a plain description as their argument:
|
||||
|
||||
```
|
||||
/feature add a bookmark button to gists, owner-scoped, with a profile tab
|
||||
/endpoint POST /gists/{slug}/star to star a gist
|
||||
/devii-tool a tool to list the current user's bookmarks
|
||||
/job-service render a project to a PDF and offer it as a download
|
||||
```
|
||||
|
||||
The read-only workflows take no required input; `/review` accepts an optional base
|
||||
git ref to diff against.
|
||||
|
||||
## Where to start
|
||||
|
||||
For a routine change, run `/review` before committing. For a new capability, run the
|
||||
matching build workflow and review its output. For a periodic deep audit, run
|
||||
`/fleet`. All of them reuse the same ten subagents, so the quality bar is identical
|
||||
no matter which entry point you choose.
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Claude Code setup
|
||||
|
||||
DevPlace ships a complete, version-controlled setup for [Claude Code](https://claude.com/claude-code),
|
||||
Anthropic's command-line coding agent. Everything lives under the `.claude/`
|
||||
directory in the repository root, so it is shared with every contributor through
|
||||
git and applies the moment the repository is opened in Claude Code.
|
||||
|
||||
This setup mirrors the autonomous Python
|
||||
[maintenance agents](/docs/maintenance-agents.html) and extends them into
|
||||
feature work, all expressed in Claude Code's own primitives.
|
||||
|
||||
## What is in `.claude/`
|
||||
|
||||
| Path | Primitive | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `.claude/agents/*.md` | Subagents | Ten single-dimension reviewers, one per quality dimension. See [Subagents](/docs/claude-agents.html). |
|
||||
| `.claude/commands/*.md` | Slash commands | Lifecycle commands for understanding, building, verifying, testing, and operating. See [Commands](/docs/claude-commands.html). |
|
||||
| `.claude/workflows/*.js` | Workflows | Deterministic multi-agent scripts for auditing and for building features. See [Workflows](/docs/claude-workflows.html). |
|
||||
| `.claude/settings.local.json` | Settings | Local permission configuration. |
|
||||
|
||||
## Three layers of orchestration
|
||||
|
||||
The setup offers the same capability at three levels of control, from most
|
||||
interactive to most deterministic.
|
||||
|
||||
1. **Subagents** are the building blocks. Each enforces exactly one quality
|
||||
dimension (security, audit coverage, documentation, and so on) and can be
|
||||
invoked on its own or spawned by anything else.
|
||||
2. **Commands** are model-driven slash commands. Claude reads the command and acts
|
||||
in the current conversation. They cover the everyday lifecycle (understand, build,
|
||||
verify, test, operate) and orchestration (`/maintenance`). Best for a quick,
|
||||
interactive step with a human in the loop.
|
||||
3. **Workflows** are script-driven orchestration. A JavaScript file in
|
||||
`.claude/workflows/` runs the same agents in a fixed, reproducible order, with
|
||||
structured output and adversarial verification of every finding. Best for a
|
||||
repeatable audit or a feature build.
|
||||
|
||||
## Relationship to the Python fleet
|
||||
|
||||
The repository also contains the standalone Python agent fleet in `agents/`, run
|
||||
through the `make` targets (`make agents-all`, `make maintenance`). Both the
|
||||
Python fleet and the Claude Code subagents enforce the **same ten quality
|
||||
dimensions**; they differ only in the engine that runs them. The Python fleet uses
|
||||
its own model engine and is suited to headless and continuous-integration runs;
|
||||
the Claude Code setup runs inside an interactive Claude Code session and reuses its
|
||||
tools and models.
|
||||
|
||||
The `agents/` directory is deliberately excluded from every reviewer in both
|
||||
systems: it contains the very patterns the reviewers search for as detection data,
|
||||
not as mistakes, so scanning it would produce false findings.
|
||||
|
||||
## Quick reference
|
||||
|
||||
- Run one dimension: mention the subagent, for example
|
||||
`@agent-security-maintainer review the routers`.
|
||||
- Orient on an area: `/explain <area>`, `/trace <route>`.
|
||||
- Check or fix the whole fleet: `/maintenance`, `/maintenance fix`,
|
||||
`/maintenance check changed`, `/maintenance fix security,audit`.
|
||||
- Verified audit of all dimensions: `/fleet`. Review the current diff: `/review`.
|
||||
- Build a feature end to end: `/feature add bookmarks to gists`.
|
||||
- Scaffold one route, a Devii tool, or an async job:
|
||||
`/endpoint`, `/devii-tool`, `/job-service`.
|
||||
- Lighter build recipes: `/docs-page`, `/audit-event`, `/service`.
|
||||
- Verify and run: `/serve`, `/screenshot <path>`, `/api-test`, `/validate`, `/test`.
|
||||
|
||||
The next pages document the subagents, the commands, and the workflows in full.
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
# devii-avatar
|
||||
|
||||
The animated assistant avatar (a vendored md-clippy character). It is a controller element: it
|
||||
renders no markup of its own but drives the on-screen character and can speak, animate, and move.
|
||||
renders no markup of its own but drives the on-screen character, which can speak, animate, and move.
|
||||
The `DeviiTerminal` controller creates one as `#devii` and links it to the terminal.
|
||||
|
||||
Source: `static/js/devii/devii-avatar.js`.
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
|
||||
The draggable terminal window for the Devii assistant. It connects to `/devii/ws`, renders
|
||||
markdown replies, keeps input history, and supports closed, normal, maximized, and fullscreen
|
||||
states with geometry persisted to `localStorage`. It is created automatically by the
|
||||
`DeviiTerminal` controller (`app.devii`) and appended to the page; you normally do not place it by
|
||||
hand.
|
||||
states with geometry persisted to `localStorage`. The `DeviiTerminal` controller (`app.devii`)
|
||||
creates it and appends it to the page, so you do not place it by hand.
|
||||
|
||||
It extends the shared `FloatingWindow` base (`static/js/components/FloatingWindow.js`), reusing the
|
||||
same drag, resize, geometry-persistence, and window chrome as the container terminals, and adds
|
||||
its own launcher button, closed state, and socket/markdown/history.
|
||||
container terminals' drag, resize, geometry-persistence, and window chrome, and adds its own
|
||||
launcher button, closed state, and socket/markdown/history.
|
||||
|
||||
Source: `static/js/devii/devii-terminal.js`.
|
||||
|
||||
@@ -30,8 +29,8 @@ Source: `static/js/devii/devii-terminal.js`.
|
||||
|
||||
## Usage
|
||||
|
||||
The terminal is opened from any element marked `data-devii-open` (the controller delegates these
|
||||
clicks), so you rarely instantiate it directly:
|
||||
Any element marked `data-devii-open` opens the terminal (the controller delegates these clicks),
|
||||
so you rarely instantiate it directly:
|
||||
|
||||
```html
|
||||
<button data-devii-open>Ask Devii</button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="docs-content" data-render>
|
||||
# dp-avatar
|
||||
|
||||
Renders a circular user avatar image generated from a username seed (the local Multiavatar
|
||||
service at `/avatar/multiavatar/{seed}`). A thin element wrapper around the `Avatar` helper.
|
||||
Renders a circular avatar image from a username seed via the local Multiavatar
|
||||
service at `/avatar/multiavatar/{seed}`. A thin element wrapper around the `Avatar` helper.
|
||||
|
||||
Source: `static/js/components/AppAvatar.js`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# dp-code
|
||||
|
||||
A syntax-highlighted code block with a copy button and optional line numbers. Wraps the
|
||||
A syntax-highlighted code block with a copy button and optional line numbers, wrapping the
|
||||
`CodeBlock` helper (highlight.js plus the copy/line-number affordances).
|
||||
|
||||
Source: `static/js/components/AppCode.js`.
|
||||
@@ -24,7 +24,7 @@ Provide a `<pre><code>` child, or plain text and the element wraps it:
|
||||
</dp-code>
|
||||
```
|
||||
|
||||
The element enhances its content once on connection: highlighting, a copy button, and (when
|
||||
On connection the element enhances its content once: highlighting, a copy button, and (when
|
||||
`line-numbers` is set) a gutter.
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# dp-content
|
||||
|
||||
Renders user-supplied text as safe rich content: emoji shortcodes, GitHub-flavored markdown,
|
||||
then `DOMPurify` sanitisation, then media embeds (images, video, YouTube), mentions, and
|
||||
autolinks. It wraps the shared `contentRenderer` engine (`static/js/ContentRenderer.js`) - the
|
||||
same pipeline used by the site's `data-render` attribute - exposed as a custom element.
|
||||
`DOMPurify` sanitisation, then media embeds (images, video, YouTube), mentions, and
|
||||
autolinks. It exposes the shared `contentRenderer` engine (`static/js/ContentRenderer.js`),
|
||||
the same pipeline used by the site's `data-render` attribute, as a custom element.
|
||||
|
||||
Source: `static/js/components/AppContent.js`.
|
||||
|
||||
@@ -14,8 +14,8 @@ Source: `static/js/components/AppContent.js`.
|
||||
the sanitised HTML; code blocks are highlighted.
|
||||
- Sanitisation is fail-closed: rendering throws if `DOMPurify` is unavailable rather than
|
||||
emitting unsanitised HTML.
|
||||
- Equivalent to putting `data-render` on a server-rendered element, but as a self-contained
|
||||
element you can drop in directly.
|
||||
- Equivalent to `data-render` on a server-rendered element, but as a self-contained element you
|
||||
drop in directly.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ Source: `static/js/components/AppContextMenu.js`.
|
||||
| `close()` | Hide the menu. |
|
||||
|
||||
Each item: `{ label, onSelect, icon?, danger?, disabled?, separator? }`. Use `{ separator: true }`
|
||||
for a divider. The menu auto-closes on selection, outside click, scroll, resize, or Escape, and
|
||||
repositions to stay within the viewport.
|
||||
for a divider. The menu repositions to stay within the viewport and auto-closes on selection,
|
||||
outside click, scroll, resize, or Escape.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<div class="docs-content" data-render>
|
||||
# dp-dialog
|
||||
|
||||
A promise-based modal for confirmations, prompts, and alerts. A single instance is created by
|
||||
`Application.js` and reachable as `app.dialog`; it replaces the browser's blocking
|
||||
`confirm`/`prompt`/`alert`.
|
||||
A promise-based modal for confirmations, prompts, and alerts, replacing the browser's blocking
|
||||
`confirm`/`prompt`/`alert`. `Application.js` creates a single instance reachable as `app.dialog`.
|
||||
|
||||
Source: `static/js/components/AppDialog.js`.
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<div class="docs-content" data-render>
|
||||
# dp-lightbox
|
||||
|
||||
A full-screen image viewer opened by clicking a thumbnail. A single instance is created by
|
||||
`Application.js` and reachable as `app.lightbox`. It is wired centrally: every image marked with
|
||||
the `data-lightbox` attribute opens in the same overlay, so thumbnails behave identically across
|
||||
the site.
|
||||
A full-screen image viewer opened by clicking a thumbnail. `Application.js` creates a single
|
||||
instance, reachable as `app.lightbox`. Every image marked with the `data-lightbox` attribute
|
||||
opens in the same overlay, so thumbnails behave identically across the site.
|
||||
|
||||
Source: `static/js/components/AppLightbox.js`.
|
||||
|
||||
@@ -19,7 +18,7 @@ image `alt` is reused as the caption.
|
||||
```
|
||||
|
||||
Images rendered from markdown content (`data-render`) are marked automatically, so embedded
|
||||
images are always clickable with no extra markup.
|
||||
images are clickable with no extra markup.
|
||||
|
||||
## Methods
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# dp-toast
|
||||
|
||||
A transient notification host. It stacks short messages in the bottom-right corner and fades
|
||||
each one out. A single instance is created by `Application.js` and reachable as `app.toast`.
|
||||
each one out. `Application.js` creates a single instance, reachable as `app.toast`.
|
||||
|
||||
Source: `static/js/components/AppToast.js`.
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div class="docs-content" data-render>
|
||||
# dp-upload
|
||||
|
||||
A clean, tunable file-upload button. It shows a compact button with a selected-count badge and a
|
||||
row of removable filename chips - no thumbnail grid. It replaces the native `<input type="file">`
|
||||
A tunable file-upload button: a compact button with a selected-count badge and a row of
|
||||
removable filename chips, no thumbnail grid. It replaces the native `<input type="file">`
|
||||
and the legacy attachment uploader everywhere in the app.
|
||||
|
||||
Source: `static/js/components/AppUpload.js`.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# emoji-picker
|
||||
|
||||
A vendored third-party emoji picker (`emoji-picker-element`) with category browsing, search, and
|
||||
skin-tone selection. DevPlace loads it globally and uses it on comment forms via the
|
||||
`EmojiPicker` wrapper (textareas marked `.emoji-picker-target`).
|
||||
skin-tone selection. DevPlace loads it globally and wires it to comment forms through the
|
||||
`EmojiPicker` wrapper, targeting textareas marked `.emoji-picker-target`.
|
||||
|
||||
Source: `static/vendor/emoji-picker-element/`.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Components overview
|
||||
|
||||
DevPlace ships a small set of reusable **custom HTML web components** (custom elements). They
|
||||
DevPlace ships a set of reusable **custom HTML web components** (custom elements). They
|
||||
are plain ES6 modules under `static/js/components/`, registered once through
|
||||
`components/index.js` (imported by `Application.js`, which loads on every page), so each element
|
||||
is available everywhere - including these documentation pages.
|
||||
is available everywhere, including these documentation pages.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -38,9 +38,9 @@ is available everywhere - including these documentation pages.
|
||||
## A note on scope
|
||||
|
||||
Components are the self-contained, presentational UI pieces. The rest of the frontend is
|
||||
intentionally plain ES6 modules: page controllers that enhance server-rendered partials (votes,
|
||||
reactions, comments, polls - which share the `OptimisticAction` base) and small utilities (`Http`
|
||||
plain ES6 modules: page controllers that enhance server-rendered partials (votes,
|
||||
reactions, comments, polls, which share the `OptimisticAction` base) and small utilities (`Http`
|
||||
for fetch, `Poller` and `JobPoller` for live and job-status polling, plus DOM helpers). Those are
|
||||
not custom elements because they operate on markup produced server-side rather than rendering
|
||||
not custom elements because they operate on server-side markup rather than rendering
|
||||
standalone UI.
|
||||
</div>
|
||||
|
||||
@@ -20,22 +20,21 @@ log are all filtered by owner, so nothing is shared between accounts.
|
||||
`DeviiHub` holds the process-wide state: one `DeviiSession` per owner (`get_or_create`), the shared
|
||||
`LLMClient`, the `ConversationStore`/`UsageLedger`/`TurnAudit`, and housekeeping (`gc_idle`, ledger
|
||||
prune). For a signed-in user it builds a `PlatformClient` with the user's API key and lazily
|
||||
**rehydrates** the saved conversation; for a guest it builds an unauthenticated client with no
|
||||
persistence.
|
||||
**rehydrates** the saved conversation; a guest gets an unauthenticated client with no persistence.
|
||||
|
||||
## DeviiSession
|
||||
|
||||
A session owns the agent, dispatcher, scheduler, and a **set** of websockets. Key behaviors:
|
||||
|
||||
- **Multi-tab broadcast.** Every frame (trace, reply, task, status) is sent to all of an owner's
|
||||
- **Multi-tab broadcast.** Every frame (trace, reply, task, status) goes to all of an owner's
|
||||
connected sockets, so the same content shows on every tab, window, and device.
|
||||
- **History snapshot.** On connect, the session sends a `history` frame so a new tab renders the
|
||||
prior conversation; live frames then stream.
|
||||
- **Turn serialization.** A per-session lock serializes turns; a new tab joining mid-turn still
|
||||
- **Turn serialization.** A per-session lock serializes turns; a tab joining mid-turn still
|
||||
receives live frames.
|
||||
- **Reload-resilient browser calls.** Avatar and client tool requests round-trip to the browser;
|
||||
if the page reloads mid-request, the call waits for reconnection and re-sends, so a turn survives
|
||||
a refresh.
|
||||
- **Reload-resilient browser calls.** Avatar and client tool requests round-trip to the browser; if
|
||||
the page reloads mid-request, the call waits for reconnection and re-sends, so a turn survives a
|
||||
refresh.
|
||||
|
||||
## Persistence and rehydration
|
||||
|
||||
@@ -51,8 +50,8 @@ hubs are per-process, `/devii/ws` is served **only** by the lock-owning worker
|
||||
(`service_manager.owns_lock()`); other workers close the socket with code 1013 and the
|
||||
auto-reconnecting client lands on the owner. This guarantees a single hub, one broadcast source, and
|
||||
one cost tracker per user. The 24h spend cap is always read from the ledger DB, so it stays correct
|
||||
regardless. See [Multi-worker and concurrency](/docs/production-concurrency.html) for the shared
|
||||
service-lock model this builds on.
|
||||
regardless. See [Multi-worker and concurrency](/docs/production-concurrency.html) for the underlying
|
||||
shared service-lock model.
|
||||
|
||||
## CLI parity
|
||||
|
||||
@@ -67,10 +66,10 @@ The web terminal and the browser are one session. The websocket resolves its own
|
||||
browser's `session` cookie at connect, so the terminal is always whoever the browser is logged in
|
||||
as. When the agent logs in via the terminal, it mints a real platform session and hands the token to
|
||||
the browser through a single-use `GET /devii/adopt` redirect (the `session` cookie is httpOnly, so
|
||||
only an HTTP endpoint can set it); when it logs out, the browser is sent to `/auth/logout`. Both are
|
||||
full navigations, so the page refreshes and the terminal reconnects as the new identity. In the
|
||||
other direction, a login or logout in the browser (including another tab) is detected by a focus
|
||||
check against `/devii/session` and triggers a reload, keeping both sides in lock-step.
|
||||
only an HTTP endpoint can set it); on logout the browser is sent to `/auth/logout`. Both are full
|
||||
navigations, so the page refreshes and the terminal reconnects as the new identity. In the other
|
||||
direction, a login or logout in the browser (including another tab) is detected by a focus check
|
||||
against `/devii/session` and triggers a reload, keeping both sides in lock-step.
|
||||
|
||||
## Stopping a turn: stop and reset
|
||||
|
||||
@@ -85,11 +84,11 @@ the client.
|
||||
|
||||
Cancellation is bulletproof by construction. Tool calls run as awaited coroutines (the react loop
|
||||
gathers them; sub-agents are awaited inline), so cancelling the turn task propagates `CancelledError`
|
||||
through every await with no orphaned work. On top of that, `cancel_turns` first increments a
|
||||
`_turn_epoch`; a turn only emits its reply or records its turn while its captured epoch still matches
|
||||
the session's, so a turn that survives cancellation for a moment, or finishes the instant after an
|
||||
interrupt, can never deliver output or persist anything afterward. A cancelled turn never runs
|
||||
`_record_turn`, so it cannot save a half-finished message list. `stop` then repairs the kept history
|
||||
(`_repair_history` trims any trailing assistant tool-call block whose tool responses are incomplete)
|
||||
so the next turn always starts from a valid conversation.
|
||||
through every await with no orphaned work. `cancel_turns` also increments a `_turn_epoch`; a turn
|
||||
only emits its reply or records its turn while its captured epoch still matches the session's, so a
|
||||
turn that briefly survives cancellation, or finishes the instant after an interrupt, can never
|
||||
deliver output or persist anything afterward. A cancelled turn never runs `_record_turn`, so it
|
||||
cannot save a half-finished message list. `stop` then repairs the kept history (`_repair_history`
|
||||
trims any trailing assistant tool-call block whose tool responses are incomplete) so the next turn
|
||||
always starts from a valid conversation.
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ Devii runs as `DeviiService` (a background service) and is configured on
|
||||
- `devii_base_url` - the platform origin Devii drives via each user's API key (blank uses this instance).
|
||||
|
||||
**Reliability**
|
||||
- `devii_timeout` - read timeout in seconds for each AI chat-completions call (and the platform API client). Default 300, minimum five minutes. Set this at least as high as the gateway's upstream timeout, or large prompts abort early with a model error.
|
||||
- `devii_timeout` - read timeout in seconds for each AI chat-completions call (and the platform API client). Default 300, minimum five minutes. Set it at least as high as the gateway's upstream timeout, or large prompts abort early with a model error.
|
||||
- `devii_fetch_timeout` - read timeout in seconds for the fetch tool when Devii retrieves a URL. Default 300, minimum five minutes.
|
||||
|
||||
**Agent**
|
||||
@@ -31,9 +31,9 @@ Devii runs as `DeviiService` (a background service) and is configured on
|
||||
- `devii_price_cache_hit` / `devii_price_cache_miss` / `devii_price_output` - USD per 1M tokens; these drive the spend cap.
|
||||
|
||||
**Web search**
|
||||
- `devii_rsearch_enabled` - enable the external `rsearch_*` web tools (web/image search, web-grounded AI answer, chat, image description). When off, those tools are refused. They reach an external public service, not this platform, and Devii uses them only when the user explicitly asks to search the web.
|
||||
- `devii_rsearch_enabled` - enable the external `rsearch_*` web tools (web/image search, web-grounded AI answer, chat, image description); when off, they are refused. They reach an external public service, not this platform, and Devii uses them only when the user explicitly asks to search the web.
|
||||
- `devii_rsearch_url` - base URL of the rsearch-compatible service those tools call.
|
||||
- `devii_rsearch_timeout` - read timeout in seconds for the `rsearch_*` tools (default 300, minimum five minutes). Web-grounded answers can take several minutes, so this is deliberately generous and separate from the fetch timeout.
|
||||
- `devii_rsearch_timeout` - read timeout in seconds for the `rsearch_*` tools (default 300, minimum five minutes). Web-grounded answers can take several minutes, so it is separate from the fetch timeout.
|
||||
|
||||
Each field falls back to its `DEVII_*` environment variable where applicable
|
||||
(`DEVII_RSEARCH_ENABLED`, `DEVII_RSEARCH_URL`, `DEVII_RSEARCH_TIMEOUT`). The service exposes
|
||||
@@ -61,8 +61,7 @@ devii --api-key <your DevPlace api_key> --base-url https://your-host
|
||||
devii -p "List my unread notifications as a bullet list."
|
||||
```
|
||||
|
||||
When the CLI runs against the same database, it resolves the account behind the supplied API key and
|
||||
uses that owner, so it shares the same per-account lesson memory and tasks as the web session. The
|
||||
CLI logs at `WARNING` by default (set `DEVII_LOG_LEVEL=DEBUG` for detail); noisy HTTP libraries are
|
||||
always quieted.
|
||||
Against the same database, the CLI resolves the account behind the supplied API key and uses that
|
||||
owner, sharing the same per-account lesson memory and tasks as the web session. It logs at `WARNING`
|
||||
by default (set `DEVII_LOG_LEVEL=DEBUG` for detail); noisy HTTP libraries are always quieted.
|
||||
</div>
|
||||
|
||||
@@ -18,22 +18,21 @@ Indexes for these are created idempotently in `database.init_db()`.
|
||||
|
||||
## Persistent vs ephemeral
|
||||
|
||||
- **Signed-in users:** conversations, tasks, and lessons persist in the main DB and survive
|
||||
restarts. The session rehydrates them on reconnect.
|
||||
- **Guests:** tasks and lessons use a fresh **in-memory** database per session and are never
|
||||
written to disk; conversations are not persisted at all. When the guest session ends, its state
|
||||
is gone.
|
||||
- **Signed-in users:** conversations, tasks, and lessons persist in the main DB, survive
|
||||
restarts, and rehydrate on reconnect.
|
||||
- **Guests:** tasks and lessons use a fresh **in-memory** database per session, never written to
|
||||
disk; conversations are not persisted at all. State is gone when the guest session ends.
|
||||
|
||||
## Owner isolation
|
||||
|
||||
Every store filters reads and writes by the owner tuple. The lesson store in particular is built
|
||||
per session over the owner's scope, so one account's lessons are never visible to another. This is a
|
||||
hard isolation boundary - see [Security and limits](/docs/devii-security.html).
|
||||
Every store filters reads and writes by the owner tuple. The lesson store is built per session over
|
||||
the owner's scope, so one account's lessons are never visible to another - a hard isolation
|
||||
boundary, see [Security and limits](/docs/devii-security.html).
|
||||
|
||||
## Display vs source of truth
|
||||
|
||||
The in-memory `CostTracker` shows a live per-session figure but resets on reboot; it is **not** used
|
||||
for the cap. The 24h limit and all spend reporting read `devii_usage_ledger`, so the financial
|
||||
ceiling is correct even across restarts and is the same number the admin analytics endpoint reports.
|
||||
The cap and pricing themselves are set in [Configuration and CLI](/docs/devii-config.html).
|
||||
ceiling is correct across restarts and matches the admin analytics endpoint. The cap and pricing
|
||||
themselves are set in [Configuration and CLI](/docs/devii-config.html).
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Devii internals
|
||||
|
||||
Admin-only technical reference for the **Devii** module (`devplacepy/services/devii/`). This is the
|
||||
implementation companion to the functional [Devii Assistant](/docs/devii.html) page that members
|
||||
see. It documents the module's structure, scopes, data model, security, and operations.
|
||||
Admin-only technical reference for the **Devii** module (`devplacepy/services/devii/`), the
|
||||
implementation companion to the functional [Devii Assistant](/docs/devii.html) page members see. It
|
||||
covers the module's structure, scopes, data model, security, and operations.
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests in the
|
||||
> sidebar, the search index, and the documentation export.
|
||||
|
||||
## What Devii is
|
||||
@@ -13,7 +13,7 @@ see. It documents the module's structure, scopes, data model, security, and oper
|
||||
Devii is an in-platform agentic assistant exposed as a WebSocket terminal at `/devii/ws` and as a
|
||||
`devii` console script. It runs a ReAct loop (plan, act, verify, reflect) over a declarative tool
|
||||
catalog. A signed-in user's Devii operates that user's own DevPlace account; guests get a sandbox.
|
||||
It runs as a configurable background service (`DeviiService`) on `/admin/services`.
|
||||
It runs as a configurable background service (`DeviiService`) configured on `/admin/services`.
|
||||
|
||||
## Module map
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ The controls that keep Devii safe and bounded. See also [Data and persistence](/
|
||||
|
||||
The self-learning lesson store is **owner-scoped and never shared**. Each session gets a
|
||||
`LessonStore` filtered by `(owner_kind, owner_id)` - the main DB for signed-in users, a private
|
||||
in-memory DB for guests. One account can never recall another account's lessons. The user can purge
|
||||
their own memory at any time with the `forget_lessons` tool (clear all, or only lessons matching a
|
||||
query). The system prompt forbids storing credentials, passwords, API keys, or secrets in a lesson.
|
||||
in-memory DB for guests. One account can never recall another account's lessons. The user purges
|
||||
their own memory with the `forget_lessons` tool (clear all, or only lessons matching a query). The
|
||||
system prompt forbids storing credentials, passwords, API keys, or secrets in a lesson.
|
||||
|
||||
## Spending limits
|
||||
|
||||
Spend is capped per owner over a rolling 24 hours, read from `devii_usage_ledger` (the authoritative
|
||||
source). The caps are admin-configurable: `devii_user_daily_usd` and `devii_guest_daily_usd`. The
|
||||
cap is checked before each turn; a turn already over the limit is refused. Pricing is configurable
|
||||
(`devii_price_*`) and is applied per turn.
|
||||
source). The admin-configurable caps are `devii_user_daily_usd` and `devii_guest_daily_usd`. The cap
|
||||
is checked before each turn; a turn already over the limit is refused. Pricing is configurable
|
||||
(`devii_price_*`) and applied per turn.
|
||||
|
||||
## JavaScript execution toggle
|
||||
|
||||
@@ -34,11 +34,11 @@ database directly for it.
|
||||
## Web fetch
|
||||
|
||||
`fetch_url` sends realistic browser headers, reduces pages to text, caps size, and refuses private,
|
||||
loopback, and link-local addresses (an SSRF guard) unless explicitly allowed. This limits the blast
|
||||
loopback, and link-local addresses (an SSRF guard) unless explicitly allowed, limiting the blast
|
||||
radius of a prompt-injected page. `http_request` (arbitrary external API calls) and `attach_url`
|
||||
(storing a remote file as an attachment) share the same SSRF guard and size cap, so neither can be
|
||||
turned into a request forger against internal addresses. `attach_url` runs the downloaded bytes
|
||||
through the normal upload validation (allowed types, size limit) before storing them.
|
||||
forged into a request against internal addresses. `attach_url` runs the downloaded bytes through the
|
||||
normal upload validation (allowed types, size limit) before storing them.
|
||||
|
||||
## Confidentiality
|
||||
|
||||
@@ -50,11 +50,11 @@ result, and to omit the model name when reporting cost.
|
||||
|
||||
Every turn is recorded in `devii_turns` (owner, username, timestamps, truncated prompt and reply,
|
||||
iterations, tool-call count, cost, and any error), giving administrators a complete account-scoped
|
||||
history of what Devii did.
|
||||
history.
|
||||
|
||||
## Account scope
|
||||
|
||||
A signed-in user's Devii operates that user's **own** account via the platform API with their API
|
||||
key, so it has exactly the permissions that user has (admins included). It cannot exceed the
|
||||
account's own authority; the platform enforces every action.
|
||||
account's authority; the platform enforces every action.
|
||||
</div>
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
# Tools and scopes
|
||||
|
||||
Devii's capabilities are a declarative **catalog** of actions. Each action declares a `handler` that
|
||||
routes it, and an auth scope. The catalog is the single source of truth: the same definitions become
|
||||
routes it and an auth scope. The catalog is the single source of truth: the same definitions become
|
||||
the model's tool schema, the dispatch routing, and this reference. See also
|
||||
[Devii internals](/docs/devii-internals.html) and [Data and persistence](/docs/devii-data.html).
|
||||
|
||||
Devii is frequently underestimated. It is a full agentic operator: it can run an entire user account,
|
||||
read and write a project's virtual filesystem line by line, drive the user's own browser, call any
|
||||
external HTTP/JSON API, manage Docker containers, learn lessons across sessions, schedule autonomous
|
||||
work, and let users mint their own tools in natural language. As of this build the catalog exposes
|
||||
**154 actions** across sixteen handlers.
|
||||
Devii is a full agentic operator: it can run an entire user account, read and write a project's
|
||||
virtual filesystem line by line, drive the user's own browser, call any external HTTP/JSON API,
|
||||
manage Docker containers, learn lessons across sessions, schedule autonomous work, and let users
|
||||
mint their own tools in natural language. As of this build the catalog exposes **154 actions**
|
||||
across sixteen handlers.
|
||||
|
||||
## How tools are declared
|
||||
|
||||
`actions/spec.py` defines `Action` (name, method, path, params, `requires_auth`, `requires_admin`,
|
||||
`handler`) and `Catalog`. `registry.py` unions the per-category tuples into `CATALOG`.
|
||||
`tool_schema()` projects each action into the JSON tool schema sent to the model;
|
||||
`tool_schemas_for(authenticated, is_admin)` is the **scope filter** - guests only receive actions
|
||||
`tool_schemas_for(authenticated, is_admin)` is the **scope filter**: guests only receive actions
|
||||
where `requires_auth` is false, and `requires_admin` actions are offered only to administrators, so
|
||||
account-operating and admin-only tools are not even presented to a guest or member.
|
||||
account-operating and admin-only tools are never presented to a guest or member.
|
||||
|
||||
**Scope legend** used throughout this page: **public** = offered to everyone including guests;
|
||||
**auth** = signed-in members (and the account-operating actions run as that user); **admin** =
|
||||
@@ -55,7 +55,7 @@ Every content delete (`delete_post`, `delete_comment`, `delete_gist`, `delete_pr
|
||||
the dispatcher: the first call is refused with a message to confirm with the user, and only a repeat
|
||||
call carrying `confirm=true` runs. Each is a **soft delete**, restorable from the admin trash. These
|
||||
tools carry `requires_auth` (not `requires_admin`) because a member may delete their **own** content
|
||||
through them; the platform API enforces the rest, so an **administrator** operating Devii may delete
|
||||
through them. The platform API enforces the rest: an **administrator** operating Devii may delete
|
||||
**any** member's post, comment, gist, project, project file, or attachment, while a member is refused
|
||||
(owner-or-admin is checked on the endpoint, not in the agent).
|
||||
|
||||
@@ -196,13 +196,13 @@ they carry `requires_auth` rather than `requires_admin`; only the read-only aggr
|
||||
| `rsearch_chat` | public | Send a prompt to the external rsearch AI for a direct answer. |
|
||||
| `rsearch_describe_image` | public | Describe a public image URL using the external vision model. |
|
||||
|
||||
`http_request` is the general HTTP client. It sends any method with a `json`, `form`, or raw `body`,
|
||||
sets the JSON Content-Type automatically, and returns the response status, headers, and content
|
||||
(non-2xx is returned, not raised, so API errors are readable). Use it to integrate with external
|
||||
REST/JSON APIs directly. It must be called as a first-class tool: a user-defined tool cannot perform
|
||||
network I/O itself, so wrapping the call in a self-invoking tool would recurse and hit the
|
||||
self-evaluation depth limit. Both `fetch` tools refuse private and loopback addresses (SSRF guard,
|
||||
overridable with `DEVII_FETCH_ALLOW_PRIVATE`) and cap the response size.
|
||||
`http_request` is the general HTTP client for external REST/JSON APIs. It sends any method with a
|
||||
`json`, `form`, or raw `body`, sets the JSON Content-Type automatically, and returns the response
|
||||
status, headers, and content (non-2xx is returned, not raised, so API errors are readable). It must
|
||||
be called as a first-class tool: a user-defined tool cannot perform network I/O itself, so wrapping
|
||||
the call in a self-invoking tool would recurse and hit the self-evaluation depth limit. Both `fetch`
|
||||
tools refuse private and loopback addresses (SSRF guard, overridable with
|
||||
`DEVII_FETCH_ALLOW_PRIVATE`) and cap the response size.
|
||||
|
||||
### Reasoning and memory (`agentic`)
|
||||
|
||||
@@ -261,7 +261,7 @@ self-evaluation cannot recurse without bound.
|
||||
|
||||
A user-defined tool is a stored prompt that re-runs Devii; the actual work is still performed by the
|
||||
built-in tools the prompt tells Devii to call (for example `http_request` for an API or the
|
||||
`project_*` tools for files). It cannot do network or file I/O on its own, and it must not be designed
|
||||
`project_*` tools for files). It cannot do network or file I/O on its own and must not be designed
|
||||
to keep calling itself.
|
||||
|
||||
### On-screen avatar (`avatar`, web only)
|
||||
@@ -313,7 +313,7 @@ to keep calling itself.
|
||||
client is not authenticated. `requires_admin` actions are gated twice: the model never receives
|
||||
their schema unless the session owner is an administrator, and the dispatcher independently rejects
|
||||
any `requires_admin` action for a non-admin owner. The owner's admin status is resolved server-side
|
||||
from the session/API-key user role (`is_admin(user)`), never from client input. Beyond this, **role**
|
||||
from the session/API-key user role (`is_admin(user)`), never from client input. **Role**
|
||||
enforcement for platform data is also done by the platform endpoints themselves (for example the
|
||||
admin endpoints return 403 to non-admins). The local handlers (`avatar`, `client`, `agentic`,
|
||||
`task`, `docs`, `cost`, `chunks`, `customization`, `virtual_tool`) act only on the owner's own
|
||||
@@ -324,10 +324,10 @@ session, browser, or memory; financial cost figures are never disclosed to non-a
|
||||
The system prompt enforces an operating protocol: `plan()` must be the first tool call;
|
||||
authentication and permissions gate account actions; `recall()` consults lessons; mutating actions
|
||||
require a `verify()`; tool errors trigger a `reflect()`. For counts and "how many" questions the
|
||||
model is told to use `site_analytics` (the admin-only [analytics endpoint](/docs/admin.html)) rather
|
||||
than paging lists, and to `search_docs` before guessing a route. To call an external API it uses
|
||||
model uses `site_analytics` (the admin-only [analytics endpoint](/docs/admin.html)) rather
|
||||
than paging lists, and `search_docs` before guessing a route. To call an external API it uses
|
||||
`http_request` directly. The external `rsearch_*` web tools are not platform-specific: the prompt
|
||||
makes Devii prefer platform tools always and call them only when the user explicitly asks to search
|
||||
makes Devii prefer platform tools and call `rsearch_*` only when the user explicitly asks to search
|
||||
the web or an outside source. See [Security and limits](/docs/devii-security.html) for the
|
||||
confidentiality and memory rules.
|
||||
|
||||
@@ -337,7 +337,7 @@ The `client` handler runs tools in the user's own browser through the web termin
|
||||
routed by `session._browser_request` to the most visible tab with a 60 second deadline. Beyond the
|
||||
guidance tools (`get_page_context`, `navigate_to`, `reload_page`, `highlight_element`, `show_toast`,
|
||||
`scroll_to_element`, `clear_highlights`) and the raw escape hatch `run_js`, Devii drives the page with
|
||||
a structured automation set built around discovery and stable element refs:
|
||||
a structured automation set built on discovery and stable element refs:
|
||||
|
||||
- **Discover:** `discover_elements` lists the actionable elements in reading order, each with a stable
|
||||
ref (e3), role, accessible label, robust selector, and live state; `read_element` inspects one.
|
||||
@@ -354,6 +354,6 @@ refs -> wait -> verify, preferring these over `run_js`. Two independent admin ga
|
||||
gated by `devii_allow_eval`, and the mutating control tools (click, fill, set, submit, press_key,
|
||||
run_sequence) by `devii_browser_control`; the read-only tools (context, discover, read, wait) are
|
||||
always available. These tools act only on the user's own screen, and platform routes still enforce
|
||||
their own confirmations (a clicked Delete still opens the confirm dialog), so they add no new
|
||||
data-loss path.
|
||||
their own confirmations (a clicked Delete still opens the confirm dialog), so they add no new path to
|
||||
data loss.
|
||||
</div>
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
<div class="docs-content" data-render>
|
||||
## What Devii does for you
|
||||
|
||||
Devii is a hands-on assistant. You describe what you want and it produces working results -
|
||||
code, automations, and actions on the platform - then verifies them for you.
|
||||
Describe what you want and Devii produces working results - code, automations, and actions on
|
||||
the platform - then verifies them for you.
|
||||
|
||||
- **Generate API clients** in any language, wired to your account.
|
||||
- **Build complete bots** that post, comment, vote, and reply on this community.
|
||||
@@ -30,8 +30,8 @@ code, automations, and actions on the platform - then verifies them for you.
|
||||
|
||||
## Generate a client in any language
|
||||
|
||||
Point Devii at the part of the platform you want and name your language. It returns a complete,
|
||||
runnable client wired to your account with your API key (see [Authentication](/docs/authentication.html)).
|
||||
Name the part of the platform you want and your language. Devii returns a complete, runnable
|
||||
client wired to your account with your API key (see [Authentication](/docs/authentication.html)).
|
||||
|
||||
> Write me a Python client for my feed and notifications, with functions to post and to mark
|
||||
> notifications read.
|
||||
@@ -76,7 +76,7 @@ Anything you can do on the site, you can ask Devii to do - and it confirms the r
|
||||
|
||||
## Pull in files and call external services
|
||||
|
||||
Devii can reach the wider internet for you and bring the results back onto the platform.
|
||||
Devii can reach the internet for you and bring the results back onto the platform.
|
||||
|
||||
> Attach this image to my project: https://example.com/diagram.png
|
||||
|
||||
@@ -103,9 +103,9 @@ Not sure how an endpoint works? Ask in the terminal on any documentation page.
|
||||
|
||||
## Try it now
|
||||
|
||||
Open the terminal and type anything - Devii plans, acts, and verifies, and shows its work as it
|
||||
Open the terminal and type anything - Devii plans, acts, verifies, and shows its work as it
|
||||
goes. Signed-in users get a persistent session that follows them across tabs and devices; guests
|
||||
get an instant sandbox to experiment in.
|
||||
get an instant sandbox.
|
||||
</div>
|
||||
|
||||
<div class="devii-doc-cta">
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Getting started
|
||||
|
||||
A single path from a fresh clone to a running instance and your first change. If you
|
||||
only want to call the API, jump to [Authentication](/docs/authentication.html) and the
|
||||
[API reference](/docs/conventions.html) instead - this page is for working on DevPlace
|
||||
itself.
|
||||
|
||||
## Run it locally
|
||||
|
||||
DevPlace is a Python package. Install it editable, then start the reloading dev server:
|
||||
|
||||
```bash
|
||||
make install # pip install -e .
|
||||
make dev # uvicorn --reload on port 10500
|
||||
```
|
||||
|
||||
Open `http://localhost:10500`. The first account you register becomes the administrator.
|
||||
`make prod` runs the two-worker production server on the same port.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
devplacepy/ the application package
|
||||
main.py mounts routers, middleware, startup/shutdown
|
||||
routers/ one directory per URL domain (mirrors the endpoint tree)
|
||||
templates/ Jinja2 pages; static/ holds CSS and ES6 modules
|
||||
database.py SQLite via dataset; query and batch helpers
|
||||
models.py Pydantic Form models schemas.py *Out JSON models
|
||||
services/ background + async-job services, Devii, containers
|
||||
tests/ unit / api / e2e, mirroring the route or source path
|
||||
agents/ autonomous maintenance fleet (developer tooling, off-limits)
|
||||
```
|
||||
|
||||
## How a feature is shaped
|
||||
|
||||
Every feature is **one data source with four faces**: the same route handler is rendered
|
||||
as HTML, served as JSON, called by the Devii assistant as a tool, and described in the API
|
||||
docs. The usual failure is changing one face and forgetting a connected one. Work in this
|
||||
order so nothing is dropped:
|
||||
|
||||
1. **Data** - query helpers in `database.py`, a `Form` model in `models.py`, an `*Out`
|
||||
model in `schemas.py`.
|
||||
2. **Server** - the handler in `routers/`, with the right guard (`get_current_user` for
|
||||
public reads, `require_user`, `require_admin`); return both faces via
|
||||
`respond(request, template, ctx, model=XOut)`.
|
||||
3. **View** - extend `base.html`; one ES6 class per file under `static/js/`.
|
||||
4. **Agent and docs** - add a Devii tool when a user could ask for the action, an
|
||||
`endpoint()` entry in `docs_api.py`, and update `README.md` and `AGENTS.md`.
|
||||
|
||||
## Validate before you finish
|
||||
|
||||
Never declare work done with a broken import or a validation error:
|
||||
|
||||
```bash
|
||||
python -c "from devplacepy.main import app" # must import clean
|
||||
make validate # Python, JS, CSS, templates
|
||||
```
|
||||
|
||||
Run the maintenance fleet over your changed files to catch style, duplication, and
|
||||
documentation drift before review:
|
||||
|
||||
```bash
|
||||
make maintenance # read-only report on changed files
|
||||
make maintenance-fix # apply fixes to changed files
|
||||
```
|
||||
|
||||
See [Running the agents](/docs/maintenance-usage.html) for the full fleet. Tests live in
|
||||
`tests/` and run with `make test-unit`, `make test-api`, and `make test-e2e`.
|
||||
|
||||
## Read next
|
||||
|
||||
- [Conventions and Errors](/docs/conventions.html) - the rules every endpoint shares.
|
||||
- [Components overview](/docs/components.html) and the [design system](/docs/styles.html) -
|
||||
the frontend building blocks and styling rules.
|
||||
- [Maintenance agents](/docs/maintenance-agents.html) - how the quality fleet keeps the
|
||||
codebase consistent.
|
||||
{% if is_admin(user) %}
|
||||
- [Architecture overview](/docs/architecture.html) - the request pipeline, backend, and
|
||||
frontend in depth.
|
||||
- [Development workflow](/docs/architecture-workflow.html) and
|
||||
[Testing overview](/docs/testing.html) - the deeper contributor reference.
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,60 +1,42 @@
|
||||
<div class="docs-content" data-render>
|
||||
# DevPlace Documentation
|
||||
|
||||
Welcome to the DevPlace developer documentation. DevPlace is a server-rendered
|
||||
social network for developers, with a clean HTTP surface that you can automate.
|
||||
Anything you can do in the browser you can also do from a script.
|
||||
DevPlace is a server-rendered social network for developers with a clean HTTP surface you
|
||||
can automate. Anything you can do in the browser you can also do from a script.
|
||||
|
||||
> Tip: use the **search box** in the sidebar, or the [docs search](/docs/search.html)
|
||||
> page, to find anything across these pages instantly.
|
||||
> Tip: use the **search box** in the sidebar, or the [docs search](/docs/search.html) page,
|
||||
> to find anything across these pages instantly.
|
||||
|
||||
## Getting started
|
||||
## Choose your path
|
||||
|
||||
- [Search the docs](/docs/search.html) - full-text search across every page (also in the sidebar).
|
||||
- [Authentication](/docs/authentication.html) - sign in with a session cookie, an
|
||||
API key, a Bearer token, or HTTP Basic credentials.
|
||||
- [Conventions & Errors](/docs/conventions.html) - base URL, request bodies, the
|
||||
AJAX header rule, pagination, identifiers, and status codes.
|
||||
- [Devii Assistant](/docs/devii.html) - automate any of this by chatting; Devii writes
|
||||
clients and bots and operates your account.
|
||||
**Use the site.** Learn what DevPlace offers and let the assistant do the work:
|
||||
[Devii Assistant](/docs/devii.html), [Media gallery](/docs/media-gallery.html),
|
||||
[Notification settings](/docs/notification-settings.html).
|
||||
|
||||
## API reference
|
||||
**Build on the API.** Authenticate, learn the shared conventions, then browse the
|
||||
reference: [Authentication](/docs/authentication.html),
|
||||
[Conventions and Errors](/docs/conventions.html). Every reference page lists its endpoints
|
||||
with copy-paste cURL, JavaScript, and Python examples and an interactive panel that runs the
|
||||
call with your own API key pre-filled.
|
||||
|
||||
Every reference page lists its endpoints with copy-paste cURL, JavaScript, and Python
|
||||
examples and an interactive panel that runs the call with your own API key pre-filled.
|
||||
Code samples are syntax-highlighted with line numbers and a one-click copy button.
|
||||
**Contribute to DevPlace.** Go from a fresh clone to a running instance and your first
|
||||
change: [Getting started](/docs/getting-started.html).
|
||||
|
||||
- [Search & Lookups](/docs/lookups.html) - user and recipient lookups.
|
||||
- [Votes, Reactions, Bookmarks & Polls](/docs/social-actions.html) - engagement actions.
|
||||
- [Posts, Comments, Projects, Gists & News](/docs/content.html) - the core content types.
|
||||
- [Profiles & Social Graph](/docs/profiles.html) - profiles, follows, leaderboard, avatars.
|
||||
- [Messaging](/docs/messaging.html) - direct messages.
|
||||
- [Notifications](/docs/notifications.html) - the notification feed and unread counts.
|
||||
- [Uploads](/docs/uploads.html) - attachment storage.
|
||||
- [Web Push](/docs/push.html) - browser push notifications.
|
||||
- [Bug Reports](/docs/bugs.html) - the public bug board.
|
||||
{% if is_admin(user) %}
|
||||
- [OpenAI Gateway](/docs/gateway.html) - the OpenAI-compatible proxy.
|
||||
- [Background Services](/docs/services.html) - manage and configure background services.
|
||||
- [Admin API](/docs/admin.html) - user, news, and settings administration.
|
||||
{% endif %}
|
||||
## Machine-readable schema
|
||||
|
||||
An OpenAPI schema and two interactive explorers are generated from the backend:
|
||||
|
||||
- [Swagger UI](/swagger) - try endpoints from an auto-generated console.
|
||||
- [ReDoc](/redoc) - a clean, readable reference rendering.
|
||||
- [OpenAPI JSON](/openapi.json) - the raw schema for codegen and tooling.
|
||||
|
||||
## Download
|
||||
|
||||
Grab the entire documentation as a single file:
|
||||
|
||||
- [Single-page HTML](/docs/download.html) - one self-contained, offline-ready file with styling and syntax highlighting.
|
||||
- [Single-page HTML](/docs/download.html) - one self-contained, offline-ready file.
|
||||
- [Markdown](/docs/download.md) - the complete docs as one Markdown document.
|
||||
|
||||
## Machine-readable schema
|
||||
|
||||
An OpenAPI schema and two interactive explorers are generated automatically from the
|
||||
backend:
|
||||
|
||||
- [Swagger UI](/swagger) - try endpoints from an auto-generated console.
|
||||
- [ReDoc](/redoc) - a clean, readable reference rendering.
|
||||
- [OpenAPI JSON](/openapi.json) - the raw schema for codegen and tooling.
|
||||
|
||||
## Base URL
|
||||
|
||||
Every example in these docs uses your current host:
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Maintenance agents
|
||||
|
||||
DevPlace is **self-maintaining**. Alongside the site you use, a fleet of
|
||||
autonomous AI agents continuously reviews the codebase for security, audit
|
||||
coverage, documentation accuracy, and code quality, then fixes what it finds and
|
||||
proves the build still works.
|
||||
DevPlace is **self-maintaining**. A fleet of autonomous AI agents continuously
|
||||
reviews the codebase for security, audit coverage, documentation accuracy, and
|
||||
code quality, fixes what it finds, and proves the build still works.
|
||||
|
||||
This page explains what the fleet is and what each agent does. The companion page,
|
||||
[Running the agents](/docs/maintenance-usage.html), shows exactly how to run them,
|
||||
with copy-paste commands.
|
||||
This page covers what the fleet is and what each agent does. The companion page,
|
||||
[Running the agents](/docs/maintenance-usage.html), shows how to run them with
|
||||
copy-paste commands.
|
||||
|
||||
> New here? You do not need to know the codebase. Talk to **Maestro**, the
|
||||
> conductor, in plain language ("is my audit coverage complete?") and it runs the
|
||||
> right agent for you and explains the result.
|
||||
> New here? You do not need to know the codebase. Ask **Maestro**, the conductor,
|
||||
> in plain language ("is my audit coverage complete?"); it runs the right agent and
|
||||
> explains the result.
|
||||
|
||||
## How it works in one minute
|
||||
|
||||
- Each agent owns **one quality dimension** and sweeps the whole repository for
|
||||
problems in that dimension only.
|
||||
- Every agent has two modes: **check** (report only, changes nothing) and **fix**
|
||||
(correct the problem, then verify the build).
|
||||
- Every agent has two modes: **check** (report only) and **fix** (correct the
|
||||
problem, then verify the build).
|
||||
- After any change, an agent runs a built-in **validator** (Python, JavaScript,
|
||||
CSS, and HTML/templates) plus an application import, and refuses to finish if the
|
||||
build is broken.
|
||||
- Every run writes a small **report** (a JSON file and a readable summary) so you
|
||||
can see exactly what was found and fixed.
|
||||
- Each agent runs **in isolation**: it only ever uses its own reviewing tools, so
|
||||
one agent can never trigger another or kick off the whole fleet by accident. Runs
|
||||
stay fast and predictable.
|
||||
- Every run writes a small **report** (a JSON file and a readable summary) showing
|
||||
what was found and fixed.
|
||||
- Each agent runs **in isolation**, using only its own reviewing tools, so one agent
|
||||
can never trigger another or kick off the whole fleet by accident.
|
||||
- When you ask Maestro to review **everything**, it runs each agent **once** and
|
||||
remembers the results to answer your follow-up questions, instead of repeating the
|
||||
same work.
|
||||
remembers the results for your follow-up questions instead of repeating the work.
|
||||
|
||||
## Meet the fleet
|
||||
|
||||
@@ -50,21 +47,20 @@ with copy-paste commands.
|
||||
|
||||
## One directory they never touch
|
||||
|
||||
The agents leave their own home alone. The `agents/` directory holds the agents'
|
||||
source, which deliberately contains the very patterns they search for (special
|
||||
characters, example bad names, dangerous command text) as **detection data**, not
|
||||
as mistakes. Scanning it would flag false problems, and fixing those would break
|
||||
the agents. So every agent run hard-blocks any change under `agents/`, and the
|
||||
agents are told never to read or scan it. The directory is off-limits to every
|
||||
checker and cleanup, by design.
|
||||
The `agents/` directory holds the agents' source, which deliberately contains the
|
||||
very patterns they search for (special characters, example bad names, dangerous
|
||||
command text) as **detection data**, not mistakes. Scanning it would flag false
|
||||
problems, and fixing those would break the agents. So every agent run hard-blocks
|
||||
any change under `agents/`, and the agents are told never to read or scan it. The
|
||||
directory is off-limits to every checker and cleanup, by design.
|
||||
|
||||
## Why this exists
|
||||
|
||||
A single feature in DevPlace touches many layers at once: a route is rendered as a
|
||||
page, served as JSON, exposed to the Devii assistant, written into the API docs,
|
||||
audited, and indexed for search. It is easy for a human to change one layer and
|
||||
forget a connected one. The fleet exists to catch exactly that, on every dimension,
|
||||
across the whole project, without anyone having to remember the full map.
|
||||
audited, and indexed for search. It is easy to change one layer and forget a
|
||||
connected one. The fleet catches exactly that, on every dimension, across the whole
|
||||
project, without anyone having to remember the full map.
|
||||
|
||||
## Where to go next
|
||||
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Running the agents
|
||||
|
||||
This page shows how to run the [maintenance agents](/docs/maintenance-agents.html).
|
||||
You do not need to know the codebase to use them. Every command below is safe to
|
||||
copy and paste from the project root.
|
||||
How to run the [maintenance agents](/docs/maintenance-agents.html). You do not need
|
||||
to know the codebase to use them. Every command below runs from the project root.
|
||||
|
||||
> The golden rule: **check** never changes anything, **fix** does. When in doubt,
|
||||
> run check first and read the report.
|
||||
|
||||
## The easiest way: talk to Maestro
|
||||
|
||||
Maestro is the conductor. You ask in plain language and it runs the right agent for
|
||||
you, then explains what it found.
|
||||
Maestro is the conductor. Ask in plain language and it runs the right agent, then
|
||||
explains what it found.
|
||||
|
||||
```bash
|
||||
make maestro
|
||||
```
|
||||
|
||||
That opens a prompt. Try asking:
|
||||
That opens a prompt. Try:
|
||||
|
||||
- `is my audit coverage complete?`
|
||||
- `check the security of the projects router`
|
||||
- `get the whole project in shape`
|
||||
|
||||
You can also ask a single question without opening the prompt:
|
||||
You can also ask a single question without the prompt:
|
||||
|
||||
```bash
|
||||
python -m agents.maestro "is my documentation up to date?"
|
||||
```
|
||||
|
||||
Maestro defaults to **check** (read-only) for questions and confirms with you
|
||||
before it makes any change. When you ask it to review the whole project, it runs
|
||||
each agent **once** and reuses those results to answer anything you ask next, so it
|
||||
never repeats the same sweep to look something up.
|
||||
Maestro defaults to **check** (read-only) for questions and confirms before any
|
||||
change. When you ask it to review the whole project, it runs each agent **once** and
|
||||
reuses those results to answer follow-up questions, never repeating a sweep.
|
||||
|
||||
## Running one agent yourself
|
||||
|
||||
@@ -69,34 +67,34 @@ make agents-all CHECK=1 # report only: run the whole fleet at once
|
||||
```
|
||||
|
||||
Because reporting changes nothing, **`CHECK=1` runs every agent concurrently** for a
|
||||
fast, read-only health check of the whole project. A **fix** run goes one agent at a
|
||||
time in dependency order, so file changes never collide.
|
||||
fast, read-only health check. A **fix** run goes one agent at a time in dependency
|
||||
order, so file changes never collide.
|
||||
|
||||
This is the command to run before a release, or in a continuous-integration job
|
||||
(in `CHECK=1` mode it returns a non-zero exit code if anything is wrong).
|
||||
Run this before a release or in a continuous-integration job: in `CHECK=1` mode it
|
||||
returns a non-zero exit code if anything is wrong.
|
||||
|
||||
## Checking only what you changed
|
||||
|
||||
While you work, you usually only want the fleet to look at the files you just
|
||||
touched, not the whole project. These two targets run the whole fleet but limit it
|
||||
to the files git reports as modified or new (untracked) under `devplacepy/` and
|
||||
`tests/`, so a sweep takes seconds instead of minutes:
|
||||
While you work, you usually want the fleet to look only at the files you just
|
||||
touched. These two targets run the whole fleet but limit it to the files git reports
|
||||
as modified or new (untracked) under `devplacepy/` and `tests/`, so a sweep takes
|
||||
seconds instead of minutes:
|
||||
|
||||
```bash
|
||||
make maintenance # report only, all agents at once, just your changed files
|
||||
make maintenance-fix # fix, just your changed files
|
||||
```
|
||||
|
||||
`make maintenance` is read-only and concurrent, exactly like `make agents-all
|
||||
CHECK=1` but narrowed to your work in progress. `make maintenance-fix` fixes, and a
|
||||
built-in safety rule guarantees it can only edit the files in that changed set,
|
||||
never anything else. If nothing under `devplacepy/` or `tests/` has changed, the run
|
||||
prints "nothing to do" and exits cleanly. The same scope is available on a single
|
||||
agent with the `--changed` flag, for example `python -m agents.security --changed`.
|
||||
`make maintenance` is read-only and concurrent, like `make agents-all CHECK=1` but
|
||||
narrowed to your work in progress. `make maintenance-fix` fixes, and a built-in
|
||||
safety rule guarantees it can edit only the files in that changed set. If nothing
|
||||
under `devplacepy/` or `tests/` has changed, the run prints "nothing to do" and
|
||||
exits cleanly. The same scope is available on a single agent with the `--changed`
|
||||
flag, for example `python -m agents.security --changed`.
|
||||
|
||||
## What you see while it runs
|
||||
|
||||
The output is meant to be read live, so you always know what is happening:
|
||||
The output is meant to be read live:
|
||||
|
||||
- **A start banner** for every agent: its name, a memorable codename (like
|
||||
`brave-otter`), what it is about to do, and where its report will be written.
|
||||
@@ -108,19 +106,19 @@ The output is meant to be read live, so you always know what is happening:
|
||||
## Reading the reports
|
||||
|
||||
Every run writes its findings to `agents/reports/`, named
|
||||
`<agent>-<codename>-<date>` so a run is easy to refer to:
|
||||
`<agent>-<codename>-<date>`:
|
||||
|
||||
- `<agent>-<codename>-<date>.json` - the machine-readable findings.
|
||||
- `<agent>-<codename>-<date>.md` - a readable summary grouped by file.
|
||||
- `fleet-<codename>-<date>.json` - the combined report when you run the whole fleet.
|
||||
|
||||
A report marked `incomplete` means the run ran out of its step budget before
|
||||
finishing; its findings are partial and it is worth running again.
|
||||
finishing; its findings are partial, so run it again.
|
||||
|
||||
## Validating the code yourself
|
||||
|
||||
The agents verify their own changes with a built-in validator that needs no extra
|
||||
tools. You can run it directly any time:
|
||||
tools. You can run it directly:
|
||||
|
||||
```bash
|
||||
make validate # check Python, JavaScript, CSS, and templates
|
||||
|
||||
@@ -4,19 +4,18 @@
|
||||
|
||||
Every image, video, and file you share on DevPlace is collected in one place: the **Media** tab on
|
||||
your profile. Whatever you attach to a post, project, gist, comment, direct message, bug report, or
|
||||
news item shows up here automatically, newest first, so you never have to hunt through old posts to
|
||||
find something you uploaded.
|
||||
news item shows up here automatically, newest first.
|
||||
|
||||
## Where to find it
|
||||
|
||||
Open any profile and choose the **Media** tab, or go straight to
|
||||
`/profile/{{ uname }}?tab=media`. The gallery is public: just like the Posts, Projects, and Gists
|
||||
`/profile/{{ uname }}?tab=media`. The gallery is public: like the Posts, Projects, and Gists
|
||||
tabs, anyone can browse a member's media.
|
||||
|
||||
## Viewing media
|
||||
|
||||
The gallery is a responsive grid of thumbnails that adapts from a wide multi-column layout on the
|
||||
desktop down to a comfortable single column on a phone.
|
||||
The gallery is a responsive grid of thumbnails, from a wide multi-column layout on the
|
||||
desktop down to a single column on a phone.
|
||||
|
||||
- **Images** show as thumbnails. Click one to open it full size in the lightbox; click the
|
||||
backdrop, press `Escape`, or use the close button to dismiss it.
|
||||
@@ -24,8 +23,8 @@ desktop down to a comfortable single column on a phone.
|
||||
- **Other files** (PDFs, archives, code, audio) appear as a labelled card showing the file name and
|
||||
size, and download when clicked.
|
||||
|
||||
Each tile links back to the post, project, or gist the media belongs to, so you can jump from a
|
||||
thumbnail to the place it was shared. Long galleries are split into numbered pages.
|
||||
Each tile links back to the post, project, or gist the media belongs to. Long galleries are split
|
||||
into numbered pages.
|
||||
|
||||
## Deleting your own media
|
||||
|
||||
@@ -33,7 +32,7 @@ Hover (or tap) a tile you uploaded and a delete button appears. Confirm, and the
|
||||
from your Media tab and from anywhere it was shown, including the original post or project. You can
|
||||
only delete media you uploaded yourself.
|
||||
|
||||
You can also ask the Devii assistant to do it for you, for example:
|
||||
You can also ask the Devii assistant to do it, for example:
|
||||
|
||||
> Delete the screenshot I attached to my last post.
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
> tools behind the profile [Media gallery](/docs/media-gallery.html).
|
||||
|
||||
DevPlace never hard-deletes an attachment when a user removes it. Deletion is a **soft delete**: the
|
||||
database row and the file on disk are kept, and the attachment's link to its parent object is left
|
||||
completely intact. The item simply disappears from every place it was shown. This makes every
|
||||
deletion reversible and gives you a moderation trail.
|
||||
database row, the file on disk, and the attachment's link to its parent object are all kept intact,
|
||||
and the item simply disappears from every place it was shown. Every deletion is therefore reversible
|
||||
and leaves a moderation trail.
|
||||
|
||||
## What a delete actually does
|
||||
|
||||
@@ -21,8 +21,7 @@ gets a `deleted_at` timestamp. Nothing else changes:
|
||||
- the item vanishes from the owner's Media tab **and** from the parent object it was attached to.
|
||||
|
||||
Because the relation is never cleared, restoring is a single step: clear `deleted_at` and the
|
||||
attachment reappears in the gallery and on its original parent object automatically, with no
|
||||
re-linking.
|
||||
attachment reappears in the gallery and on its original parent object, with no re-linking.
|
||||
|
||||
## Who can do what
|
||||
|
||||
@@ -32,8 +31,8 @@ re-linking.
|
||||
| Restore media | Admin only | Brings a soft-deleted item back everywhere |
|
||||
| Purge media | Admin only | Permanent hard delete (row + file) |
|
||||
|
||||
Members and guests are never shown the soft-delete, restore, or purge concepts; from their point of
|
||||
view a delete simply removes the item. Moderation is an administrator surface.
|
||||
Members and guests are never shown the soft-delete, restore, or purge concepts; to them a delete
|
||||
simply removes the item. Moderation is an administrator surface.
|
||||
|
||||
## The Media trash
|
||||
|
||||
@@ -49,9 +48,9 @@ all users, newest first, with its uploader and the time it was deleted. From her
|
||||
## Invariant: parent deletion still cleans up files
|
||||
|
||||
Soft delete only affects the per-attachment delete path. When a whole parent object is permanently
|
||||
deleted (for example deleting a project), its attachments are still hard-deleted, including any that
|
||||
were already soft-deleted, so files are never orphaned. The display filters that hide soft-deleted
|
||||
media are deliberately not applied to that cascade.
|
||||
deleted (for example a project), its attachments are still hard-deleted, including any already
|
||||
soft-deleted, so files are never orphaned. The display filters that hide soft-deleted media are
|
||||
deliberately not applied to that cascade.
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Notification settings
|
||||
|
||||
DevPlace notifies you when things happen that involve you: someone comments on your post, replies to
|
||||
your comment, mentions you, upvotes your work, follows you, sends you a direct message, or when you
|
||||
earn a badge, level up, or get an update on a bug you filed. You decide which of these reach you, and
|
||||
how.
|
||||
DevPlace notifies you when something involves you: a comment on your post, a reply to your comment, a
|
||||
mention, an upvote on your work, a new follower, a direct message, a badge, a level-up, or an update
|
||||
on a bug you filed. You decide which reach you, and how.
|
||||
|
||||
Each notification type can be delivered on two independent channels:
|
||||
Each notification type is delivered on two independent channels:
|
||||
|
||||
- **In-app** - the notification appears on DevPlace (the bell in the top navigation and the
|
||||
`/notifications` page).
|
||||
- **Push** - a native web push notification is sent to the devices where you enabled push.
|
||||
|
||||
The two channels are separate: you can keep a type in-app but silence its push, or the other way
|
||||
around.
|
||||
The channels are independent: you can keep a type in-app but silence its push, or the reverse.
|
||||
|
||||
## Where to find it
|
||||
|
||||
@@ -22,8 +20,8 @@ Open your profile and choose the **Notifications** tab, or go straight to
|
||||
`/profile/{{ uname }}?tab=notifications`. The tab is private: only you (and administrators) can see or
|
||||
change your settings.
|
||||
|
||||
You get one row per notification type with two checkboxes, **In-app** and **Push**. Tick or untick a
|
||||
box and it saves immediately - there is no separate save button.
|
||||
Each notification type is one row with two checkboxes, **In-app** and **Push**. Ticking or unticking a
|
||||
box saves immediately - there is no separate save button.
|
||||
|
||||
## What each type covers
|
||||
|
||||
@@ -43,28 +41,27 @@ box and it saves immediately - there is no separate save button.
|
||||
|
||||
Every type and channel is **on** until you turn it off, so notifications work out of the box. A type
|
||||
you have never changed follows the platform default; once you tick or untick a box, your choice is
|
||||
remembered and is no longer affected by later changes to the default.
|
||||
remembered and no longer follows later changes to the default.
|
||||
|
||||
Use **Reset to defaults** at the bottom of the tab to clear all of your choices at once and go back to
|
||||
the platform defaults.
|
||||
Use **Reset to defaults** at the bottom of the tab to clear all of your choices and return to the
|
||||
platform defaults.
|
||||
|
||||
## Turning push on
|
||||
|
||||
Toggling **Push** for a type only has an effect once you have enabled push notifications on the
|
||||
device, which you do with the bell-with-slash button in the top navigation or on the `/notifications`
|
||||
page. Until then, push has nowhere to be delivered. See
|
||||
[Push notifications](/docs/push.html) for the device-side setup.
|
||||
Toggling **Push** for a type only takes effect once you have enabled push notifications on the device
|
||||
with the bell-with-slash button in the top navigation or on the `/notifications` page. Until then,
|
||||
push has nowhere to be delivered. See [Push notifications](/docs/push.html) for the device-side setup.
|
||||
|
||||
## Ask Devii
|
||||
|
||||
You can change these settings in plain language through the Devii assistant, for example:
|
||||
You can change these settings in plain language through the Devii assistant:
|
||||
|
||||
> Turn off push notifications for upvotes.
|
||||
|
||||
> Stop notifying me about new followers entirely.
|
||||
|
||||
Devii reads your current settings, makes the change, and can reset everything back to the defaults
|
||||
(it asks you to confirm a reset first, since that clears all of your choices).
|
||||
Devii reads your current settings, makes the change, and can reset everything to the defaults (it asks
|
||||
you to confirm a reset first, since that clears all of your choices).
|
||||
</div>
|
||||
|
||||
<div class="devii-doc-cta">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Multi-worker and concurrency
|
||||
|
||||
DevPlace runs safely under multiple Uvicorn workers. This page documents the model, what is safe by design, and the rules that keep it safe. See also [Production overview](/docs/production.html), [Deploy and update](/docs/production-deploy.html), and [nginx and networking](/docs/production-nginx.html).
|
||||
DevPlace runs safely under multiple Uvicorn workers. This page covers the model, what is safe by design, and the rules that keep it safe. See also [Production overview](/docs/production.html), [Deploy and update](/docs/production-deploy.html), and [nginx and networking](/docs/production-nginx.html).
|
||||
|
||||
## The model
|
||||
|
||||
@@ -19,9 +19,9 @@ DevPlace runs safely under multiple Uvicorn workers. This page documents the mod
|
||||
The in-process caches (`_user_cache` in `utils.py`, `_settings_cache` in `database.py`) are fast but per-process. To keep them correct across workers, invalidation is coordinated through a `cache_state` table of named integer versions:
|
||||
|
||||
- A mutation that must invalidate a cache calls `bump_cache_version(name)` - an atomic `UPDATE ... version + 1` in the shared table.
|
||||
- Before serving from a cache, the worker calls `sync_local_cache(name, cache)`, which reads the table's version directly and compares it to the version its local cache was built against; if they differ, it clears the local cache and rebuilds from the database. The version read is an uncached indexed primary-key lookup on the tiny `cache_state` table, so invalidation is immediate across workers and never lags behind a write.
|
||||
- Before serving from a cache, the worker calls `sync_local_cache(name, cache)`, which reads the table's version and compares it to the version its local cache was built against; if they differ, it clears and rebuilds the local cache from the database. The version read is an uncached indexed primary-key lookup on the tiny `cache_state` table, so invalidation is immediate across workers.
|
||||
|
||||
The result: a logout, ban, role change, or settings save in one worker is honored by every other worker on its next request, while the common path stays a cheap in-memory hit on the data itself. `auth` covers `_user_cache` (logout, deactivation, role and password changes); `settings` covers `_settings_cache` (every `set_setting` and the admin settings save).
|
||||
A logout, ban, role change, or settings save in one worker is therefore honored by every other worker on its next request, while the common path stays a cheap in-memory hit. `auth` covers `_user_cache` (logout, deactivation, role and password changes); `settings` covers `_settings_cache` (every `set_setting` and the admin settings save).
|
||||
|
||||
## Rate limiting
|
||||
|
||||
@@ -29,10 +29,10 @@ The rate limiter counts per IP in a per-process structure, so N workers would ot
|
||||
|
||||
## First-boot serialization
|
||||
|
||||
Two startup actions are not safe to run concurrently from a cold state, so both are serialized with a file lock:
|
||||
Two startup actions are unsafe to run concurrently from a cold state, so both are serialized with a file lock:
|
||||
|
||||
- **Database initialization.** Every worker runs `init_db()` at startup; the seed-if-missing inserts have a check-then-insert race that could duplicate rows on a fresh database. Startup wraps `init_db()` in a blocking `flock` on `devplace-init.lock`, so one worker initializes while the others wait, then run the idempotent path.
|
||||
- **VAPID key generation.** If the push keys are absent, concurrent workers could each generate a different keypair and overwrite each other, breaking push. `ensure_certificates()` early-returns when the keys exist and otherwise generates them under a `.vapid.lock` `flock`, so exactly one worker creates the keypair.
|
||||
- **VAPID key generation.** If the push keys are absent, concurrent workers could each generate a different keypair and overwrite each other, breaking push. `ensure_certificates()` early-returns when the keys exist, and otherwise generates them under a `.vapid.lock` `flock`, so exactly one worker creates the keypair.
|
||||
|
||||
## Preferred rules
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ First deploy, updates, environment, and rollback for the production stack. See a
|
||||
|
||||
## Environment
|
||||
|
||||
Configuration is a single `.env` file in the project root (git-ignored), loaded both by Compose (`env_file`) and by the app at startup (`python-dotenv`). Copy the committed template and edit it:
|
||||
Configuration is a single `.env` file in the project root (git-ignored), loaded both by Compose (`env_file`) and by the app at startup (`python-dotenv`). Copy the committed template and edit it.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
@@ -39,7 +39,7 @@ make docker-up # start (creates ./var, mounts the socket)
|
||||
|
||||
Open `http://<host>:<PORT>`. Useful targets: `make docker-logs` (tail), `make docker-down` (stop), `make docker-clean` (down).
|
||||
|
||||
The make targets always apply the `docker-compose.containers.yml` overlay, so the admin-only Container Manager works with no extra setup. Always update through the make targets - a plain `docker compose up -d` drops the overlay and silently removes the docker socket and CLI the manager needs.
|
||||
The make targets always apply the `docker-compose.containers.yml` overlay, so the admin-only Container Manager works with no extra setup. Always update through them: a plain `docker compose up -d` drops the overlay and silently removes the docker socket and CLI the manager needs.
|
||||
|
||||
## Updating
|
||||
|
||||
@@ -79,5 +79,5 @@ Back up `devplace.db` (and its `-wal`/`-shm`) before a risky change; the schema
|
||||
|
||||
## Bare-metal alternative
|
||||
|
||||
`make prod` runs the same app without containers (`uvicorn ... --workers 2 --proxy-headers`) from the project root, sharing the identical database and files. It binds port 10500 directly, so it conflicts with the Docker front door on the same port - run one or the other, or set a different `PORT`. Keep `DEVPLACE_WEB_WORKERS` in lockstep with `--workers`; see [Multi-worker and concurrency](/docs/production-concurrency.html) for why.
|
||||
`make prod` runs the same app without containers (`uvicorn ... --workers 2 --proxy-headers`) from the project root, sharing the same database and files. It binds port 10500 directly, conflicting with the Docker front door on that port - run one or the other, or set a different `PORT`. Keep `DEVPLACE_WEB_WORKERS` in lockstep with `--workers`; see [Multi-worker and concurrency](/docs/production-concurrency.html) for why.
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ The nginx image (`nginx/Dockerfile`) renders `nginx/nginx.conf.template` at star
|
||||
|
||||
## Uploads are forced downloads
|
||||
|
||||
The application mounts `/static/uploads` through `UploadStaticFiles`, which sets `Content-Disposition: attachment` so a user-supplied file downloads instead of rendering inline - a stored-XSS defense against uploaded SVG or HTML on the same origin. In production nginx serves those files directly from disk and would bypass that header, so the `/static/uploads/` location **re-applies** `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`. Keep this rule whenever the static handling changes.
|
||||
The application mounts `/static/uploads` through `UploadStaticFiles`, which sets `Content-Disposition: attachment` so a user-supplied file downloads instead of rendering inline - a stored-XSS defense against uploaded SVG or HTML on the same origin. In production nginx serves those files directly from disk, bypassing that header, so the `/static/uploads/` location **re-applies** `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`. Keep this rule whenever the static handling changes.
|
||||
|
||||
## WebSockets
|
||||
|
||||
@@ -32,7 +32,7 @@ map $http_upgrade $connection_upgrade {
|
||||
}
|
||||
```
|
||||
|
||||
and the `/devii/ws` location forwards `Upgrade $http_upgrade` and `Connection $connection_upgrade` with a long read timeout. Note the app serves `/devii/ws` only from the worker holding the service lock; other workers close with code 1013 and the auto-reconnecting client lands on the owner, so a connection may take a brief reconnect to settle. See [Multi-worker and concurrency](/docs/production-concurrency.html) for the lock-owner model.
|
||||
and the `/devii/ws` location forwards `Upgrade $http_upgrade` and `Connection $connection_upgrade` with a long read timeout. The app serves `/devii/ws` only from the worker holding the service lock; other workers close with code 1013 and the auto-reconnecting client lands on the owner, so a connection may take a brief reconnect to settle. See [Multi-worker and concurrency](/docs/production-concurrency.html) for the lock-owner model.
|
||||
|
||||
## Upload size ceiling
|
||||
|
||||
@@ -40,7 +40,7 @@ and the `/devii/ws` location forwards `Upgrade $http_upgrade` and `Connection $c
|
||||
|
||||
## Security headers and caching
|
||||
|
||||
The server block sets `X-Content-Type-Options`, `X-Frame-Options: DENY`, `X-XSS-Protection`, and `Referrer-Policy` (these are inherited only by locations that declare no `add_header` of their own). gzip is enabled for text, JSON, JS, CSS, and SVG. The micro-cache is off by default; set `NGINX_CACHE_ENABLED=true` to cache proxied 200s for one minute with `X-Cache-Status` reporting.
|
||||
The server block sets `X-Content-Type-Options`, `X-Frame-Options: DENY`, `X-XSS-Protection`, and `Referrer-Policy`, inherited only by locations that declare no `add_header` of their own. gzip is enabled for text, JSON, JS, CSS, and SVG. The micro-cache is off by default; set `NGINX_CACHE_ENABLED=true` to cache proxied 200s for one minute with `X-Cache-Status` reporting.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Production overview
|
||||
|
||||
How DevPlace runs in production: a single host, two containers, and one set of files shared with the development server. See also [Deploy and update](/docs/production-deploy.html), [nginx and networking](/docs/production-nginx.html), and [Multi-worker and concurrency](/docs/production-concurrency.html).
|
||||
DevPlace runs on a single host: two containers and one set of files shared with the development server. See also [Deploy and update](/docs/production-deploy.html), [nginx and networking](/docs/production-nginx.html), and [Multi-worker and concurrency](/docs/production-concurrency.html).
|
||||
|
||||
## Topology
|
||||
|
||||
@@ -14,9 +14,9 @@ There is no separate database service: the data lives in SQLite files on the hos
|
||||
|
||||
## Shared database and files
|
||||
|
||||
This is the defining property of the setup: **production uses the same database and shares as the development server.**
|
||||
The defining property of the setup: **production uses the same database and shares as the development server.**
|
||||
|
||||
The app container bind-mounts the host project directory (`./` to `/app`) and runs as the host user (`DEVPLACE_UID:DEVPLACE_GID`, default `1000:1000`). `config.py` resolves the database to an absolute path under the project root, so with no `DEVPLACE_DATABASE_URL` override the container reads and writes the **same `devplace.db`** as `make dev`. The same bind mount also shares:
|
||||
The app container bind-mounts the host project directory (`./` to `/app`) and runs as the host user (`DEVPLACE_UID:DEVPLACE_GID`, default `1000:1000`). `config.py` resolves the database to an absolute path under the project root, so with no `DEVPLACE_DATABASE_URL` override the container reads and writes the **same `devplace.db`** as `make dev`. The bind mount also shares:
|
||||
|
||||
- `devplace.db` (plus its `-wal` / `-shm` companions)
|
||||
- `static/uploads/` (user attachments and images)
|
||||
@@ -29,7 +29,7 @@ Because the container runs as the host user, every file it writes stays owned by
|
||||
## Why this is safe
|
||||
|
||||
- **Concurrency.** SQLite runs in WAL mode with a 30s busy timeout, which allows concurrent readers and a serialized writer. The dev process and the container workers can use the file at the same time.
|
||||
- **One services owner.** The background services (news, bots, Devii hub) are guarded by an exclusive `fcntl.flock` on `devplace-services.lock`. Across every process that shares that file - dev and prod alike - exactly one acquires the lock and runs the services; the others skip them. News is never fetched twice and bots never double-run.
|
||||
- **One services owner.** The background services (news, bots, Devii hub) are guarded by an exclusive `fcntl.flock` on `devplace-services.lock`. Across every process that shares that file - dev and prod alike - exactly one acquires the lock and runs the services; the others skip them, so news is never fetched twice and bots never double-run.
|
||||
- **One DB, one truth.** No `DEVPLACE_DATABASE_URL` override means nothing can silently diverge to a second database.
|
||||
|
||||
## Same-host requirement
|
||||
@@ -38,5 +38,5 @@ SQLite is a local-file database; it cannot be shared across machines. For produc
|
||||
|
||||
## Code updates without rebuild
|
||||
|
||||
Because the host project directory is bind-mounted over `/app`, the container runs the host's current source. A code update is `git pull` followed by a restart - no image rebuild. The image is only rebuilt when Python dependencies in `pyproject.toml` change (those are installed into the image at build time). See [Deploy and update](/docs/production-deploy.html).
|
||||
Because the host project directory is bind-mounted over `/app`, the container runs the host's current source. A code update is `git pull` followed by a restart - no image rebuild. The image is rebuilt only when Python dependencies in `pyproject.toml` change, since those are installed into the image at build time. See [Deploy and update](/docs/production-deploy.html).
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# BotsService
|
||||
|
||||
`name` `bots` - disabled by default - 15s interval (minimum 5s). It runs a fleet of Playwright-driven AI personas that browse a DevPlace instance and post, comment, vote, and react like real users, for populating and exercising an instance. Source: `services/bot/`. See also [Reading service data](/docs/services-data.html).
|
||||
`name` `bots` - disabled by default - 15s interval (minimum 5s). It runs a fleet of Playwright-driven AI personas that browse a DevPlace instance and post, comment, vote, and react like real users, to populate and exercise the instance. Source: `services/bot/`. See also [Reading service data](/docs/services-data.html).
|
||||
|
||||
> Audience: administrators and maintainers. The bots require the `bots` extra and a Playwright Chromium install; without them the fleet stays idle.
|
||||
|
||||
@@ -21,7 +21,7 @@ Each bot runs an open-ended loop of sessions with varying "moods" (lurking, casu
|
||||
|
||||
## What a bot does
|
||||
|
||||
A bot registers or logs in as its own DevPlace account through the browser, then performs a mix of actions driven by an LLM persona: create posts (seeded from news articles ranked by persona interest, with a persona-weighted category, a rewritten human title, quality-checked and de-duplicated), comment and reply (with mentions), create gists and projects (gists pass a non-triviality quality gate), file bugs, vote and star posts/comments/gists/projects/polls, react with emoji, follow users, update its profile, send and answer messages, take part in threaded discussion with other bots, and browse, search, and read pages with human-like timing. All interaction is real HTTP through Playwright Chromium against the target site; the bot authenticates exactly as a person would.
|
||||
A bot registers or logs in as its own DevPlace account through the browser, then performs a mix of actions driven by an LLM persona: create posts (seeded from news articles ranked by persona interest, with a persona-weighted category, a rewritten human title, quality-checked and de-duplicated), comment and reply (with mentions), create gists and projects (gists pass a non-triviality quality gate), file bugs, vote and star posts/comments/gists/projects/polls, react with emoji, follow users, update its profile, send and answer messages, take part in threaded discussion with other bots, and browse, search, and read pages with human-like timing. All interaction is real HTTP through Playwright Chromium against the target site, authenticating exactly as a person would.
|
||||
|
||||
## Configuration fields
|
||||
|
||||
@@ -39,7 +39,7 @@ Plus the inherited Enabled, Run interval, and Log buffer size fields. See [Confi
|
||||
|
||||
## State and dependencies
|
||||
|
||||
Bots do not use database tables; each bot persists its state to a JSON file per slot (`state_slot{N}.json`) under the bots data directory, and a shared article registry file prevents two bots from posting the same article. Dependencies are Playwright Chromium (the `bots` extra) and the LLM gateway. The bots browse the target site rather than touching the database directly.
|
||||
Bots do not use database tables and never touch the database directly; each bot persists its state to a per-slot JSON file (`state_slot{N}.json`) under the bots data directory, and a shared article registry file prevents two bots from posting the same article. Dependencies are Playwright Chromium (the `bots` extra) and the LLM gateway.
|
||||
|
||||
## Data it offers
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Container Manager
|
||||
|
||||
The Container Manager runs supervised container instances with full lifecycle control, from the web UI,
|
||||
The Container Manager runs supervised container instances with full lifecycle control from the web UI,
|
||||
the HTTP API, and Devii. Every instance runs ONE shared, prebuilt image; there is no per-project image
|
||||
building inside the app. A reconciling `BaseService` supervises the instances.
|
||||
|
||||
@@ -20,7 +20,7 @@ building inside the app. A reconciling `BaseService` supervises the instances.
|
||||
- **ContainerService** (`service.py`): a reconciler. Each tick it snapshots `docker ps` (filtered by
|
||||
the `devplace.instance` label), converges every instance to its `desired_state`, applies restart
|
||||
policies, reaps orphan containers whose DB row is gone, fires due schedules, and samples metrics. The
|
||||
`devplace.instance=<uid>` label is the join key that guarantees no orphans and no lost state. Only the
|
||||
`devplace.instance=<uid>` label is the join key, guaranteeing no orphans and no lost state. Only the
|
||||
service lock owner reconciles; HTTP handlers only edit `desired_state`.
|
||||
|
||||
## The shared `ppy` image
|
||||
@@ -30,7 +30,7 @@ with `make ppy` from `ppy.Dockerfile`. The image is a Python + Playwright base w
|
||||
common Python libraries preinstalled, and it bakes in the security model: the `pravda` user at uid/gid
|
||||
`1000:1000`, a `sudo` superclone that never escalates (it runs commands as `pravda`, so a container can
|
||||
never write a root-owned file into the bind-mounted workspace), and the stdlib `pagent` agent at
|
||||
`/usr/bin/pagent.py`. Creating an instance is then an instant `docker run` (no build wait); it fails
|
||||
`/usr/bin/pagent.py`. Creating an instance is then an instant `docker run` with no build wait; it fails
|
||||
fast with a clear error if the `ppy` image has not been built yet.
|
||||
|
||||
Projects that need a library not in the image install it at runtime with `pip install` (pravda owns the
|
||||
@@ -65,7 +65,7 @@ A running instance can be exposed by setting an `ingress_slug` plus an `ingress_
|
||||
mapped container ports). The `/p/<slug>` route (`routers/proxy.py`) then reverse-proxies both HTTP and
|
||||
WebSocket traffic to that container's published host port, stripping the `/p/<slug>` prefix. It is
|
||||
public but SSRF-safe: the target host and port are derived from the instance row, never from the
|
||||
request. So a container WebSocket server on port 8899 becomes reachable at `wss://<your-host>/p/<slug>/ws`.
|
||||
request. A container WebSocket server on port 8899 becomes reachable at `wss://<your-host>/p/<slug>/ws`.
|
||||
|
||||
Set the **Public URL** in admin settings (`/admin/settings`) to your production origin; the container
|
||||
tools then return an absolute `ingress_url` (for example `https://your-host/p/<slug>`) so Devii and API
|
||||
@@ -79,20 +79,20 @@ automatically and self-derive its inputs, so it works with no `sudo`, no `/srv`
|
||||
`.env` editing; a plain `docker compose up -d` drops the overlay and re-breaks it. The overlay mounts
|
||||
the docker socket (root on the host - trusted admins only), installs the docker CLI via the
|
||||
`INSTALL_DOCKER_CLI` build arg, and adds the host docker group (gid read from the socket via
|
||||
`stat -c '%g' /var/run/docker.sock`). Two docker-in-docker details matter: the data dir must be
|
||||
`stat -c '%g' /var/run/docker.sock`). Two docker-in-docker details matter. First, the data dir must be
|
||||
bind-mounted at the **same absolute path** on host and in the app container (make sets
|
||||
`DEVPLACE_DATA_DIR` to the project's own `./var`) because `docker run -v` resolves the source against
|
||||
the host; and the ingress proxy reaches a published port through the container's own docker bridge
|
||||
gateway plus the published host port. The reconciler records each instance's `container_ip` and
|
||||
the host. Second, the ingress proxy reaches a published port through the container's own docker bridge
|
||||
gateway plus the published host port: the reconciler records each instance's `container_ip` and
|
||||
`container_gateway` from `docker inspect`, and the `/p/<slug>` proxy dials `gateway:host_port` by
|
||||
default. This avoids the loopback path, which fails on hosts where docker's `nat OUTPUT` rule excludes
|
||||
`127.0.0.0/8` from DNAT and no `docker-proxy` binds loopback for a `0.0.0.0`-published port. Dialing
|
||||
the container's own bridge IP is also avoided because docker's bridge-isolation rule
|
||||
(`DOCKER ! -i docker0 -o docker0 -j DROP`) can drop direct bridge-IP traffic from the host. Set
|
||||
`DEVPLACE_CONTAINER_PROXY_HOST` only to override this (for example a containerized app via the docker
|
||||
socket uses `host.docker.internal`); when set, the proxy dials that host with the published port
|
||||
`127.0.0.0/8` from DNAT and no `docker-proxy` binds loopback for a `0.0.0.0`-published port. It also
|
||||
avoids the container's own bridge IP, which docker's bridge-isolation rule
|
||||
(`DOCKER ! -i docker0 -o docker0 -j DROP`) can drop from the host. Set
|
||||
`DEVPLACE_CONTAINER_PROXY_HOST` to override this (a containerized app via the docker
|
||||
socket uses `host.docker.internal`); the proxy then dials that host with the published port
|
||||
instead of the gateway. Before the reconciler has recorded a gateway, the proxy falls back to
|
||||
`127.0.0.1`. Remember to run `make ppy` on the production host too. Full steps are in the README
|
||||
`127.0.0.1`. Run `make ppy` on the production host too. Full steps are in the README
|
||||
"Container Manager wiring".
|
||||
|
||||
## Observability, CLI, and Devii
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Reading service data
|
||||
|
||||
This page is the answer to "what are the AI statistics of the services that are running?" Live status and metrics for every background service are available over HTTP, admin-only, and refreshed every few seconds from the `service_state` table. See also the [Services overview](/docs/services.html).
|
||||
Live status and metrics for every background service are available over HTTP, admin-only, and refreshed every few seconds from the `service_state` table. See also the [Services overview](/docs/services.html).
|
||||
|
||||
> Audience: administrators and maintainers. All endpoints here require an administrator; non-admins get redirected or a 403.
|
||||
|
||||
@@ -26,7 +26,7 @@ Returns `{ "services": [ ... ] }` where each entry is the service's full `descri
|
||||
| `metrics` | The service-specific data (see below). This is where the live AI statistics live. |
|
||||
| `fields`, `field_groups` | The configuration specs. |
|
||||
|
||||
This single call is the right source when asked for the running statistics of all services. The per-service `metrics` block is the running AI statistics, already computed.
|
||||
This single call is the right source for the running statistics of all services. Each per-service `metrics` block holds the live AI statistics, already computed.
|
||||
|
||||
## One service
|
||||
|
||||
@@ -34,7 +34,7 @@ This single call is the right source when asked for the running statistics of al
|
||||
GET /admin/services/{name}/data
|
||||
```
|
||||
|
||||
Returns `{ "service": { ... } }` with the same `describe()` shape for a single service, for example `GET /admin/services/openai/data` for the AI gateway or `GET /admin/services/devii/data` for the assistant.
|
||||
Returns `{ "service": { ... } }` with the same `describe()` shape for one service, for example `GET /admin/services/openai/data` for the AI gateway or `GET /admin/services/devii/data` for the assistant.
|
||||
|
||||
## What is in each metrics block
|
||||
|
||||
@@ -68,7 +68,7 @@ GET /admin/analytics
|
||||
|
||||
## How Devii should answer service-stats questions
|
||||
|
||||
When asked about running services or their AI statistics, the answer is `GET /admin/services/data`, not the in-session cost tracker. The `cost_stats` tool reports only the current Devii conversation's token usage; it is not the platform's service statistics. For gateway depth use `GET /admin/ai-usage/data`; for platform counts use `GET /admin/analytics`. Devii can reach all three through its `http` action because the platform enforces admin access, not the agent.
|
||||
For questions about running services or their AI statistics, the answer is `GET /admin/services/data`, not the in-session cost tracker. The `cost_stats` tool reports only the current Devii conversation's token usage, not the platform's service statistics. For gateway depth use `GET /admin/ai-usage/data`; for platform counts use `GET /admin/analytics`. Devii reaches all three through its `http` action because the platform enforces admin access, not the agent.
|
||||
|
||||
## Controlling a service
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<div class="docs-content" data-render>
|
||||
# DeviiService
|
||||
|
||||
`name` `devii` - disabled by default - 60s interval (minimum 10s). Devii is the in-platform agentic assistant, exposed as a WebSocket terminal at `/devii/ws`. Source: `services/devii/`. The technical internals of the agent (tools, sessions, lessons) have their own section under [Devii internals](/docs/devii-internals.html); this page covers Devii as a background service. See also [Reading service data](/docs/services-data.html).
|
||||
`name` `devii` - disabled by default - 60s interval (minimum 10s). Devii is the in-platform agentic assistant, exposed as a WebSocket terminal at `/devii/ws`. Source: `services/devii/`. The agent internals (tools, sessions, lessons) have their own [Devii internals](/docs/devii-internals.html) section; this page covers Devii as a background service. See also [Reading service data](/docs/services-data.html).
|
||||
|
||||
> Audience: administrators and maintainers. This page describes limits, spend, and persistence.
|
||||
|
||||
## The service loop versus the real work
|
||||
|
||||
Devii is event-driven: the actual work happens per WebSocket message, not in `run_once()`. The 60s service loop does housekeeping only: it prunes usage-ledger rows older than 48 hours and garbage-collects idle sessions (sessions with zero open connections), logging the counts. The agent turns themselves are driven by the terminal at `/devii/ws`.
|
||||
Devii is event-driven: the work happens per WebSocket message, not in `run_once()`. The 60s service loop does housekeeping only: it prunes usage-ledger rows older than 48 hours and garbage-collects idle sessions (zero open connections), logging the counts. The terminal at `/devii/ws` drives the agent turns.
|
||||
|
||||
## Sessions, owners, and the worker lock
|
||||
|
||||
`DeviiHub` keeps one `DeviiSession` per owner, keyed by `(owner_kind, owner_id)`: `user`/uid for a signed-in user, `guest`/cookie for an anonymous visitor. All of an owner's open tabs share that one session, and a per-session `asyncio.Lock` serializes turns so an owner never runs two turns at once. A signed-in user's Devii operates their own DevPlace account using their own API key; guests get a sandbox.
|
||||
|
||||
Because the hub is in-memory, `/devii/ws` is served only by the worker that owns the services lock. Non-owner workers close new Devii sockets with code 1013, so the client reconnects until it reaches the owner. This keeps every session, and therefore every cost and quota check, in one place. The database (conversations, ledger, tasks, lessons) is the durable source of truth, so a new lock owner rebuilds sessions on demand after a restart.
|
||||
Because the hub is in-memory, `/devii/ws` is served only by the worker that owns the services lock. Non-owner workers close new Devii sockets with code 1013, so the client reconnects until it reaches the owner, keeping every session and every cost and quota check in one place. The database (conversations, ledger, tasks, lessons) is the durable source of truth, so a new lock owner rebuilds sessions on demand after a restart.
|
||||
|
||||
## Spend caps
|
||||
|
||||
@@ -49,5 +49,5 @@ The in-memory `CostTracker` is display-only; the ledger is authoritative for spe
|
||||
|
||||
## Data it offers
|
||||
|
||||
Live metrics (in `describe()`, at `GET /admin/services/data`) are a labelled stats list: active sessions, open connections, 24h spend across all owners, the user/guest/admin caps, whether guests are allowed, and the model name. Per-turn token and cost detail lives in `devii_usage_ledger`; the per-turn audit trail lives in `devii_turns`.
|
||||
Live metrics (in `describe()`, at `GET /admin/services/data`) are a labelled stats list: active sessions, open connections, 24h spend across all owners, the user/guest/admin caps, whether guests are allowed, and the model name. Per-turn token and cost detail lives in `devii_usage_ledger`; the per-turn audit trail in `devii_turns`.
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Service framework
|
||||
|
||||
The framework is two files: `services/base.py` (the `ConfigField` and `BaseService` classes) and `services/manager.py` (the `ServiceManager` singleton). Together they give every service identical configuration, supervision, persistence, and monitoring. See also the [Services overview](/docs/services.html) and [Reading service data](/docs/services-data.html).
|
||||
Two files make up the framework: `services/base.py` (the `ConfigField` and `BaseService` classes) and `services/manager.py` (the `ServiceManager` singleton). Together they give every service identical configuration, supervision, persistence, and monitoring. See also the [Services overview](/docs/services.html) and [Reading service data](/docs/services-data.html).
|
||||
|
||||
> Audience: administrators and maintainers.
|
||||
|
||||
@@ -9,7 +9,7 @@ The framework is two files: `services/base.py` (the `ConfigField` and `BaseServi
|
||||
|
||||
A `ConfigField` is one typed, validated setting backed by a `site_settings` row. Fields declare a `key`, `label`, `type`, `default`, `help`, optional `group`, optional `minimum`/`maximum`, optional `options`, and a `secret` flag.
|
||||
|
||||
Supported types and their coercion: `int` and `float` (range-checked against `minimum`/`maximum`), `bool` (stored as `"0"`/`"1"`, rendered as a select), `select` (value must be one of `options`), `url` (must start with `http://` or `https://` if non-empty), and `str`. Coercion raises a readable `ValueError` on bad input, which the save handler turns into a per-field error message.
|
||||
Supported types and their coercion: `int` and `float` (range-checked against `minimum`/`maximum`), `bool` (stored as `"0"`/`"1"`, rendered as a select), `select` (value must be one of `options`), `url` (must start with `http://` or `https://` if non-empty), and `str`. Bad input raises a readable `ValueError`, which the save handler turns into a per-field error message.
|
||||
|
||||
Key methods:
|
||||
|
||||
@@ -52,6 +52,7 @@ The supervisor is an asyncio task started by `start_supervisor()` and stopped co
|
||||
|
||||
`_execute_run()` re-reads the interval live, stamps `last_run`, runs `run_once()` inside a try/except that logs any error to the buffer, then schedules `next_due = now + interval`. Because the interval is read every run, retuning it on `/admin/services` takes effect immediately.
|
||||
|
||||
|
||||
Timing constants: `TICK_SECONDS = 1`, `PERSIST_SECONDS = 3` (minimum gap between non-forced state writes), `STALE_SECONDS = 15` (a heartbeat older than this reads as `stalled`).
|
||||
|
||||
## State persistence and status
|
||||
@@ -81,7 +82,7 @@ The singleton `service_manager` (imported from `services/manager.py`) holds the
|
||||
|
||||
Production runs multiple uvicorn workers. At startup each worker tries to acquire an exclusive non-blocking lock on `devplace-services.lock`. Exactly one wins, calls `service_manager.supervise()`, and sets `set_lock_owner(True)`; the rest decline and run no services. `owns_lock()` reports the result.
|
||||
|
||||
This guarantees each service runs once across the whole deployment: news is fetched once, the bot fleet is sized once, the gateway pool and circuit breaker are per-owner. It is also why `/devii/ws` is served only by the lock owner; non-owner workers close new Devii sockets with code 1013 so the client reconnects until it reaches the owner. Setting `DEVPLACE_DISABLE_SERVICES=1` skips all supervision (the test suite does this).
|
||||
This guarantees each service runs once across the whole deployment: news is fetched once, the bot fleet is sized once, the gateway pool and circuit breaker are per-owner. It is also why `/devii/ws` is served only by the lock owner; non-owner workers close new Devii sockets with code 1013 so the client reconnects until it reaches the owner. `DEVPLACE_DISABLE_SERVICES=1` skips all supervision (the test suite does this).
|
||||
|
||||
## Adding a service
|
||||
|
||||
@@ -89,5 +90,5 @@ This guarantees each service runs once across the whole deployment: news is fetc
|
||||
2. Override `run_once()`; optionally `collect_metrics()`, `on_enable()`, `on_disable()`.
|
||||
3. Register it in `main.py` startup with `service_manager.register(YourService())`.
|
||||
|
||||
It then appears on `/admin/services`, is configurable, supervised, persisted, and monitored, with no further work.
|
||||
It then appears on `/admin/services`, configurable, supervised, persisted, and monitored, with no further work.
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="docs-content" data-render>
|
||||
# GatewayService (AI)
|
||||
|
||||
`name` `openai` - enabled by default - 30s interval (minimum 5s). The gateway is an OpenAI-compatible endpoint at `/openai/v1/*` and the **single source of truth for all AI** on the platform. Every other AI consumer (news grading, the bots, Devii) routes through it, so all spend is attributed, priced, and limitable in one place. Source: `services/openai_gateway/`. See also [Reading service data](/docs/services-data.html).
|
||||
`name` `openai` - enabled by default - 30s interval (minimum 5s). The gateway is an OpenAI-compatible endpoint at `/openai/v1/*` and the **single source of truth for all AI** on the platform. Every other AI consumer (news grading, the bots, Devii) routes through it, so all spend is attributed, priced, and limitable in one place. Source: `services/openai_gateway/`. See [Reading service data](/docs/services-data.html).
|
||||
|
||||
> Audience: administrators and maintainers. This page describes credentials, pricing, and cost.
|
||||
|
||||
## What it does
|
||||
|
||||
It forwards chat-completions and other `/v1/*` calls to a configured upstream (DeepSeek by default), optionally describing any image content with a vision model first so vision-less upstreams still work, records every call to a usage ledger, computes cost, retries transient failures, and trips a circuit breaker on sustained upstream errors. The service loop itself (every 30s) samples concurrency and prunes old usage rows; the real work happens per request.
|
||||
It forwards chat-completions and other `/v1/*` calls to a configured upstream (DeepSeek by default), optionally describing image content with a vision model first so vision-less upstreams still work, records every call to a usage ledger, computes cost, retries transient failures, and trips a circuit breaker on sustained upstream errors. The 30s service loop samples concurrency and prunes old usage rows; the real work happens per request.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -26,7 +26,7 @@ Chat requests are augmented (vision), forwarded, retried, priced, and ledgered.
|
||||
- the presented key equals the auto-generated **internal key** (`gateway_internal_key`), which is how DevPlace's own services authenticate, or
|
||||
- the request carries a session for an admin (and `Allow admins` is on) or any user (and `Allow users` is on).
|
||||
|
||||
`resolve_owner()` then records who made the call: `("internal", "devii")` for the internal key, `("key", "access")` for the access key, `("admin", uid)` or `("user", uid)` for a session user, else `("anonymous", "anonymous")`. This owner tuple is written on every ledger row so usage rolls up per user. Devii operating a signed-in user authenticates with that user's own API key, so the user's full gateway spend rolls up under their uid.
|
||||
`resolve_owner()` then records who made the call: `("internal", "devii")` for the internal key, `("key", "access")` for the access key, `("admin", uid)` or `("user", uid)` for a session user, else `("anonymous", "anonymous")`. This owner tuple is written on every ledger row so usage rolls up per user. Devii operating a signed-in user authenticates with that user's own API key, so their full gateway spend rolls up under their uid.
|
||||
|
||||
## Configuration fields
|
||||
|
||||
@@ -44,7 +44,7 @@ Chat requests are augmented (vision), forwarded, retried, priced, and ledgered.
|
||||
|
||||
## Cost computation
|
||||
|
||||
If the upstream response carries a numeric `cost` field, that is used verbatim and split proportionally into input and output (the row is flagged `native_cost`). Otherwise cost is computed from the per-1M pricing: for chat, `cache_hit_tokens` and `cache_miss_tokens` are priced separately on input plus `completion_tokens` on output; for vision, input and output use the vision rates. This is why prompt-cache hit rate directly drives cost: cache hits are roughly fifty times cheaper than misses at the default rates.
|
||||
If the upstream response carries a numeric `cost` field, that value is used verbatim and split proportionally into input and output (the row is flagged `native_cost`). Otherwise cost is computed from the per-1M pricing: for chat, `cache_hit_tokens` and `cache_miss_tokens` are priced separately on input plus `completion_tokens` on output; for vision, input and output use the vision rates. Prompt-cache hit rate directly drives cost: at the default rates cache hits are roughly fifty times cheaper than misses.
|
||||
|
||||
## Reliability layer
|
||||
|
||||
@@ -52,7 +52,7 @@ If the upstream response carries a numeric `cost` field, that is used verbatim a
|
||||
|
||||
## Vision augmentation
|
||||
|
||||
Before forwarding a chat request, image blocks are described by the vision model and replaced with a text description, so a text-only upstream still answers questions about images. Descriptions are cached in an LRU keyed by image hash; vision calls are themselves ledgered with `backend="vision"`. If vision is disabled or the key is missing, images are replaced with a placeholder note rather than failing the request.
|
||||
Before forwarding a chat request, image blocks are described by the vision model and replaced with a text description, so a text-only upstream still answers questions about images. Descriptions are cached in an LRU keyed by image hash; vision calls are themselves ledgered with `backend="vision"`. If vision is disabled or the key is missing, images become a placeholder note rather than failing the request.
|
||||
|
||||
## Data it offers
|
||||
|
||||
@@ -62,5 +62,5 @@ Before forwarding a chat request, image blocks are described by the vision model
|
||||
|
||||
**Concurrency samples** `gateway_concurrency_samples`: `in_flight` sampled once per service tick.
|
||||
|
||||
**Deep analytics** at `GET /admin/ai-usage/data` (`GatewayUsageOut`): per-hour buckets, token and latency percentiles, error breakdowns, cost (this hour, 24h, projected monthly, caching savings, by model, by caller), and behavior. TTFT and inter-token latency are intentionally null because the gateway calls the upstream non-streaming. Per-user usage is at `GET /admin/users/{uid}/ai-usage`. The whole picture is rendered on `/admin/ai-usage` by `AiUsageMonitor.js`.
|
||||
**Deep analytics** at `GET /admin/ai-usage/data` (`GatewayUsageOut`): per-hour buckets, token and latency percentiles, error breakdowns, cost (this hour, 24h, projected monthly, caching savings, by model, by caller), and behavior. TTFT and inter-token latency are null because the gateway calls the upstream non-streaming. Per-user usage is at `GET /admin/users/{uid}/ai-usage`. It is all rendered on `/admin/ai-usage` by `AiUsageMonitor.js`.
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
4. For each new article it grades the article, decides published versus draft, upserts the `news` row, records the sync state, and stores any extracted images.
|
||||
5. Logs a summary: new, updated, draft, grading-failed, and skipped counts.
|
||||
|
||||
**Grading:** the article title, description, and content (each length-capped) are sent to the grading endpoint with a content-strategy prompt that asks for a single integer from 1 to 10. The first integer in the response is parsed. An article graded at or above `news_grade_threshold` is published; below threshold it is a draft; if grading fails it is stored as a draft with grade 0 and sync status `grading_failed`. Nothing is silently discarded.
|
||||
**Grading:** the article title, description, and content (each length-capped) are sent to the grading endpoint with a content-strategy prompt asking for a single integer from 1 to 10, and the first integer in the response is parsed. An article graded at or above `news_grade_threshold` is published; below threshold it is a draft; a grading failure stores it as a draft with grade 0 and sync status `grading_failed`. Nothing is silently discarded.
|
||||
|
||||
## Configuration fields
|
||||
|
||||
@@ -35,5 +35,5 @@ Plus the inherited Enabled, Run interval, and Log buffer size fields.
|
||||
|
||||
## Data it offers
|
||||
|
||||
NewsService does not override `collect_metrics()`, so its `metrics` block is empty. Its health is read from the framework fields surfaced at `GET /admin/services/data`: `status` (`running` / `stopped` / `stalled`), `last_run`, `next_run`, `heartbeat`, and the `log_buffer`, where each run records its new/updated/draft/failed/skipped summary. The graded articles themselves are the product, visible under `/news` and in the `news` table.
|
||||
NewsService does not override `collect_metrics()`, so its `metrics` block is empty. Health is read from the framework fields surfaced at `GET /admin/services/data`: `status` (`running` / `stopped` / `stalled`), `last_run`, `next_run`, `heartbeat`, and the `log_buffer`, where each run records its new/updated/draft/failed/skipped summary. The graded articles are the product, visible under `/news` and in the `news` table.
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Services overview
|
||||
|
||||
DevPlace runs four long-lived background services inside the FastAPI process: the **AI gateway**, the **Devii assistant**, the **news fetcher**, and the **bot fleet**. They share one small framework (`BaseService` plus a `ServiceManager` singleton) so that every service is configured, supervised, monitored, and persisted the same way, and every service appears automatically on `/admin/services` with no extra wiring.
|
||||
DevPlace runs four long-lived background services inside the FastAPI process: the **AI gateway**, the **Devii assistant**, the **news fetcher**, and the **bot fleet**. They share one small framework (`BaseService` plus a `ServiceManager` singleton), so every service is configured, supervised, monitored, and persisted the same way and appears automatically on `/admin/services` with no extra wiring.
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests in the sidebar, the search index, and the documentation export. They describe internal mechanics, live data, and money.
|
||||
|
||||
@@ -18,12 +18,13 @@ The framework itself (lifecycle, configuration, persistence, the single-owner wo
|
||||
|
||||
## Design choices
|
||||
|
||||
**One framework, many services.** A service is a subclass of `BaseService` that overrides `async def run_once(self)` and declares a list of `ConfigField` objects. Registering it in `main.py` with `service_manager.register(...)` is the only integration step; configuration UI, validation, the run loop, status, logs, metrics, and the admin page are all inherited. This keeps each service focused on its own work and keeps the operational surface identical across all of them.
|
||||
**One framework, many services.** A service is a subclass of `BaseService` that overrides `async def run_once(self)` and declares a list of `ConfigField` objects. Registering it in `main.py` with `service_manager.register(...)` is the only integration step; configuration UI, validation, the run loop, status, logs, metrics, and the admin page are all inherited. Each service stays focused on its own work, and the operational surface is identical across all of them.
|
||||
|
||||
**Configuration lives in the database, not in code or environment.** Every tunable is a `ConfigField` stored in `site_settings` and editable at `/admin/services/{name}`. Defaults are sensible production values, so a service behaves correctly before any row exists. Secrets (API keys) are write-only in the UI: the current value is never echoed back, only whether it is set.
|
||||
|
||||
**State is persisted, memory is only an accelerator.** Each service writes a heartbeat, status, log tail, and metrics snapshot to the `service_state` table every few seconds. The admin page and the JSON endpoints read from that table, so status survives a worker restart and is visible from any worker, not only the one running the loop.
|
||||
|
||||
|
||||
**Single-owner supervision.** Across multiple uvicorn workers, exactly one worker acquires an exclusive file lock and supervises every service; the others run none. This is why news is never fetched twice, why the bot fleet is sized once, and why the Devii WebSocket is served only by the lock-owning worker. See [Service framework](/docs/services-framework.html) for the mechanics.
|
||||
|
||||
**AI is centralized.** The gateway is the only component that holds real upstream provider keys and URLs. Every other AI consumer (news grading, the bots, Devii) points at the internal gateway and authenticates with the auto-generated internal key, so all AI spend is attributed, priced, and limitable in one place. See [GatewayService (AI)](/docs/services-gateway.html).
|
||||
@@ -38,7 +39,7 @@ The framework itself (lifecycle, configuration, persistence, the single-owner wo
|
||||
|
||||
## What data each service offers
|
||||
|
||||
Every service exposes a `metrics` dictionary through `describe()`, surfaced live at `GET /admin/services/data`. This is the authoritative place to read the running AI statistics of all services at once.
|
||||
Every service exposes a `metrics` dictionary through `describe()`, surfaced live at `GET /admin/services/data` - the authoritative place to read the running AI statistics of all services at once.
|
||||
|
||||
| Service | Headline metrics it reports |
|
||||
|---------|------------------------------|
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
## What it does
|
||||
|
||||
A caller enqueues a job describing what to archive. The lock-owning worker materializes the source onto a temporary directory, runs a standalone Python subprocess to compress it, computes a CRC32 over the output, names the file from that checksum, and records full statistics. Callers poll the job until it is `done`, then download the archive. Archives that go unused for the retention window are deleted automatically.
|
||||
A caller enqueues a job describing what to archive. The lock-owning worker materializes the source onto a temporary directory, runs a standalone Python subprocess to compress it, computes a CRC32 over the output, names the file from that checksum, and records statistics. Callers poll the job until it is `done`, then download the archive. Archives unused for the retention window are deleted automatically.
|
||||
|
||||
## Source contract
|
||||
|
||||
@@ -17,15 +17,15 @@ A zip job payload carries a `source` object with a `type`. The only implemented
|
||||
{"source": {"type": "project_tree", "project_uid": "...", "path": "src"}}
|
||||
```
|
||||
|
||||
An empty `path` archives the whole project; a non-empty `path` archives that file or directory subtree. `ZipService._materialize` dispatches on the type and calls `project_files.export_to_dir`, which reconstructs the virtual project filesystem (inline text rows plus on-disk binary blobs) onto a real staging directory under the runtime data dir (`config.DATA_DIR/zip_staging/{job_uid}`, default `var/`, outside the package), with a traversal guard on every written path. Materialization runs inside the worker via `asyncio.to_thread`, never in the request handler.
|
||||
An empty `path` archives the whole project; a non-empty `path` archives that file or directory subtree. `ZipService._materialize` dispatches on the type and calls `project_files.export_to_dir`, which reconstructs the virtual project filesystem (inline text rows plus on-disk binary blobs) onto a staging directory under the runtime data dir (`config.DATA_DIR/zip_staging/{job_uid}`, default `var/`, outside the package), with a traversal guard on every written path. Materialization runs inside the worker via `asyncio.to_thread`, never in the request handler.
|
||||
|
||||
## Subprocess compression
|
||||
|
||||
`ZipService._run_worker` spawns `python -m devplacepy.services.jobs.zip_worker <staging_dir> <tmp_zip>` with `asyncio.create_subprocess_exec`. The worker is stdlib-only (no system `zip` dependency), walks the tree (sorted) into a `ZIP_DEFLATED` archive with a fixed entry timestamp so the output is **deterministic** (identical content yields identical bytes), computes a streaming `zlib.crc32` over the result, and prints a JSON stats line: `bytes_in`, `bytes_out`, `file_count`, `dir_count`, `crc32`. The service parses that JSON; a non-zero exit raises and the job is marked `failed`. Because the archive is deterministic, the CRC32 acts as a content identity: re-archiving the same content produces the same filename, and the final write simply overwrites the previous archive.
|
||||
`ZipService._run_worker` spawns `python -m devplacepy.services.jobs.zip_worker <staging_dir> <tmp_zip>` with `asyncio.create_subprocess_exec`. The worker is stdlib-only (no system `zip` dependency), walks the tree (sorted) into a `ZIP_DEFLATED` archive with a fixed entry timestamp so the output is **deterministic** (identical content yields identical bytes), computes a streaming `zlib.crc32` over the result, and prints a JSON stats line: `bytes_in`, `bytes_out`, `file_count`, `dir_count`, `crc32`. The service parses that JSON; a non-zero exit raises and the job is marked `failed`. Determinism makes the CRC32 a content identity: re-archiving the same content produces the same filename.
|
||||
|
||||
## Naming and storage
|
||||
|
||||
The final name is `{crc32}.{slug}.zip`, where the slug comes from the caller's preferred name with any trailing `.zip` stripped and the rest slugified. The archive lives under `config.DATA_DIR/zips/{uid[:2]}/{uid[2:4]}/` (outside the package, not under `/static`); it is served by the `/zips/{uid}/download` route via `FileResponse`, never the static mount. The temporary `{uuid4}.zip` is renamed into place with `os.replace`, so re-running a job simply overwrites the previous archive. The staging directory is removed afterwards.
|
||||
The final name is `{crc32}.{slug}.zip`, where the slug comes from the caller's preferred name with any trailing `.zip` stripped and the rest slugified. The archive lives under `config.DATA_DIR/zips/{uid[:2]}/{uid[2:4]}/` (outside the package, not under `/static`); it is served by the `/zips/{uid}/download` route via `FileResponse`, never the static mount. The temporary `{uuid4}.zip` is renamed into place with `os.replace`, so re-running a job overwrites the previous archive. The staging directory is then removed.
|
||||
|
||||
## Endpoints
|
||||
|
||||
@@ -34,11 +34,11 @@ The final name is `{crc32}.{slug}.zip`, where the slug comes from the caller's p
|
||||
- `GET /zips/{uid}` returns the job status and statistics (`ZipJobOut`). `download_url` is null until the job is `done`.
|
||||
- `GET /zips/{uid}/download` streams the finished archive as `application/zip` and extends the retention window on each access.
|
||||
|
||||
The job uid (a sortable uuid7) is the capability token: anyone with the link can download, which matches the fact that projects are publicly viewable. The owner recorded on the job is used only for attribution and metrics, never for access control.
|
||||
The job uid (a sortable uuid7) is the capability token: anyone with the link can download, matching the fact that projects are publicly viewable. The owner recorded on the job is used only for attribution and metrics, never for access control.
|
||||
|
||||
## Frontend
|
||||
|
||||
`static/js/ZipDownloader.js` (instantiated as `app.zipDownloader`) drives the whole flow: it POSTs to the enqueue URL, polls `GET /zips/{uid}` through the shared `JobPoller` (`static/js/JobPoller.js`), and triggers a hidden download link once the job is `done`, with toasts for progress and errors. Any element with a `data-zip-download` attribute is wired automatically (the "Download zip" button on the project detail page and the "Download as zip" button on the files page); the files context menu calls `app.zipDownloader.start(url)` directly for a single file or folder.
|
||||
`static/js/ZipDownloader.js` (instantiated as `app.zipDownloader`) drives the flow: it POSTs to the enqueue URL, polls `GET /zips/{uid}` through the shared `JobPoller` (`static/js/JobPoller.js`), and triggers a hidden download link once the job is `done`, with toasts for progress and errors. Any element with a `data-zip-download` attribute is wired automatically (the "Download zip" button on the project detail page and the "Download as zip" button on the files page); the files context menu calls `app.zipDownloader.start(url)` directly for a single file or folder.
|
||||
|
||||
## Configuration, metrics, and quotas
|
||||
|
||||
|
||||
@@ -12,41 +12,41 @@ stamps two columns on its row instead of erasing it:
|
||||
- `deleted_by` - who removed it: a user uid, or `system` for automated actions.
|
||||
|
||||
A live row has `deleted_at = NULL`. Every read path that lists or counts data filters on
|
||||
`deleted_at IS NULL`, so a soft-deleted record disappears from the entire product immediately, yet
|
||||
stays in the database for audit and recovery. Both columns are indexed (`idx_<table>_deleted`) so the
|
||||
filter stays fast at scale.
|
||||
`deleted_at IS NULL`, so a soft-deleted record disappears from the product immediately yet stays in
|
||||
the database for audit and recovery. Both columns are indexed (`idx_<table>_deleted`) to keep the
|
||||
filter fast at scale.
|
||||
|
||||
## The two stages of deletion
|
||||
|
||||
Soft delete only makes sense alongside a second stage that actually frees storage. The platform
|
||||
distinguishes them deliberately:
|
||||
Soft delete pairs with a second stage that frees storage. The platform distinguishes them
|
||||
deliberately:
|
||||
|
||||
1. **Removal (soft).** A user or admin removing something they care about - a post, comment, gist,
|
||||
project, news article, project file, attachment, a vote/reaction/bookmark/follow toggle, a
|
||||
session (logout), a container instance or schedule, a Devii task, lesson, or custom tool, a
|
||||
per-owner customization. These set `deleted_at` + `deleted_by` and are reversible.
|
||||
2. **Purge / garbage collection (hard).** Operations whose entire purpose is to bound storage stay
|
||||
physical deletes, because soft-deleting them would mean nothing is ever freed: async job rows and
|
||||
their staging directories, the container metrics ring buffer, AI usage-ledger retention sweeps and
|
||||
quota resets, expired-session cleanup, fork-rollback of a half-created project, and the news-sync
|
||||
image refresh. The admin **Purge** action below is the manual counterpart - it empties the trash.
|
||||
1. **Removal (soft).** A user or admin removing a post, comment, gist, project, news article, project
|
||||
file, attachment, a vote/reaction/bookmark/follow toggle, a session (logout), a container instance
|
||||
or schedule, a Devii task, lesson, or custom tool, or a per-owner customization. These set
|
||||
`deleted_at` + `deleted_by` and are reversible.
|
||||
2. **Purge / garbage collection (hard).** Operations whose purpose is to bound storage stay physical
|
||||
deletes, since soft-deleting them would free nothing: async job rows and their staging
|
||||
directories, the container metrics ring buffer, AI usage-ledger retention sweeps and quota resets,
|
||||
expired-session cleanup, fork-rollback of a half-created project, and the news-sync image refresh.
|
||||
The admin **Purge** action below is the manual counterpart that empties the trash.
|
||||
|
||||
This is why "everything is soft-deleted" is true for removals while retention and ring-buffer
|
||||
mechanics remain hard: the second stage is what makes the first stage safe.
|
||||
The second stage is what makes the first safe: removals are reversible while retention and
|
||||
ring-buffer mechanics remain hard.
|
||||
|
||||
## Toggles revive instead of duplicating
|
||||
|
||||
Vote, reaction, bookmark, follow, and poll-vote actions are toggles. Toggling off soft-deletes the
|
||||
row; toggling the same target back on **revives the same physical row** (clears `deleted_at`) rather
|
||||
than inserting a second one. Counts and "did I react?" reads filter `deleted_at IS NULL`, so a
|
||||
revived toggle is counted exactly once and history is never duplicated.
|
||||
revived toggle is counted exactly once.
|
||||
|
||||
## Cascades share one timestamp
|
||||
|
||||
When a parent is removed, its children are soft-deleted in the same operation with the **same
|
||||
`deleted_at` timestamp and `deleted_by`**. Deleting a post, for example, stamps the post plus its
|
||||
comments, votes, reactions, bookmarks, poll, and attachments with one shared timestamp. That single
|
||||
timestamp identifies the whole deletion event, which is what makes a one-click restore possible.
|
||||
`deleted_at` timestamp and `deleted_by`**. Deleting a post stamps the post plus its comments, votes,
|
||||
reactions, bookmarks, poll, and attachments with one shared timestamp. That timestamp identifies the
|
||||
whole deletion event, which makes a one-click restore possible.
|
||||
|
||||
## Who can do what
|
||||
|
||||
@@ -56,8 +56,8 @@ timestamp identifies the whole deletion event, which is what makes a one-click r
|
||||
| Restore | Admin only | Reverses the entire deletion event |
|
||||
| Purge | Admin only | Permanent hard delete of the event (rows + files) |
|
||||
|
||||
Members and guests are never shown the soft-delete, restore, or purge concepts; from their point of
|
||||
view a delete simply removes the item. Recovery is an administrator surface.
|
||||
Members and guests never see the soft-delete, restore, or purge concepts; for them a delete simply
|
||||
removes the item. Recovery is an administrator surface.
|
||||
|
||||
## The Trash
|
||||
|
||||
@@ -66,29 +66,29 @@ The **Trash** entry in the admin sidebar opens `/admin/trash`. It lists soft-del
|
||||
it was removed, and the resolved `deleted_by` username. Two actions per row:
|
||||
|
||||
- **Restore** (`POST /admin/trash/{table}/{uid}/restore`) - reads the row's `deleted_at` and reverses
|
||||
every row across all tables that share that timestamp, bringing the item and everything removed with
|
||||
it back to the live product in one step.
|
||||
every row across all tables sharing that timestamp, bringing the item and everything removed with it
|
||||
back to the live product in one step.
|
||||
- **Purge** (`POST /admin/trash/{table}/{uid}/purge`) - permanently hard-deletes every row sharing
|
||||
that timestamp and unlinks any attachment files or project-file blobs from disk. This cannot be
|
||||
undone and is the only way soft-deleted content is physically removed from the UI.
|
||||
|
||||
The existing **Media** trash (`/admin/media`) is the attachment-specific view of the same model and
|
||||
continues to work as before; see [Media moderation](/docs/media-moderation.html).
|
||||
The **Media** trash (`/admin/media`) is the attachment-specific view of the same model; see
|
||||
[Media moderation](/docs/media-moderation.html).
|
||||
|
||||
## Security and audit notes
|
||||
|
||||
- **Accountability.** `deleted_by` records the actor for every removal, including cascades, so the
|
||||
trash doubles as an audit trail of who removed what and when.
|
||||
- **Logout is auditable, not destructive.** Signing out soft-deletes the session row (`deleted_by`
|
||||
is the user) and the auth cache is cleared, so the session is rejected immediately; expired
|
||||
sessions are still hard-swept by the cleanup path.
|
||||
is the user) and clears the auth cache, so the session is rejected immediately; expired sessions
|
||||
are still hard-swept by the cleanup path.
|
||||
- **Right-to-be-forgotten.** Bulk "forget" operations (clearing a Devii conversation, forgetting all
|
||||
lessons, resetting a usage quota) remain hard deletes by design, so a privacy wipe leaves nothing
|
||||
lessons, resetting a usage quota) remain hard deletes, so a privacy wipe leaves nothing
|
||||
recoverable.
|
||||
- **Purge is irreversible.** It is the only hard delete reachable from the admin UI and deletes files
|
||||
from disk; the confirmation dialog reflects that.
|
||||
- **Retention is unchanged.** Job artifacts, metrics rings, and AI usage ledgers continue to be
|
||||
pruned on their existing schedules; soft delete does not extend their lifetime.
|
||||
- **Retention is unchanged.** Job artifacts, metrics rings, and AI usage ledgers are still pruned on
|
||||
their existing schedules; soft delete does not extend their lifetime.
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
{% raw %}
|
||||
# Colors
|
||||
|
||||
The palette is defined once, as CSS custom properties on `:root` in `static/css/variables.css`. Every stylesheet references those tokens; no file hardcodes a hex value. To change the theme you edit one file.
|
||||
The palette is defined once, as CSS custom properties on `:root` in `static/css/variables.css`. Every stylesheet references those tokens; no file hardcodes a hex value, so changing the theme means editing one file.
|
||||
|
||||
## The vision
|
||||
|
||||
DevPlace is a tool people keep open for hours, often at night. The theme is therefore **dark, low-glare, and deliberately quiet**: deep violet-black backgrounds, soft lavender text, and a *single* warm accent.
|
||||
DevPlace is kept open for hours, often at night. The theme is therefore **dark, low-glare, and deliberately quiet**: deep violet-black backgrounds, soft lavender text, and a *single* warm accent.
|
||||
|
||||
- **One accent, used sparingly.** A single orange (`--accent`, `#ff6b35`) marks what matters: the primary action, the active navigation item, links, and the current vote. Because nothing else competes for it, the accent always means "this is the thing to act on". The moment a second strong colour is used decoratively, the accent stops working.
|
||||
- **One accent, used sparingly.** A single orange (`--accent`, `#ff6b35`) marks what matters: the primary action, the active navigation item, links, and the current vote. Because nothing else competes for it, the accent always means "act on this". A second strong colour used decoratively breaks it.
|
||||
- **Backgrounds layer by elevation.** A near-black page sits behind slightly lighter cards, behind a lighter hover state. Depth comes from these few background steps plus a soft shadow, not from borders alone.
|
||||
- **Text is a three-step hierarchy.** Primary for content, secondary for supporting text, muted for metadata. There is no fourth shade.
|
||||
- **Colour with meaning is reserved.** Status colours (success, warning, danger, info) and topic colours carry semantics. They are never used to "brighten up" a layout.
|
||||
- **Colour with meaning is reserved.** Status colours (success, warning, danger, info) and topic colours carry semantics. They never "brighten up" a layout.
|
||||
|
||||
## Background and surface
|
||||
|
||||
@@ -58,7 +58,7 @@ A strict three-step hierarchy. Never introduce a fourth text shade.
|
||||
|
||||
## Status colours (semantic only)
|
||||
|
||||
These encode meaning and must only be used to convey that meaning.
|
||||
These encode meaning and must only be used to convey it.
|
||||
|
||||
| Token | Value | Means |
|
||||
|-------|-------|-------|
|
||||
@@ -82,7 +82,7 @@ Used exclusively for topic badges (`.badge-devlog`, `.badge-showcase`, …) and
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Never hardcode a colour.** Use the token: `color: var(--text-secondary)`, never `color: #b8a8d0`. If a value you need is missing, add a token to `variables.css` rather than inlining a hex.
|
||||
1. **Never hardcode a colour.** Use the token: `color: var(--text-secondary)`, never `color: #b8a8d0`. If a value is missing, add a token to `variables.css` rather than inlining a hex.
|
||||
2. **One accent per view.** Exactly one primary action should carry `--accent`. Everything else is `--btn-secondary`/ghost.
|
||||
3. **Status and topic colours are semantic.** Do not use `--danger` for a non-destructive button or a topic colour as a background flourish.
|
||||
4. **Respect the text hierarchy.** Three shades only: primary, secondary, muted.
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
{% raw %}
|
||||
# Consistency rules
|
||||
|
||||
These are hard, structural rules, not suggestions. They are derived from the **posts / feed page**, which is the reference implementation: structurally and visually it is the most correct page in the application. Other pages will be refactored to match it; when a new page is built, it follows these rules from the start.
|
||||
These are hard, structural rules, not suggestions. They are derived from the **posts / feed page**, the reference implementation and the most structurally and visually correct page in the application. Other pages are refactored to match it; new pages follow these rules from the start.
|
||||
|
||||
The point of the rules is that the header, breadcrumb, content root, and footer are *shared infrastructure*. A page supplies its content and its stylesheet; it never rebuilds the frame.
|
||||
The header, breadcrumb, content root, and footer are *shared infrastructure*. A page supplies its content and its stylesheet; it never rebuilds the frame.
|
||||
|
||||
## The reference skeleton
|
||||
|
||||
This is the shape of the posts page, reduced to its structure. A content page looks like this and nothing else:
|
||||
This is the posts page reduced to its structure. A content page looks like this and nothing else:
|
||||
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
@@ -38,7 +38,7 @@ Every page is `{% extends "base.html" %}`. Page CSS is added only through `{% bl
|
||||
|
||||
### 2. The header is shared, never re-implemented
|
||||
|
||||
The top nav is `base.html`'s `.topnav`. Pages do not build their own header or navigation bar. The active link is derived from the request path inside `base.html`. If a page needs a page-level toolbar (tabs, filters), that toolbar lives *inside the content*, like the feed's `.feed-nav` strip, below the breadcrumb, not as a second site header.
|
||||
The top nav is `base.html`'s `.topnav`. Pages do not build their own header or navigation bar; the active link is derived from the request path inside `base.html`. A page-level toolbar (tabs, filters) lives *inside the content*, like the feed's `.feed-nav` strip, below the breadcrumb, never as a second site header.
|
||||
|
||||
### 3. Breadcrumbs are mandatory (this is the most-broken rule)
|
||||
|
||||
@@ -52,7 +52,7 @@ seo_ctx = list_page_seo(
|
||||
)
|
||||
```
|
||||
|
||||
This is not only for SEO. The `.breadcrumb` element carries `padding-top: calc(var(--nav-height) + 0.5rem)`, and that padding is **what pushes the content clear of the fixed top nav**. A page that ships no breadcrumbs (or only one) has its first content hidden behind the header. Always supply two or more.
|
||||
This is not only for SEO. The `.breadcrumb` element carries `padding-top: calc(var(--nav-height) + 0.5rem)`, the padding that **pushes content clear of the fixed top nav**. A page with no breadcrumbs (or only one) has its first content hidden behind the header. Always supply two or more.
|
||||
|
||||
### 4. All content lives in the single content root
|
||||
|
||||
@@ -70,11 +70,11 @@ Side columns are `<aside>`; the fluid centre column sets `min-width: 0`. The gri
|
||||
|
||||
### 6. One H1 per page
|
||||
|
||||
Each page has exactly one `<h1>`. When the heading would be visually redundant (the feed's title), it is kept for accessibility with `class="sr-only"` rather than removed.
|
||||
Each page has exactly one `<h1>`. When the heading is visually redundant (the feed's title), keep it for accessibility with `class="sr-only"` rather than removing it.
|
||||
|
||||
### 7. The footer is shared
|
||||
|
||||
The footer is `base.html`'s `.site-footer`, rendered from the `footer` block. Pages do not build their own footer. Override the `footer` block only with a deliberate reason (for example, a full-bleed tool page), and document why.
|
||||
The footer is `base.html`'s `.site-footer`, rendered from the `footer` block. Pages do not build their own footer. Override the `footer` block only with a deliberate reason (for example, a full-bleed tool page) and document why.
|
||||
|
||||
### 8. Cards and surfaces use tokens
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
{% raw %}
|
||||
# Layout
|
||||
|
||||
Layout uses CSS Grid and Flexbox. There is one shared page shell, and a small set of approved content layouts that sit inside it. New pages pick one of these rather than inventing a new structure.
|
||||
Layout uses CSS Grid and Flexbox: one shared page shell with a small set of approved content layouts inside it. New pages pick one of these rather than inventing a structure.
|
||||
|
||||
## The page shell
|
||||
|
||||
Every page is wrapped by `base.html` in the same four-part shell, from top to bottom:
|
||||
|
||||
1. **Fixed top nav** (`.topnav`, height `--nav-height` = `56px`, `position: fixed`, `z-index: 1000`). It is always on screen and is never re-implemented per page.
|
||||
2. **Breadcrumb** (`.breadcrumb`, optional but expected). It renders only when the page supplies two or more crumbs. Critically, it carries `padding-top: calc(var(--nav-height) + 0.5rem)`, which is what pushes content clear of the fixed nav. A page that omits breadcrumbs slides under the header. See [Consistency rules](/docs/styles-consistency.html).
|
||||
3. **Content root** (`<main class="page">`, `max-width: var(--max-content)` = `1200px`, centred, `padding: 1rem`). All page content lives inside this single element.
|
||||
1. **Fixed top nav** (`.topnav`, height `--nav-height` = `56px`, `position: fixed`, `z-index: 1000`). Always on screen, never re-implemented per page.
|
||||
2. **Breadcrumb** (`.breadcrumb`, optional but expected). It renders only when the page supplies two or more crumbs. It carries `padding-top: calc(var(--nav-height) + 0.5rem)`, which pushes content clear of the fixed nav; a page that omits breadcrumbs slides under the header. See [Consistency rules](/docs/styles-consistency.html).
|
||||
3. **Content root** (`<main class="page">`, `max-width: var(--max-content)` = `1200px`, centred, `padding: 1rem`). All page content lives inside this one element.
|
||||
4. **Footer** (`.site-footer`), rendered from the `footer` block.
|
||||
|
||||
```text
|
||||
@@ -32,7 +32,7 @@ A page never re-declares `max-width: 1200px`, never adds a second centred wrappe
|
||||
|
||||
### 1. Three-column app layout (the default)
|
||||
|
||||
The feed / posts page. A fixed-width left sidebar, a fluid main column, and a fixed-width right rail.
|
||||
The feed / posts page: a fixed-width left sidebar, a fluid main column, and a fixed-width right rail.
|
||||
|
||||
```css
|
||||
.feed-layout {
|
||||
@@ -83,7 +83,7 @@ Auto-filling responsive grid for galleries such as projects:
|
||||
|
||||
## Cards
|
||||
|
||||
A card is the standard content surface: `--bg-card` background, a `--border` hairline, `--radius-lg` corners, and `1rem` padding. The shared `.card` class encodes exactly that, and `.post-card` / `.project-card` are the same recipe. On hover a card lifts its border to `--border-light` and adds `--shadow-sm`; it does not change its background wholesale.
|
||||
A card is the standard content surface: `--bg-card` background, a `--border` hairline, `--radius-lg` corners, and `1rem` padding. The shared `.card` class encodes that; `.post-card` / `.project-card` use the same recipe. On hover a card lifts its border to `--border-light` and adds `--shadow-sm`, without changing its background.
|
||||
|
||||
## Spacing
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{% raw %}
|
||||
# Responsiveness
|
||||
|
||||
The interface is designed to work from a 320px phone to a wide desktop without a separate mobile site. The same HTML reflows; only the CSS changes per breakpoint.
|
||||
The interface works from a 320px phone to a wide desktop without a separate mobile site. The same HTML reflows; only the CSS changes per breakpoint.
|
||||
|
||||
## The goal
|
||||
|
||||
@@ -20,7 +20,7 @@ The viewport is declared once in `base.html` and must not be changed:
|
||||
|
||||
## The standard breakpoint ladder
|
||||
|
||||
Use these four breakpoints. They are `max-width` (desktop-first refinements), and they are the ones the shared shell already targets, so a page that uses them stays in step with the nav, breadcrumb, and footer.
|
||||
Use these four breakpoints. They are `max-width` (desktop-first refinements) and the ones the shared shell already targets, so a page that uses them stays in step with the nav, breadcrumb, and footer.
|
||||
|
||||
| Breakpoint | What changes |
|
||||
|-----------|--------------|
|
||||
@@ -29,11 +29,11 @@ Use these four breakpoints. They are `max-width` (desktop-first refinements), an
|
||||
| `480px` | Compact paddings; secondary actions hidden; post/detail typography steps down; `grid-2col` becomes one column; floating buttons honour safe-area insets. |
|
||||
| `360px` | Headers allowed to wrap; timestamps drop to their own line; the comment avatar is hidden so the input keeps room. |
|
||||
|
||||
> A handful of one-off breakpoints (960, 900, 760, 720, 640, …) exist in older stylesheets. They are exactly the inconsistency this section exists to remove. **Do not add new ad-hoc breakpoints**: reach for `1024 / 768 / 480 / 360` first, and only introduce another value if a specific component genuinely breaks between two of them, with a comment saying why.
|
||||
> A handful of one-off breakpoints (960, 900, 760, 720, 640, …) exist in older stylesheets, the exact inconsistency this section exists to remove. **Do not add new ad-hoc breakpoints**: reach for `1024 / 768 / 480 / 360` first, and only introduce another value if a specific component genuinely breaks between two of them, with a comment saying why.
|
||||
|
||||
## How it is achieved
|
||||
|
||||
**Collapse a grid to one column.** This is the whole mobile story for a multi-column page:
|
||||
**Collapse a grid to one column.** The whole mobile story for a multi-column page:
|
||||
|
||||
```css
|
||||
@media (max-width: 1024px) {
|
||||
@@ -57,7 +57,7 @@ Use these four breakpoints. They are `max-width` (desktop-first refinements), an
|
||||
}
|
||||
```
|
||||
|
||||
**Guarantee touch targets and prevent zoom.** One shared rule in `base.css` covers every page, so individual pages do not repeat it:
|
||||
**Guarantee touch targets and prevent zoom.** One shared rule in `base.css` covers every page, so pages do not repeat it:
|
||||
|
||||
```css
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
{% raw %}
|
||||
# Design system
|
||||
|
||||
DevPlace has one visual language. It is hand-written vanilla CSS driven by design tokens, with no preprocessor, no CSS-in-JS, and no utility framework. This section is the canonical reference for how the interface looks and is structured, and it states the rules every page is expected to follow.
|
||||
DevPlace has one visual language: hand-written vanilla CSS driven by design tokens, with no preprocessor, no CSS-in-JS, and no utility framework. This section is the canonical reference for how the interface looks and is structured, and states the rules every page must follow.
|
||||
|
||||
> The feed / posts page is the reference implementation. When two pages disagree, the feed page wins and the other is the one to fix. The pages below describe the system; the [Consistency rules](/docs/styles-consistency.html) page turns it into hard requirements.
|
||||
> The feed / posts page is the reference implementation. When two pages disagree, the feed page wins and the other gets fixed. The pages below describe the system; the [Consistency rules](/docs/styles-consistency.html) page turns it into hard requirements.
|
||||
|
||||
## The four pages
|
||||
|
||||
@@ -18,7 +18,7 @@ DevPlace has one visual language. It is hand-written vanilla CSS driven by desig
|
||||
## Principles
|
||||
|
||||
- **Tokens over literals.** Every colour, space, radius, and shadow is a CSS custom property defined once in `variables.css`. Stylesheets reference the token, never a raw hex or pixel value.
|
||||
- **One layout per page.** A page has a single top-level layout container inside the shared content root; it does not nest competing wrappers or re-declare the maximum width.
|
||||
- **One layout per page.** A page has a single top-level layout container inside the shared content root; it never nests competing wrappers or re-declares the maximum width.
|
||||
- **Mobile is not an afterthought.** Multi-column layouts collapse to one fluid column, touch targets grow, and nothing scrolls horizontally by accident.
|
||||
- **Structure is shared, content is per page.** The header, breadcrumb, content root, and footer come from `base.html`. A page supplies only its own content and its own stylesheet.
|
||||
{% endraw %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Coverage and CI/CD
|
||||
|
||||
How the test suite is measured for coverage and how the automated DTAP streets move a change from a developer's machine to production. See also [Testing overview](/docs/testing.html) and [Running tests via make](/docs/testing-make.html).
|
||||
How the test suite is measured for coverage, and how the automated DTAP streets move a change from a developer's machine to production. See also [Testing overview](/docs/testing.html) and [Running tests via make](/docs/testing-make.html).
|
||||
|
||||
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
|
||||
|
||||
@@ -10,7 +10,7 @@ How the test suite is measured for coverage and how the automated DTAP streets m
|
||||
Coverage is measured with `coverage.py` over the `devplacepy` package, configured in `.coveragerc`:
|
||||
|
||||
- **Source** is `devplacepy`; `tests/*` and `sitecustomize.py` are omitted.
|
||||
- **`parallel = true`** so coverage data from subprocesses is collected and later combined. This matters because the end-to-end tests run the application as a separate Uvicorn subprocess: `COVERAGE_PROCESS_START` plus a `sitecustomize.py` hook starts coverage inside that server process, so browser-driven requests count toward coverage, not just the in-process tests.
|
||||
- **`parallel = true`** so coverage data from subprocesses is collected and later combined. The end-to-end tests run the application as a separate Uvicorn subprocess: `COVERAGE_PROCESS_START` plus a `sitecustomize.py` hook starts coverage inside that server process, so browser-driven requests count toward coverage, not just the in-process tests.
|
||||
- **`sigterm = true`** so the subprocess flushes its coverage data when it is terminated at the end of the session.
|
||||
- **`show_missing = true`** lists the uncovered lines in the report.
|
||||
|
||||
@@ -22,7 +22,7 @@ The `make` targets that produce coverage:
|
||||
| `make coverage-headed` | The same, with a visible browser. |
|
||||
| `make coverage-html` | Runs `make coverage`, then `coverage html` and writes the browsable report to `htmlcov/index.html`. |
|
||||
|
||||
The whole suite already runs in a single process (serial execution is enforced globally in `pyproject.toml`), so `coverage combine` only has to merge two profiles: the test process and the application's Uvicorn subprocess. There are no per-worker profiles to reconcile, which keeps the combined report stable.
|
||||
The whole suite runs in a single process (serial execution is enforced globally in `pyproject.toml`), so `coverage combine` merges only two profiles: the test process and the application's Uvicorn subprocess. There are no per-worker profiles to reconcile, which keeps the combined report stable.
|
||||
|
||||
## Continuous integration: Gitea Actions
|
||||
|
||||
@@ -31,8 +31,8 @@ The pipeline is defined in `.gitea/workflows/test.yaml` and runs on every **push
|
||||
1. Check out the repository.
|
||||
2. Set up Python 3.13.
|
||||
3. Install dependencies: `pip install -e ".[dev]"` and `python -m playwright install chromium --with-deps`.
|
||||
4. Run the full serial suite under coverage: `python -m coverage run -m pytest tests/`, with `PLAYWRIGHT_HEADLESS=1` and `COVERAGE_PROCESS_START` pointing at `.coveragerc`. The serial flags (`--tb=line -p no:xdist`) come from `addopts` in `pyproject.toml`, so the command line stays minimal.
|
||||
5. Build the coverage report (`coverage combine`, `coverage report`, `coverage html`). This step runs with `if: always()` so a report is produced even when tests fail.
|
||||
4. Run the full serial suite under coverage: `python -m coverage run -m pytest tests/`, with `PLAYWRIGHT_HEADLESS=1` and `COVERAGE_PROCESS_START` pointing at `.coveragerc`. The serial flags (`--tb=line -p no:xdist`) come from `addopts` in `pyproject.toml`.
|
||||
5. Build the coverage report (`coverage combine`, `coverage report`, `coverage html`). This step runs with `if: always()`, so a report is produced even when tests fail.
|
||||
6. Publish the `coverage-html` artifact (the `htmlcov/` directory), always.
|
||||
7. On failure only, upload the `failure-screenshots` artifact from `/tmp/devplace_test_screenshots/` (saved by the `pytest_runtest_makereport` hook in `conftest.py`).
|
||||
|
||||
@@ -40,7 +40,7 @@ The pipeline is defined in `.gitea/workflows/test.yaml` and runs on every **push
|
||||
|
||||
## The automated DTAP streets
|
||||
|
||||
DevPlace promotes a change through automated Development, Test, Acceptance, and Production stages. They are wired to two branches (`master` and `production`) and the CI pipeline above:
|
||||
DevPlace promotes a change through automated Development, Test, Acceptance, and Production stages, wired to two branches (`master` and `production`) and the CI pipeline above:
|
||||
|
||||
| Street | Where | What happens |
|
||||
|--------|-------|--------------|
|
||||
@@ -49,5 +49,5 @@ DevPlace promotes a change through automated Development, Test, Acceptance, and
|
||||
| **Acceptance** | the `master` to `production` promotion | `make deploy` fast-forwards the release branch: `git checkout production && git merge master && git push origin production`. Only commits that are already green on `master` reach `production`, so the promotion is the controlled release gate. |
|
||||
| **Production** | the host running Docker Compose | The production host pulls the `production` branch and runs the nginx + app stack. Most updates are a `git pull` plus a restart because code is bind-mounted; the image is rebuilt only when dependencies change. See the [Production overview](/docs/production.html) section. |
|
||||
|
||||
The streets are sequential and gated: nothing reaches `production` that has not passed the Test street on `master`, and the `production` branch is only ever fast-forwarded from tested `master` commits. Coverage is produced and archived at the Test street on every run, so the coverage trend is always available as a CI artifact.
|
||||
The streets are sequential and gated: nothing reaches `production` that has not passed the Test street on `master`, and the `production` branch is only ever fast-forwarded from tested `master` commits. Coverage is archived at the Test street on every run, so the coverage trend is always available as a CI artifact.
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ The structure of the correctness suite, the fixtures that back it, and the manda
|
||||
|
||||
## Playwright, not pytest-playwright
|
||||
|
||||
The suite uses **raw Playwright** driven by plain `pytest`, not the `pytest-playwright` plugin. The two conflict over fixture names. If `pytest-playwright` is installed, uninstall it before running the suite.
|
||||
The suite uses **raw Playwright** driven by plain `pytest`, not the `pytest-playwright` plugin, because the two conflict over fixture names. If `pytest-playwright` is installed, uninstall it before running the suite.
|
||||
|
||||
## Layout: three tiers, a directory tree that mirrors the path
|
||||
|
||||
@@ -21,7 +21,7 @@ A test's tier is decided by the fixtures it uses: a browser fixture (`page`/`ali
|
||||
|
||||
**unit directories mirror the source module:** tests for `devplacepy/utils.py` live in `tests/unit/utils.py`, and tests for `devplacepy/services/audit/store.py` live in `tests/unit/services/audit/store.py`.
|
||||
|
||||
Every directory is a Python package (`__init__.py`). Files are **not** `test_`-prefixed, so `pyproject.toml` widens discovery with `python_files = ["*.py"]` and points `testpaths` at the three subdirectories; test *functions* still start with `test_`. The single `tests/conftest.py` stays at the `tests/` root so its fixtures inherit into every tier. Helpers shared across files import from the canonical new module path (for example `from tests.e2e.post import create_post`).
|
||||
Every directory is a Python package (`__init__.py`). Files are **not** `test_`-prefixed, so `pyproject.toml` widens discovery with `python_files = ["*.py"]` and points `testpaths` at the three subdirectories; test *functions* still start with `test_`. The single `tests/conftest.py` stays at the `tests/` root so its fixtures inherit into every tier. Helpers shared across files import from the canonical module path (for example `from tests.e2e.post import create_post`).
|
||||
|
||||
## Fixture stack
|
||||
|
||||
@@ -46,7 +46,7 @@ Because every test shares one server and one database, tests that mutate global
|
||||
|
||||
## Mandatory patterns
|
||||
|
||||
These are not style preferences. Violating them produces flaky or hanging tests:
|
||||
These are not style preferences; violating them produces flaky or hanging tests:
|
||||
|
||||
- **Always pass `wait_until="domcontentloaded"`** to every `page.goto(...)` and `page.wait_for_url(...)`. CDN libraries and avatars make the default `"load"` event time out.
|
||||
- **Prefer `page.locator(...).wait_for(state="visible")`** over `wait_for_selector`.
|
||||
@@ -63,7 +63,7 @@ A `pytest_runtest_makereport` hook saves a screenshot to `/tmp/devplace_test_scr
|
||||
- **JSON response shape, auth guards, status codes** against the live server: an HTTP test in `tests/api/<endpoint>.py`.
|
||||
- **Data helpers and pure logic** with no server: a unit test in `tests/unit/<module>.py`.
|
||||
|
||||
Many features need all three: the unit test pins helper logic, the api test pins the JSON contract and guards, and the e2e test proves the page works.
|
||||
Many features need all three: the unit test pins helper logic, the api test pins the JSON contract and guards, and the e2e test proves the page renders.
|
||||
|
||||
## Running a single test
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
## What it simulates
|
||||
|
||||
A single `DevPlaceUser` class models a realistic visitor with `wait_time = between(1, 5)` seconds between actions. Its tasks are weighted so common reads happen far more often than rare writes: browsing the feed, opening posts, projects, gists, and news weigh heavily, while creating content, commenting, voting, and admin actions weigh less. On start each simulated user registers or logs in, so authenticated flows are exercised, not just public pages.
|
||||
A single `DevPlaceUser` class models a visitor with `wait_time = between(1, 5)` seconds between actions. Tasks are weighted so common reads happen far more often than rare writes: browsing the feed and opening posts, projects, gists, and news weigh heavily, while creating content, commenting, voting, and admin actions weigh less. On start each user registers or logs in, so authenticated flows are exercised, not just public pages.
|
||||
|
||||
The file seeds a pool of users and content at launch (via an `events.init` listener) and harvests real slugs and UUIDs from the running site, so requests hit live resources rather than fabricated identifiers.
|
||||
|
||||
@@ -18,7 +18,7 @@ make locust # interactive: opens the Locust web UI
|
||||
make locust-headless # CI mode: fixed users/run-time, writes an HTML report
|
||||
```
|
||||
|
||||
`make locust` opens the Locust web interface, where you set the user count and spawn rate and watch live charts. `make locust-headless` runs unattended with preset parameters and writes a report to the run directory.
|
||||
`make locust` opens the web interface, where you set the user count and spawn rate and watch live charts. `make locust-headless` runs unattended with preset parameters and writes a report to the run directory.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -46,5 +46,5 @@ The rig points `DEVPLACE_DATABASE_URL` at a fresh file under `LOCUST_DB_DIR`, re
|
||||
|
||||
## Reading the results
|
||||
|
||||
In the web UI, watch the requests-per-second, the response-time percentiles, and the failure count. A healthy run holds a low median latency with a stable RPS and zero failures. Rising p95 latency or a climbing failure rate as users ramp marks the point where the platform saturates. The headless report captures the same numbers as a static HTML file for archiving or CI comparison.
|
||||
In the web UI, watch the requests-per-second, the response-time percentiles, and the failure count. A healthy run holds a low median latency with stable RPS and zero failures. Rising p95 latency or a climbing failure rate as users ramp marks where the platform saturates. The headless report captures the same numbers as a static HTML file for archiving or CI comparison.
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="docs-content" data-render>
|
||||
# Running tests via make
|
||||
|
||||
Every test workflow has a `make` target so the commands are short and identical across machines and CI. See also [Test framework and rules](/docs/testing-framework.html) and [Load testing with Locust](/docs/testing-locust.html).
|
||||
Every test workflow has a `make` target, so commands stay short and identical across machines and CI. See also [Test framework and rules](/docs/testing-framework.html) and [Load testing with Locust](/docs/testing-locust.html).
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -26,7 +26,7 @@ python -m playwright install chromium # browser used by the E2E suite
|
||||
| `make coverage-headed` | Coverage run with a visible browser. |
|
||||
| `make coverage-html` | Runs `coverage`, then writes the browsable HTML report to `htmlcov/index.html`. |
|
||||
|
||||
`make test` sets `PLAYWRIGHT_HEADLESS=1`; `make test-headed` sets it to `0`. The whole suite runs in a single process: serial execution is enforced in `pyproject.toml` (`addopts = "--tb=line -p no:xdist"`), so there are no parallel workers and the coverage targets collect everything in one process, exactly how CI runs.
|
||||
`make test` sets `PLAYWRIGHT_HEADLESS=1`; `make test-headed` sets it to `0`. The whole suite runs in a single process: serial execution is enforced in `pyproject.toml` (`addopts = "--tb=line -p no:xdist"`), so there are no parallel workers and the coverage targets collect everything in one process, as CI runs it.
|
||||
|
||||
Tests are split into `tests/unit/`, `tests/api/`, and `tests/e2e/` as a directory tree that mirrors the path (api/e2e by endpoint, for example `tests/e2e/admin/aiusage.py`; unit by source module); see [Test framework and rules](/docs/testing-framework.html) for the naming convention. To run one test instead of the suite, call pytest directly:
|
||||
|
||||
@@ -41,7 +41,7 @@ python -m pytest tests/e2e/feed.py::test_name -v --tb=line -x
|
||||
| `make locust` | Starts a dedicated server and opens the Locust web UI. |
|
||||
| `make locust-headless` | Runs Locust unattended with preset users and run-time, writing an HTML report. |
|
||||
|
||||
Both manage their own server and throwaway database. Tuning variables are documented in [Load testing with Locust](/docs/testing-locust.html).
|
||||
Both manage their own server and throwaway database. Tuning variables are in [Load testing with Locust](/docs/testing-locust.html).
|
||||
|
||||
## Housekeeping
|
||||
|
||||
@@ -51,5 +51,5 @@ make clean # remove __pycache__, *.pyc, the egg-info, and .venv
|
||||
|
||||
## What CI runs
|
||||
|
||||
`.gitea/workflows/test.yaml` mirrors these targets: it installs `".[dev]"` and Chromium, then runs the serial suite under coverage (`coverage run -m pytest tests/`), publishes the coverage HTML, and on failure uploads the screenshots from `/tmp/devplace_test_screenshots/`. Running `make coverage` locally reproduces the CI result before you push.
|
||||
`.gitea/workflows/test.yaml` mirrors these targets: it installs `".[dev]"` and Chromium, runs the serial suite under coverage (`coverage run -m pytest tests/`), publishes the coverage HTML, and on failure uploads the screenshots from `/tmp/devplace_test_screenshots/`. Running `make coverage` locally reproduces the CI result before you push.
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ How DevPlace verifies itself: an end-to-end browser suite, in-process unit tests
|
||||
|
||||
## Three tiers, a directory tree that mirrors the path
|
||||
|
||||
The `tests/` directory holds the correctness suite: 932 tests organised into three category directories by *what each test exercises*. Within each tier the **directory structure mirrors the path** - one segment per directory, the last segment is the file:
|
||||
The `tests/` directory holds the correctness suite: 932 tests in three category directories by *what each test exercises*. Within each tier the **directory structure mirrors the path** - one segment per directory, the last segment is the file:
|
||||
|
||||
- **`tests/api/` - HTTP integration tests.** `requests`/`httpx` drive a real Uvicorn server (`app_server`) and assert on status, JSON, and headers without a browser. **Organised by endpoint path.**
|
||||
- **`tests/e2e/` - end-to-end (Playwright).** A real Chromium browser drives the same Uvicorn server, covering anything a user sees or clicks. **Organised by endpoint path.**
|
||||
@@ -21,7 +21,7 @@ Every directory is a Python package (`__init__.py`). Files are not `test_`-prefi
|
||||
|
||||
## The load layer
|
||||
|
||||
`locustfile.py` is a separate concern: it does not assert correctness, it measures throughput and latency under concurrent traffic. It spins up its own server on its own port against a throwaway database, then floods it with simulated users. Run it from `make locust` (interactive web UI) or `make locust-headless` (CI-style, writes an HTML report). See [Load testing with Locust](/docs/testing-locust.html).
|
||||
`locustfile.py` does not assert correctness; it measures throughput and latency under concurrent traffic. It spins up its own server on its own port against a throwaway database, then floods it with simulated users. Run it from `make locust` (interactive web UI) or `make locust-headless` (CI-style, writes an HTML report). See [Load testing with Locust](/docs/testing-locust.html).
|
||||
|
||||
## Coverage and continuous delivery
|
||||
|
||||
@@ -29,5 +29,5 @@ Every run is measured for coverage (`coverage.py` over the `devplacepy` package,
|
||||
|
||||
## The cardinal rule
|
||||
|
||||
**Every user-facing feature requires an end-to-end Playwright test.** A JSON endpoint or data helper may be covered by an in-process test alone, but anything rendered in the browser must be exercised through the browser. A feature is not complete until its E2E test passes. The framework conventions that make these tests reliable are documented in [Test framework and rules](/docs/testing-framework.html).
|
||||
**Every user-facing feature requires an end-to-end Playwright test.** A JSON endpoint or data helper may be covered by an in-process test alone, but anything rendered in the browser must be exercised through the browser. A feature is not complete until its E2E test passes. The framework conventions behind reliable tests are documented in [Test framework and rules](/docs/testing-framework.html).
|
||||
</div>
|
||||
|
||||
@@ -23,17 +23,18 @@
|
||||
<span class="icon">🔍</span>
|
||||
Search
|
||||
</a>
|
||||
{% set ns = namespace(section=None) %}
|
||||
{% for p in pages %}
|
||||
{% if p.get('section') and p.section != ns.section %}
|
||||
<div class="sidebar-heading sidebar-subheading">{{ p.section }}</div>
|
||||
{% set ns.section = p.section %}
|
||||
{% elif not p.get('section') %}{% set ns.section = None %}{% endif %}
|
||||
<a href="/docs/{{ p.slug }}.html" class="sidebar-link {% if p.get('section') %}sidebar-link-nested{% endif %} {% if p.slug == current %}active{% endif %}">
|
||||
{% for audience, tiers in nav %}
|
||||
<div class="sidebar-heading sidebar-tier">{{ audience }}</div>
|
||||
{% for section, section_pages in tiers %}
|
||||
<div class="sidebar-heading sidebar-subheading">{{ section }}</div>
|
||||
{% for p in section_pages %}
|
||||
<a href="/docs/{{ p.slug }}.html" class="sidebar-link sidebar-link-nested {% if p.slug == current %}active{% endif %}">
|
||||
<span class="icon">📘</span>
|
||||
{{ p.title }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="sidebar-heading docs-download-heading">Download</div>
|
||||
<div class="sidebar-nav">
|
||||
|
||||
Reference in New Issue
Block a user