DeviiTerminal bound [data-devii-open] click listeners only after the
async /devii/session fetch resolved, silently dropping early clicks.
Switch to a single delegated document listener bound in the
constructor, matching the ModalManager/dp-lightbox pattern.
The steal-confirm and comment-vote e2e tests asserted DOM state right
after a click with no wait for the triggering POST to land, racing the
server under CI load. Wrap those clicks in page.expect_response.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A shared-server crash mid-suite previously cascaded into hundreds of
opaque connection-refused errors across every later api/e2e test,
making the real cause invisible. pytest_runtest_setup now polls the
tracked subprocess and, on the first test after it exits, reports the
exit code plus the server's own log tail once instead of forcing every
subsequent test to fail blind.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Test-process reflections of the projects table cached a reduced schema
when they ran before the app server had ALTER-TABLE'd in website_url,
repo_url, cover_attachment_uid, logo_attachment_uid, platforms,
release_date, and demo_date, causing spurious KeyErrors under full-suite
ordering.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR #164 shifted --bg-primary/secondary/card/input/modal and the body
gradient to a purple-toned palette; restore the prior values.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The workspace editor hung for 60s and then 504'd. Three independent faults were
stacked behind that one symptom.
Reachability: editor_target delegated to proxy_target, which returns
CONTAINER_PROXY_HOST plus the published host port and never falls back to the
container. From inside the app container that address crosses docker0 into the
host INPUT chain, whose policy is DROP with an allow-list that does not include
the published port range, so the packet was dropped and the request hung rather
than being refused. Measured from the app container: container_ip:8443 answers
302, gateway:20006 is dropped. One shared reachable_target now prefers the
direct container leg and falls back to the published port, and editor_target
uses tunnel_target as services/containers/CLAUDE.md already required. The same
defect affected /p/{slug} ingress and every tunnel, since all three resolved
through proxy_target.
The recorded measurement that motivated the old order (container_ip times out,
gateway connects) no longer holds: make docker-attach puts the app on the
instances' bridge network, which is what makes the direct leg work.
Duplicate response headers: the forwarding core relayed the upstream Date and
Server alongside the ones the serving layer generates, so every proxied
response carried two of each. Both are singleton headers and duplicating them
is malformed HTTP.
Serialization: WorkspaceViewOut declared flag_reason and three sibling strings
as str, so a NULL column made the workspace page 500 for JSON clients.
Documents the two public hostnames and the devplace.net SSH tunnel, so a future
session does not conclude the site is down after pointing curl --resolve at an
address the hostname does not resolve to, and adds the layered procedure for
diagnosing a production failure.
Verified on production with Playwright over both hostnames: the code-server
login renders and the workbench loads. Suite: 3345 passed.
data/devplace.db is the live database and make dev, make prod and the
Docker stack all share it, so an agent-initiated command that reaches it
is a production incident waiting for a typo. The hazard is invisible in
the command text: the script that prompted this named no path at all, it
imported devplacepy and therefore resolved config.DATA_DIR to the real
file. A path-pattern rule would have sailed straight past it.
The PreToolUse hook reads the script and judges it on content, so one
that points DEVPLACE_DATABASE_URL at a scratch file passes and an
unguarded one does not. It also refuses commands naming the database or a
production data directory, the management CLI, and python -m devplacepy.
The suite, the server targets and the mandated import gate stay free.
permissions.deny additionally refuses Write and Edit anywhere under data,
which the Bash hook cannot see.
The escape hatch is two-factor and cannot be self-served: without
confirmation the command is denied outright rather than prompted, and the
token that downgrades it to a prompt may only be added after the user has
confirmed in their own words. Verified against thirty-five commands, and
the heuristic is narrower than it looks because the repository path
itself contains the package name, so it matches an import statement
rather than the bare word.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An instance kept production-identical for extended manual testing is
otherwise taxed forever by its own safety controls: five consents, a
versioned terms gate on every mutating request, and every account
predating the trust and safety commit reading terms_version NULL because
init_db deliberately never backfills it. AcceptanceService grants each
agreement to each account that has not declined it, so the instance stays
production byte for byte while nobody clicks the same dialog again. It is
opt-in, dry run by default, and one switch per agreement type.
The application is not allowed to know it exists. One registration line
in main.py is the only import anywhere, there is no route, schema,
template, Devii tool or environment flag, and a unit test greps the tree
and fails the suite if a second importer appears. The decline register
needs no storage: the ledger is append-only in effect, the service only
ever grants, so any withdrawn row was written by a human and that pair is
never touched again. No provenance column, nothing to observe.
Satisfaction is the gate's own expression, never a proxy, which is why
the ordering is created_at then id exactly as consent_state selects, and
why the live-account clauses are built with has_column: init_db ensures
terms_version and deletion_requested_at but not is_active, so a hardcoded
reference raises no such column on an instance where nobody was ever
suspended. Every write is one conditional statement decided on the real
rowcount, proven with sixteen processes racing one account to exactly one
ledger row and one audit row. The two existing audit keys carry it, with
actor kind service, because a service that silently mutated consent state
would be the worst possible exception to the append-only rule.
lensfl.md is the source brief accept.md records the design against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
code-server runs authenticateOrigin on every websocket and resolves the
request host as Forwarded, then X-Forwarded-Host, then Host. The forward
core put the public host into additional_headers, but the websockets
client already writes its own Host for the real TCP target and Headers
appends, so the handshake carried two Host lines; Node keeps the first
(the internal gateway:port), the origin check failed, and code-server
answered 403. Because the browser socket was accepted before the upstream
was dialled, that surfaced as a 101 followed by 1011 and the editor died
on "the workbench failed to connect to the server". Dialling first and
carrying the public host in the connect URI fixes both planes.
The two header builders that had drifted apart are now one core, so a
websocket carries the same client and forwarded headers as an HTTP
request. Responses stream instead of buffering whole, which is what makes
a large tunnel download cost constant memory and lets SSE work; byte
accounting moved onto the completion callback. Subprotocols negotiate,
the upstream client is reused across requests, and the path and query are
forwarded byte-exactly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A member whose account has not accepted the terms in force now gets one
dialog on the action they attempted instead of a dead-end refusal. The
client handler is the single TermsGate, wired into every Http POST helper
so the four optimistic controllers cannot swallow the gate into an error
flash, and the original request is replayed once the acceptance is
recorded. Reading the site and deleting an account stay unblocked.
apple.md is the source brief the compliance research documents reference.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
The API key block and the workspace editor password each carried their
own copy of the same monospace, break-all, boxed styling. Both now use a
single secret-value class in base.css, so a new secret surface inherits
the presentation instead of duplicating it.
ARCHITECTURE.md is a structural overview of the application derived from
the source tree, the live schema and the imported app object, linked
from the README.
A new tunnel row was inert at pending until the next WorkspaceService
tick picked it up, so the editor certificate waited up to 30 seconds
before the ACME order even started. Opening a workspace and reaching its
public hostname in that window served the wildcard default certificate
and produced ERR_CERT_COMMON_NAME_INVALID in the browser.
certs.certify now owns the whole transition - provisioning, issue,
active or failed, then the owner notification - and both callers use it.
provision.ensure schedules it for the editor tunnel the moment the
workspace exists, claiming the row synchronously so the tick can never
issue the same host twice and burn a duplicate-certificate slot. The
service loop keeps calling certify per pending row as the safety net for
user-created tunnels and for rows created while molohttp was
unconfigured.
Issuance is a loop task rather than an await: an order takes about ten
seconds and certs.issue allows up to 180, which would hang the workspace
POST. The task is held until it completes so it cannot be collected mid
flight.
resume also restarts the idle clock, and the workspace routes gain
HTTP-level tests covering every refusal path they had none for.
json_error takes (status_code, message). Every refusal in the two
workspace routers passed them the other way round, which builds a
JSONResponse with a string status and raises TypeError inside
Response.init_headers before a byte is written. The branch meant to
explain a limit to the user returned 500 instead.
A member already holding the default two workspaces therefore got a 500
from the workspace page's Open workspace button rather than the quota
message. Twelve call sites corrected across both routers.
workspace_open also let ContainerError escape as a second 500 on the
same button when the ppy image is not built; it now returns 400 through
the shared fail helper.
tests/unit/responses.py AST-scans the whole package for both argument
orders, so the swap cannot reappear anywhere.
The workspace feature shipped its routes, agent tools and docs, but the
editor was never reachable: the project page had no entry point, the ppy
image had no code-server binary, no certificate was ever requested for a
tunnel, and both nginx and the proxy dropped what the editor needs.
- Add a VS Code button to the project action row and a Workspace item to
the overflow menu, gated by can_open_workspace plus a running instance
(viewer_can_workspace and workspace_editor_url on ProjectDetailOut).
- Install a pinned code-server in ppy.Dockerfile before USER pravda and
assert it in the build smoke test, so an image that cannot run the
editor no longer builds green.
- Run the editor with --auth password and a per workspace 8 character
pronounceable secret, minted once at the ensure_editor_password choke
point and injected as PASSWORD. Keep it off WorkspaceViewOut, which the
admin listing shares.
- Publish the editor tunnel when a workspace is created and issue its
certificate from a new WorkspaceService phase against the molohttp admin
API, then notify the owner with the live URL and the password. Only
pending rows are retried, so a broken host cannot burn the ACME failure
rate limit. Renewal stays molohttp's job.
- Forward the original Host on proxied requests and the client cookie on
proxied websockets, so code-server scopes its session cookie to the
public hostname and authenticates the workbench socket.
- Recreate a container stuck in the created state instead of retrying
docker start forever against an image it can no longer run.
- Return a JSON string from WorkspaceController.dispatch; raw dicts landed
in a tool message and aborted the turn at the model endpoint.
- Let the nginx catch-all carry websocket upgrades, keeping upstream
keepalive, so tunnelled apps and the editor both connect.
Online status had three server-side candidate populations and two
client-side deciders, so the feed roster and the /messages indicators
could legitimately disagree.
PresenceRelayService built its online set from whichever topics happened
to be subscribed on a given tick: the roster candidates on /feed, only
the per-uid dot rows on /messages. Different populations meant a
different hysteresis baseline, so the same user could be online in one
place and offline in the other. On top of that, PresenceManager re-derived
online status client-side from a frozen data-presence-last-seen with a
strict timeout and no hysteresis, re-evaluated every 20s, so any element
whose relay frame was missed drifted grey after the timeout and stayed
there. AppChat carried a third renderer with its own PubSubClient that
only ever wrote online/offline, plus hand-built dot markup duplicating
_presence_dot.html.
The relay now collapses to one set on one topic. Each tick it reads the
online population in a single indexed query (online_candidates, capped by
the new PRESENCE_TRACK_LIMIT), applies hysteresis once, and publishes
{count, online, users} on public.presence.roster only when the uid set
changes. online is the authority for every avatar dot; users is the same
set trimmed to PRESENCE_ONLINE_LIMIT for the feed panel. The per-uid
public.presence.{uid} topics are gone, which also removes one
subscription per distinct author on a page.
is_online(user) is now stays_online(seconds_since(last_seen), False), so
the server-rendered initial state and the live set apply one formula.
PresenceManager makes one subscription and renders every
[data-presence-uid] element as membership of that set, with no clock and
no expiry timer; before the first frame the server-rendered state stands.
Relative "last seen" text is a <time data-dt data-dt-mode="ago"> handled
by the shared LocalTime. AppChat lost its presence code entirely, and the
new Avatar.badgeElement is the JS twin of _presence_dot.html, so dot
markup now lives in exactly two places.
Also fix the awards column ensure-block, which the awards tests exposed.
backfill_api_keys opened with an "if users not in db.tables" guard, but
on a brand-new database that is precisely the state at init_db time, so
the whole users ensure-block was skipped. The first signup then created
users with only the columns of that INSERT, leaving every ensured-but-
unwritten column absent from the server's reflected metadata for the rest
of the process lifetime. That is why the awards tab, the prominent award
banner and the avatar award badge were invisible on a fresh database. It
now calls get_table("users") unconditionally.
test_avatar_badge_on_feed_when_prominent asserted that any online user
carries an award badge while only awarding a user who was never active,
so it passed only by accident. It now makes the awarded user active and
asserts the badge on that user's roster entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pillow 12 renames Image.getdata to get_flattened_data; the award image
normaliser and the isslop hue histogram both read pixels that way. The image
stack also needs pango, harfbuzz, fontconfig and a base font in the container,
so text rendering has glyphs to work with.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README covers the provider model, the two providers and their transports, the
admin configuration surface at /admin/services/push, the delivery loop and the
updated file map. events.md records that push.subscribe and push.update now
carry the provider in their metadata, with endpoint_host set only for
endpoint-based providers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Unit tests for the provider registry, both providers' registration parsing, the
APNs payload translation, provider token signing and caching, header and status
mapping against a mock transport, provider grouping and the delivery timeout
clamp, plus service tests for the configuration surface, the retention sweep and
the per-provider metrics. Api tests cover the provider listing on GET
/push.json, registration with and without an explicit provider, idempotency and
the rejection of an unknown or unconfigured provider.
Provider settings in unit tests are supplied by monkeypatching the provider's
setting reader rather than writing site_settings, because the unit tier shares
its database with the running api-tier server.
devplacepy/push/CLAUDE.md documents the protocol, how to add a provider, the
invariants and the APNs specifics; the root, routers and services files point at
it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Split the push delivery library into a provider architecture. devplacepy/push
becomes a package: a PushProvider protocol with a registry, the existing Web
Push implementation moved unchanged behind it, a new APNs provider, a store
owning every push_registration access, and a delivery loop that groups a user's
subscriptions by provider, prepares each provider's payload once and sends over
a single shared client.
APNs delivers over HTTP/2 with an ES256 provider token cached per credential
fingerprint, so a worker signs at most one token per 45 minutes. Registrations
carry a hexadecimal device token; 410 and the Unregistered class of reasons soft
delete the subscription exactly like a gone Web Push endpoint.
All provider configuration is edited at /admin/services/push through the same
ConfigField surface every other subsystem uses, assembled from the registry so a
future provider needs no edit to the service. A provider that is disabled,
unconfigured or holding an unusable credential accepts no registrations and is
skipped during delivery, never failing the other providers.
POST /push.json accepts a registration for any active provider; a body without a
provider field is a Web Push body, so existing clients are unchanged. GET
/push.json keeps publicKey at the top level and adds the active providers.
push_registration gains provider and token columns, ensured in init_db with a
converging backfill; existing rows are never rewritten.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.
Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.
Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.
Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.
Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.
Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.
Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.
Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.
Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.
Full suite: 2989 passed, 1 skipped.
The creation and run quotas were checked and then acted on, so two concurrent
create_task calls or two schedulers could both pass the check and overshoot the
limit. Both are now a single conditional INSERT decided on the driver rowcount:
reserve_run takes a run slot after the claim and releases the claim by deferring
when the quota is spent, and insert_task_within_quota does the same for the task
row itself. Racing twelve and sixteen processes now yields exactly the limit.
The atomic insert names its columns, and dataset skips a None valued key when it
creates a table lazily, so the store declares the full task column set up front.
Both the column and index ensures now tolerate a concurrent duplicate, since
several processes build a store at once and SQLite DDL is not idempotent.
Adds the quota, task-run context, guard, store and scheduler test suites, and
documents the chokepoints and the unhackable task-run flag.
The primary administrator was the earliest Admin by created_at with no further
condition, so a soft-deleted or deactivated account could hold the role and then
be refused by the api-key path, leaving nobody able to use the database API, the
backup download or cross-owner container management. Rows with no recorded signup
time also sorted ahead of every real account. Scan the earliest admins instead and
take the first that is neither deleted nor deactivated, with missing timestamps
sorted last.
Also stop the projects listing returning 500 when project_type or description is
NULL (the dict default never applies to an existing NULL column), align the issue
test fixture with its unit twin so a combined run cannot collide on a fixed uid,
read the settings value from the database rather than a stale per-process cache,
and give the seeded and fixture admins the is_active and created_at fields that
every real signup writes.
The degradation guard in crawl() wrapped only chromium.launch(), while
the driver start sat outside it in the async context manager. A failure
to start the driver therefore propagated out of crawl() and failed the
whole DeepSearch job, contradicting the warning it logs on that path
("pages will use httpx only").
Start the driver inside the guarded block and stop it in the finally, so
an unavailable driver degrades to httpx-only fetching as intended. The
crawl loop body is unchanged apart from indentation.
.editorconfig fixes indentation and line endings for every editor. The
scripts/ helpers (database import checks, the monolith guard, and the two
refactor migrations) were only ever local.
The suite ran with -x, so a run stopped at the first failure and finding N
failures cost N full runs. Move -rf into the pytest addopts so every run
lists each failure, and add the triage targets test-fast (unit + api, no
browser), test-failed (--last-failed), test-first-failure (the old -x),
test-slowest and test-cache-clean. A stale .pytest_cache holding node ids
from deleted files made --last-failed select everything; make clean and
test-cache-clean drop it.
Fix the fifteen failures this surfaced.
DeepSearch crawling raised AttributeError in its finally block on every run:
async_playwright().__aenter__() returns a Playwright, which has no __aexit__.
Use start()/stop() at both call sites.
Update the tests left behind by changed signatures: VectorStore is async now,
fetch_page takes a browser, _summary_payload takes dom_evidence, and the
messages/notifications page compounds take a user_uid.
Close four real flakes that fail-fast had been hiding, all of them late in
the run. Harvest assertions pinned crop.reward_coins while is_golden pays
five times on about five percent of harvests, so they now assert through
realizable_harvest_coins with the observed golden flag. Market saturation
fixtures assumed a single active farm and landed two tiers milder once the
api and e2e tiers had created farms, so they scale by active_farms(). The
primary-administrator container test raced the one second cross-worker cache
version window and now waits for the server to agree. The isslop tools test
matched the collapsed nav dropdown link instead of the tools grid card.
Stop burning ninety seconds waiting out server-side display caches:
DEVPLACE_RANKING_TTL and DEVPLACE_MARKET_SATURATION_TTL follow the existing
sitemap and home cache precedent and are zero for the suite, taking the
leaderboard test from 60.6s to 4.4s and the saturation test from 30.1s to
under a second.
2881 passed, 1 skipped in 15:13.
Document the current Code Farm mechanics across the docs: raids and
steal windows, prestige and refactor, defense upkeep and downgrade,
infrastructure and cosmetics, the community treasury and weekly grant,
market saturation, mastery, and the Era boards - in README.md, the root
and routers CLAUDE.md, and the game service CLAUDE.md.
Complete the read and update faces of the signed-in user's attachment
management over the existing attachments table:
- GET /uploads: paginated list of the user's own attachments, newest
first, with an optional linked/orphaned filter
- GET /uploads/{uid}: fetch one attachment (owner or admin)
- PATCH /uploads/{uid}: rename the display filename, always preserving
the original extension (owner or admin, audited as attachment.rename)
Adds get_user_attachments/get_user_attachment data helpers, the
rename_attachment operation, AttachmentRenameForm, the UploadItemOut and
UploadsListOut schemas, the Devii tools list_attachments/get_attachment/
rename_attachment, expanded API reference documentation for the full
lifecycle including delete, and api-tier tests.
- Policy: every change ends with make test (all tiers, all tests) green; docs and agent guardrails updated accordingly
- schema.py: ensure the full filtered/indexed column set of instances via get_table (ingress_slug, ports_json, container_gateway, slug, status, ...) so a partial first insert can never break the ingress proxy
- docs_api: award body param location body -> json; gateway endpoints documented public -> user to match enforced auth
- tests: missing get_table import (trash restore), audit read as admin (award), deterministic online-roster and leaderboard-cache handling, container visibility updated to the primary-admin-only rule, scoped AI usage heading selector past the hidden Tools nav links
Every purchase/upgrade path (new and pre-existing) is now race-safe against concurrent requests via atomic conditional SQL updates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add `cmd_isslop_prune`, `cmd_isslop_clear`, `cmd_isslop_analyze` CLI commands for AI usage analysis job management
- Register `/game` router with `index` and `farm` endpoints for Code Farm idle game
- Add `politics` to allowed TOPICS constant replacing `signals`
- Introduce `ISSLOP_DIR`, `ISSLOP_WORKSPACES_DIR`, `ISSLOP_RUNS_DIR`, `ISSLOP_MEDIA_DIR` config paths
- Add `clear_user_stars` and `clear_user_projects_cache` calls on vote and project create/delete
- Update `make prod` to use `nproc` workers via `DEVPLACE_WEB_WORKERS` env var
- Convert `database.py` and `utils.py` to packages for modular structure
- Add `devplace apikey` and `devplace token` CLI subcommands for API key and access token management
Introduce a new `_sanitize_mentions` method in `BotHelpersMixin` that deduplicates and removes self-mentions from text, using a compiled regex for `@handle` patterns. Apply this sanitizer before the 2000-character truncation in both `engage.py` comment posting and `social.py` reply posting to prevent duplicate or self-referential mentions from being cut off mid-handle. Additionally, fix profile URL parsing in `social.py` to strip trailing path segments, and refine `LLMClient.clean` to handle asterisks and underscores more precisely without breaking adjacent alphanumeric characters.
Add `last_seen` column to users table with index, implement `set_last_seen` and `get_online_users` database functions, expose presence config env vars (`PRESENCE_TIMEOUT_SECONDS`, `PRESENCE_ONLINE_LIMIT`, `PRESENCE_ONLINE_MARGIN_SECONDS`), include `last_seen` in follow list responses, and update profile docs to mention online indicator.
Add `url` field to profile activity items so post cards link to `/posts/{slug}` and comment cards link to the parent post with a `#comment-{uid}` anchor, matching notification click behavior. Include the shared `_card_link.html` overlay in the template, fix XP bar rendering for missing `xp` key, and add API + e2e tests verifying the link structure and navigation.
Consolidate scattered `X-Real-IP` / `request.client.host` fallback logic into the single `client_ip()` helper from `utils`, reducing duplication in the rate limiter middleware, project file zip routes, tools shared module, and audit record builder. Also refactor three admin endpoints (`/ai-usage/data`, `/analytics`, `/users/{uid}/ai-usage`) to use the existing `require_admin()` guard instead of repeating manual auth checks, and remove now-unused `get_current_user`/`is_admin` imports from those modules.
- Add GET /profile route returning own profile page with tab support, backed by new my_profile_page handler in profile/index.py and documented in docs_api.py
- Introduce _clean_breadcrumbs helper in seo.py that strips markdown from breadcrumb names before passing to schema generation
- Skip data-confirm modal for elements with data-auto-submit attribute in ModalManager.js
- Guard PushManager subscription against missing userUid and capture it from document body dataset
- Switch profile bio template from render_title to render_content and use div instead of span for proper block rendering
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
Add comprehensive test coverage for seven new API areas:
- Backup schedules CRUD operations in admin panel
- Service configuration saving and retrieval for news AI model
- Token authentication with email/username and JSON body support
- Bookmarking for project and news targets with invalid type validation
- Content permissions testing with member and admin session fixtures
- Devii adopt endpoint redirect behavior and session cookie handling
- Devrant authentication flow with user registration and token retrieval
Add `_alt_from_url` helper in Python rendering and `altFromUrl` in JS ContentRenderer to generate descriptive alt attributes from image filenames, replacing empty alt strings. Update image embed templates and Avatar component to use derived alt text. Migrate inline `style.width` assignments to CSS custom properties (`--poll-pct`, `--hud-xp`, `--quest-pct`, `--bar-pct`, `--quota-pct`) across PollManager, GameFarm, and profile/quest CSS for better maintainability. Introduce new admin, docs, projects, services, and base CSS utility classes. Add "Code Farm" docs page entry.
Introduce a new `content_preview()` function in `rendering.py` that strips HTML from rendered content and truncates to a given length with an ellipsis. Add a corresponding `plainText()` and `preview()` method to the `ContentRenderer` JS class. Register `content_preview` as a Jinja2 global in `templating.py`. Update multiple templates (`feed.html`, `gists.html`, `landing.html`, `messages.html`, `news.html`, `notifications.html`, `post.html`, `profile.html`) to use `render_title`, `render_content`, or `content_preview` instead of raw string slicing, ensuring consistent rendering of rich text (e.g., Markdown, HTML) in previews, descriptions, bios, and notification messages. Also fix the conversation preview in `MessagesLayout.js` to use the new JS preview method.
Extend the notification system with a third channel (telegram) alongside existing in_app and push channels. Add `telegram_enabled` column to `notification_preferences` table, update `NOTIFICATION_CHANNELS` and `_NOTIFICATION_CHANNEL_COLUMNS` mappings, and set telegram default to off (`_NOTIFICATION_CHANNEL_DEFAULTS`). Create `telegram_outbox` table with columns for uid, user_uid, chat_id, text, status, attempts, created_at, and sent_at, plus an index on status/id for efficient polling. Register `TelegramOutboxService` in the service manager lifecycle. Update API documentation strings to describe the new channel and its pairing requirement. Extend admin notification defaults view to include telegram column. Pass `notif_telegram_paired` flag to profile template based on `telegram_store.is_paired()` check. Update `NotificationPrefForm` and `NotificationDefaultForm` model literals to accept "telegram" as a valid channel value.
Move Leaderboard link into a right-aligned icon-only button group alongside Tools dropdown, converting both from text+caret to compact icon buttons. Adjust dropdown menu alignment from left to right and remove redundant caret styling.
Implement a Farmville-style cooperative idle game mounted at `/game` with member-only play and public farm viewing. The data layer is pure and timestamp-driven with no background tick: plot states are derived from `ready_at` vs current time, never stored. Key features include plantable software projects (shell script through kernel) that build over real time, harvest for coins and XP, CI tier upgrades for faster builds, plot purchasing with doubling cost, watering mechanics for friends' builds, daily quests, and prestige system. All game endpoints negotiate HTML or JSON and return full farm state for single-request client refresh. Pub/sub notifications broadcast farm updates on `public.game.farm.{username}` topics. Database schema adds `game_farms`, `game_plots`, and `game_quests` tables with appropriate indexes.
Add `comment-reply-to` span showing parent author with ↳ indicator in comment headers, and introduce `.comment-replies-flat` dashed border style for deeply nested threads. Adjust responsive spacing: reduce comment gap and vote min-width on mobile, hide reply-to indicator on small screens, and increase reply padding for better readability.
- Fix audit event names in CLI prune/clear commands from `cli.seo.*` to `cli.seo_meta.*`
- Refactor `_delete_attachment_file` to accept full attachment dict instead of storage_path string, using directory and stored_name fields with ATTACHMENTS_DIR
- Add `safe_next` validation for referer header in validation error redirect and media redirect
- Add `is_active` check in login router to reject deactivated accounts with "Account is deactivated" error
- Replace raw `request.headers.get("Referer")` with `redirect_back()` utility in bookmarks, polls, reactions, and votes routers
- Move `mark_conversation_read` call from `get_conversation_messages` to `messages_page` to avoid side effects during message retrieval
- Fix poll audit link to use `option.get("label")` instead of `option.get("text")`
- Add `VOTABLE` set validation in votes router to reject invalid target types with 400 response
- Strip control characters (0x00-0x20) from URLs in `_safe_url` instead of simple strip
- Add `__getattr__` fallback in services `__init__.py` for dynamic attribute access
- Invalidate `_hot_settings_value` and `_hot_settings_at` in `_patch_limits` so stale
cached limits from prior tests do not persist after monkey-patching `get_int_setting`.
- Add a polling retry loop (up to 5 seconds) in `test_maintenance_mode_blocks_guests`
to handle eventual consistency of the maintenance-mode flag propagation.
- Replace brittle hardcoded `@bob_test` assertion in mention test with dynamic
`data-username` attribute extraction from the active dropdown item.
- Replace `asyncio.run` calls in bot unit tests with shared `run_async` helper from
`tests.conftest` to avoid event-loop conflicts in the test suite.
- Change `insert` to `upsert` with `["uid"]` conflict target in Gitea test fixture
to prevent duplicate-key errors on repeated test runs.
- Replace `asyncio.new_event_loop().run_until_complete` in Telegram worker tests
with the same shared `run_async` helper for consistency.
Extend `database.text_search_clause` with an `author_field` parameter that resolves username matches to user UIDs, enabling author-username search across the three public listings (`/feed`, `/gists`, `/projects`). Update the corresponding API docs and README route descriptions to reflect the new search scope. Add six partial indexes (`idx_comments_target_live`, `idx_votes_target_live`, `idx_reactions_target_live`, `idx_gists_live_created`, `idx_projects_live_created`, `idx_attachments_user_created_live`) to optimize filtered queries on non-deleted rows. Introduce a hot-settings cache (`_hot_settings`) with a 2-second TTL for `maintenance_mode`, `rate_limit_per_minute`, and `rate_limit_window_seconds`, plus a periodic rate-limit store sweep (`_sweep_rate_limit_store`) to evict stale IP entries every 60 seconds. Add `GZipMiddleware` to the FastAPI app for response compression.
Implement a new `SeoMetaService` subservice that generates clean SEO title/description/keywords for published content items, distinct from the existing SEO diagnostics auditor. Add `seo_metadata` polymorphic table with soft-delete support, batch query methods, and usage tracking. Extend the CLI with `seo-meta prune` and `seo-meta clear` commands for job row lifecycle management. Wire `schedule_seo_meta_for_table` into content creation and editing flows in `content.py`. Document the new service in `AGENTS.md` and `README.md`, including the `extra_head` site setting for custom `<head>` injection.
Remove the `.planning-header` flex container and `.planning-status` spinner block, add a global `[hidden]` display rule, increase `.planning-ticket-list` max-height to 26rem, and convert ticket labels from inline flex to a CSS grid with tabular-nums on the count.
Add a `numbers` form field to the planning report endpoint, allowing
admins to generate a report for a specific subset of open tickets
instead of always planning all open tickets. The selected numbers are
parsed from a comma-separated string, deduplicated, and passed to the
background job. The job's scope is recorded in the audit log and the
Devii tool description is updated to document the new parameter.
Add `issue_usage` table with columns for user_uid, tokens, cost, and latency metrics, including a unique index on user_uid. Wire `accumulate_usage` into `enhance_ticket` and `generate_plan` to capture per-request AI usage, persist totals via `add_issue_usage` in both `IssueCreateService` and `PlanningReportService`, and expose aggregated usage as metric cards through `IssueTrackerService.collect_metrics`. Update planning API docs summary to reflect the new phased implementation document format.
Implement full attachment CRUD for issues and comments, restricted to open issues only. Attachments are stored locally and mirrored to the Gitea tracker via new `mirror_attachment_to_gitea` and `set_gitea_asset_id` functions. Add `gitea_asset_id` column to the attachments table, extend `IssueForm` and `IssueCommentForm` with `attachment_uids` field, and expose new API endpoints for listing, adding, and deleting issue attachments. Update agent configuration to include web research tools, and document the new capability in README, AGENTS.md, and CLAUDE.md.
Introduce a new Claude agent command file at `.claude/commands/aria.md` that defines an accessibility specialist persona with comprehensive WCAG 2.2 AA+ auditing capabilities. The command includes detailed project rules for semantic HTML, ARIA attributes, keyboard accessibility, dynamic content handling, and landmark regions, along with a structured workflow for codebase analysis and section-by-section upgrades.
Introduce a new changelog file documenting feature releases, bug fixes, and infrastructure changes across multiple dates, including user relations, news service, backup management, and gateway admin UI.
- Add `/block`, `/mute` endpoints with block/unblock and mute/unmute functionality in `routers/relations.py`, hiding blocked users' content everywhere except their own profile while muting only suppresses notifications
- Introduce `devplace emoji-sync` CLI command to regenerate `static/js/emoji-shortcodes.js` from the emoji library, documented in `CLAUDE.md` and wired in `cli.py`
- Create `get_blocked_uids()` database helper and apply it in `content.py` `load_detail()` to filter blocked users' posts from detail views
- Implement `_uid_index()` and `_drop_index()` helpers in `database.py` for unique uid indexes across tables, with `user_relations` added to `SOFT_DELETE_TABLES`
- Document new routes in `AGENTS.md` and `README.md`, including emoji shortcodes rendering behavior distinct from the emoji picker
Add AI-powered body reformatting for valid articles in the news pipeline, converting raw text walls into clean Markdown with paragraphs, headings, and lists. Introduce `news_usage` database table and `add_news_usage`/`get_news_usage` helpers to track per-cycle gateway costs (calls, tokens, latency, USD) from response headers, reported on the admin Services page. Remove unused `.sidebar-more` CSS and apply `render_title()` to poll question/label fields.
- Add comprehensive documentation for backend content rendering in AGENTS.md, detailing the new `render_content` and `render_title` Jinja globals built on mistune with media processing, emoji shortcodes, and XSS protection
- Document the `X-Response-Time` header and bottom-left render time indicator in README.md
- Update bot token pricing documentation to clarify fallback vs gateway cost headers
- Add `email_accounts` to soft-delete tables and `idx_users_role` composite index in database schema
- Implement `telegram_pairings` and `telegram_links` table creation with column migration and indexes
- Add `/profile/{username}/telegram` endpoint to docs API with request/unpair actions
- Register `TelegramService` in main.py lifespan and add `response_timing` middleware emitting `X-Response-Time` header
- Introduce `TelegramPairForm` model and `guard_public_host_sync` synchronous host validation function
Add DevPlace agent configuration with dynamic OpenAPI schema fetching, implement access token issuance/resolution/revocation with configurable expiry, and create generic FastAPI dependency for JSON or form-encoded data validation against Pydantic models. Include comprehensive unit tests for form-data parsing and token lifecycle operations.