AI gateway:
- Add a generic, admin-selectable `client_profile` field on gateway_providers
(e.g. "opencode") so a provider needing special request headers (OpenCode
Zen's client-identity spoofing) is configured like any other provider, not
hardcoded by name.
- Track per-(provider, model) reliability/speed/latency health in memory,
seeded from the existing gateway_usage_ledger at startup - purely
observational, never influences routing.
- New Stats tab on /admin/gateway: request volume, latency, per-model
breakdowns, and reliability weight, charted with a vendored Chart.js and
devplace's own theme tokens.
- Record which model a failed request actually fell back to
(fallback_used_route), surfaced in the Recent Failures table.
- Stop excluding context_length errors from fallback, and skip a primary
attempt outright when its known context window is already too small for
the estimated request size, going straight to the fallback.
- gateway_usage_ledger's provider/fallback_used_route columns and indexes
are ensured centrally in database/schema.py's init_db(), the single point
of truth for this table's schema.
- Non-OpenAI upstream routing and client-model passthrough; trust only the
upstream's own X-Gateway-Model header for served-model attribution.
Devii agent:
- Fix a real lockup: plan/verify tools could be individually disabled via
the admin tool toggles while still being required by the protocol gate,
permanently bricking any task that needed tools. They can no longer be
disabled, and the gate now also checks the tool is actually offered.
- Fix compaction being silently calibrated for a 1M-token model while
running a much smaller one: context budget is now percentage-based and
the summarizer's own request is sized to fit the real model.
- Give a specific, actionable retry message when plan()'s own arguments get
cut off by the output limit, and tighten its schema to discourage
overlong plans.
Other:
- Backup service: offload completed backups to a remote Hetzner Storage Box.
- Container manager: fix orphan blob leaks from sync races, add a two-phase
plan/execute `system prune` CLI command.
- Admin gateway UI: replace the JS-rendered model/provider tables with
server-rendered forms and pages.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhmEkvutuwtzFVcLbTrhdo
The previous fix (d88b3cea) gave every sticky sidebar/rail a
viewport-derived max-height + internal overflow-y:auto so a tall
column's content couldn't get skipped during a fast sticky unstick.
That solved reachability but broke something more visible: on any
page where a side column is naturally SHORTER than the page's tallest
column (the common case - main content usually exceeds the sidebars),
the row/page still grows to the tallest column's height, but the
capped side column's own box now stops at its cap and never continues,
leaving a large empty gap below it for the remainder of the row. On
the reported post page this cut "Related Discussions"/"Projects from"
off mid-list with dead space underneath, i.e. the site was no longer
"at least as long as its longest section" in the sense that mattered:
a shorter section's own rendered box stopped short of the room the
page actually had for it.
Verified empirically (not just by reasoning about sticky/grid
semantics, which are easy to get wrong) that plain `position: sticky`
with no cap already guarantees every part of a column becomes visible
at some point during ordinary page scroll, for any pair of column
heights on a page whose main content is bounded (post, profile,
leaderboard, docs, quiz scoreboard) - the browser's native stuck ->
released transition is smooth and passes the element's full content
through the viewport once its own natural height's worth of scroll
distance has elapsed, regardless of how much taller the other columns
are. Removed max-height/overflow-y/overscroll-behavior/scrollbar-
hiding from .sidebar-card, .feed-right, .leaderboard-page > aside,
.post-page-sidebar, .profile-sidebar, and .dashboard-sidebar, back to
plain sticky; simplified the nested-.sidebar-card overrides (needed
only to stop double-nested stickiness) to just `position: static`.
.ds-chat-log and .pf-tree keep their scrollbar-hiding - those are
genuinely bounded, self-contained widget panes (not page-scroll sticky
columns) and were never part of this problem.
Verified on the exact reported URL (devplace.net/posts/
607ebbd19b95-...): document height now matches the tallest column
(1494px, driven by the 1265px main column), every one of the 11
related-title entries across all three sidebar sections becomes
visible during a full incremental scroll, and the full-page screenshot
shows no truncation or dead space. Also verified full reachability
(every item seen, nothing capped) on profile, leaderboard, docs nav,
and the quiz scoreboard.
Known residual trade-off, not fixed here: on a page whose main column
is effectively unbounded (the feed's post listing), a side column
taller than one viewport only fully reveals its tail in the scroll
range immediately before the very end of that (very long) page, which
real users rarely reach. Fixing that would need an actual
independent-scroll-pane architecture (JS-driven), not a CSS-only
sticky pattern, and reintroduces the same capped/dead-space trade-off
this commit just removed - flagging it rather than silently
overclaiming full coverage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vnp7vzE4hvsytMo5YjszJm
.related-title/.related-meta (the "Gists from X"/"Projects from X"/
"Related Discussions" preview lines in the post-page sidebar) had no
overflow protection at all - no overflow, word-break, text-overflow,
or min-width. They're plain-text previews from content_preview(), not
run through the render_content markdown pipeline that normal post/
comment bodies get (which already has word-break: break-word) - so
this bug was unique to these sidebar previews. Combined with
.related-link being a flex-direction: column container with
align-items: flex-start, each line sizes to its own content width
instead of the card's, so one long unbroken token (a URL) runs past
the card edge with nothing to stop it.
Reported case (a project description ending in a bare GitHub URL)
happens to fit by half a pixel in one exact browser/width/font
combination, which is why it doesn't always reproduce - proved the
underlying CSS is still unsafe by reproducing the real content in an
isolated harness against the actual CSS and forcing a worst-case
unbroken string: it overflowed the card by 72px.
Added min-width: 0 to .related-link and single-line ellipsis
truncation (max-width: 100%, overflow: hidden, text-overflow:
ellipsis, white-space: nowrap) to both .related-title and
.related-meta - the same pattern already used correctly by the
messages conversation list. Checked every other consumer of
content_preview() and every visually similar flex-column card
sitewide; this was the only remaining gap.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vnp7vzE4hvsytMo5YjszJm
Every sidebar/rail column across the app (feed's left filter panel and
right rail, post detail's left/right rails, profile's sidebar, the
leaderboard's two rails, the dashboard sidebar, and the DeepSearch
citations pane) is `position: sticky` with no max-height or overflow
of its own. When a column's content is taller than the viewport, it
doesn't scroll with the page while pinned - it renders past the
bottom of the screen, unreachable until it "un-sticks" and flies past
during a fast page scroll. Confirmed live: the docs nav sidebar alone
is ~4300px of links against a ~900px viewport.
Each sticky column now gets a max-height matched to its own top
offset, `overflow-y: auto`, `overscroll-behavior: contain`, and a
hidden scrollbar (scrollbar-width/-ms-overflow-style/::-webkit-
scrollbar), the same invisible-scroll recipe already used by the
messages page. Purely additive CSS - no template changes, since every
target already had a distinguishing class.
Fixing the shared `.sidebar-card` component in sidebar.css covers
feed, admin, docs, gists, projects, battles, quizzes, and every
/tools/* page in one place. Also caught and fixed a latent bug along
the way: post.css already neutralized nested `.sidebar-card`s inside
`.post-page-sidebar` (position: static) to stop them independently
double-sticking, but the identical setup on the quiz page
(`.feed-right` wrapping the scoreboard's two `.sidebar-card`s) never
got that override - added the matching `.feed-right .sidebar-card`
rule.
Verified live with Playwright at a squeezed viewport height on feed,
post, profile, leaderboard, docs, and the quiz scoreboard: every
column's scrollTop reaches its true end independently, the center
column never moves, and no scrollbar is visible in any screenshot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vnp7vzE4hvsytMo5YjszJm
Two independent contrast bugs, both from a token being used somewhere
it was never validated as a text color:
- Links/inline code used --accent as their text color against dark
card surfaces, measuring 2.8-3.6:1 (below the 4.5:1 AA threshold for
normal text) depending on the surface. Worst case: the sender's own
DM chat bubble uses --accent as its *background*, so a link inside
your own message was the exact same color as the bubble - 1.0:1,
functionally invisible. Added --link/--link-hover (same hue/
saturation as --accent, lightened until they clear 4.5:1 against the
darkest realistic surface) and repointed the sitewide `a`/`a:hover`
default, `.rendered-content a`/`code`, `.docs-chat-bubble a`, and
`.ds-chat-citations a`. The DM chat bubble additionally needed
bubble-scoped overrides (link, AI-adjusted note, attachment-pending
placeholder, failed-send outline/retry-hint) since its background
*is* --accent, which --link alone doesn't fix.
- `background: var(--warning); color: var(--white)` (chat's connection
banner, a container badge, a service-status badge, and docs' PUT/4xx
badges) measured ~1.7:1 - yellow needs dark text. Added --on-warning
mirroring the existing --on-accent pattern and repointed just the
warning-backed variants, leaving sibling info/success/danger badges
untouched (they have the same underlying issue but are out of scope
for this pass).
--accent itself is untouched, so every button/badge/nav element using
it as a solid fill is unchanged. Verified with computed WCAG contrast
ratios and Playwright screenshots against a live render of the real
CSS, plus the production homepage to confirm no regression to brand
colors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vnp7vzE4hvsytMo5YjszJm
BaseService logged "Next run in Ns" at INFO on every single execution
of every service, including the 1-2s-interval pub/sub bridges
(presence/notification/live-view relays), drowning the console in
routine noise while actual failures (run_once errors, loop errors,
on_disable errors) were logged at the same INFO level and got lost in
it. log() now takes an optional level (default INFO, unchanged for
existing callers); the routine tick line drops to DEBUG and the three
failure paths move to WARNING. Added DEVPLACE_LOG_LEVEL (default INFO)
so DEBUG-level ticks stay available on demand without a code change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vnp7vzE4hvsytMo5YjszJm
Restructures the disk-cleanup command to show location, current on-disk
size, and an estimated reclaim per area (attachments, project files,
container workspaces) before touching anything - --dry-run stops there.
A real run re-executes each check, reports actual items/bytes freed,
an "After" size per area, and a final total with elapsed time, flagging
any drift between the estimate and execution passes (e.g. the live
server wrote something in between).
store.gc_workspaces() now reports bytes freed alongside the removed
count (previously count-only), so the containers gc-workspaces CLI
action picks up the same reporting for free.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5tFWkm3UbcstcbPKFE5gP
Happy 404: an HTML 404 (unmatched route, or an explicit not-found inside a
real route) now renders a random existing post instead of the error page,
using the exact same context builder as a real post view. Toggle is the
happy_404_enabled site setting (default on, /admin/settings); JSON/API
requests and a handful of excluded prefixes are never affected. The pool of
candidate slugs is cached in-process and resampled periodically so it stays
fast and eventually cycles the whole posts table; on any internal failure it
falls straight through to the real 404 page.
Applying this everywhere surfaced ~60 existing tests that asserted a literal
404 for a legitimate resource-not-found flow (deleted post, unknown
container, wrong project slug, etc.) - each now disables the setting for the
duration of that specific check and restores it after, so the underlying
not-found behavior stays covered independently of the new feature.
Post page also gained, all built on the same shared post_page_context() so
they render identically on both a real post and a happy-404 page:
- A left sidebar (three separate cards, matching /feed's sidebar-card
convention) for "Gists from {author}", "Projects from {author}" (private
projects filtered through the normal visibility check), and "Related
Discussions" - each cached per author and invalidated on create/edit/
delete so new content shows up immediately.
- A right column reusing /feed's exact Daily Topic widget class for up to
three "Featured" articles (the existing but previously-unused `featured`
news flag), cached as a pool with per-request random sampling.
- A "Next post -> " link beside "Back to Feed", pointing at the next older
post site-wide (blocked authors skipped). Wired through the same next_url
mechanism already used for listing pagination, so it emits a real
backend-rendered <link rel="next"> tag for SEO, not just a visible link.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWJy6PrMMt5hwWxQwia2rd
An upstream the gateway forwards to may itself emit X-Gateway-* headers
(e.g. another DevPlace-style gateway), which can collide with the ones
about to be built for the response. Only X-Gateway-Model is ever trusted
from upstream and relayed as-is - it is the one field an upstream can
legitimately know better than we do (it may have resolved an alias or
served a different pinned version). Every other header (cost, tokens,
latency, context, app-reference) is always our own measurement and is
never overwritten, since blending in an upstream's own accounting would
corrupt the usage ledger's per-model rollups and the quota math built on
top of it.
usage.upstream_reported_model() extracts that one header defensively
(case-insensitive lookup, rejects anything oversized or containing a
control character) and gateway._apply_served_model() applies it, display-
only, at the tail of every response-header build across chat, streaming,
embeddings, images, and passthrough.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWJy6PrMMt5hwWxQwia2rd
sync_workspace (user-triggered) and the reconciler's sync_bidirectional_sync
could run concurrently for the same project, and store_upload's read-then-
write on a changed path meant two racing imports each wrote their own blob
while only one ever got referenced - the loser leaked forever. Combined with
no build-artifact exclusion, an actively-compiling workspace hit this
constantly and leaked 5.9M orphan blobs (~96GB) in production before it was
caught.
Closes it at the root: api._sync_dir_bidirectional_locked serializes both
call sites per-project (non-blocking - a project already mid-sync is simply
skipped until the next tick), and IMPORT_SKIP_NAMES/IMPORT_SKIP_EXTENSIONS
keep build output (build/, dist/, *.o, *.pyc, ...) out of the walk entirely.
Recovering what already leaked is a separate concern: a new CLI subcommand
(plus matching make targets) sweeps soft-deleted attachment/project-file
blobs and any blob with zero DB reference at all, plus orphaned container
workspace directories. run_maintenance_cleanup.sh wraps the existing
prune/clear commands for routine disk upkeep.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWJy6PrMMt5hwWxQwia2rd
config.STATIC_VERSION becomes f"{APP_VERSION}-{BOOT_ID}": APP_VERSION is
read live from pyproject.toml's version (auto-bumped on every commit by
.githooks/pre-commit), BOOT_ID is the same per-process launch marker as
before (DEVPLACE_STATIC_VERSION env or a wall-clock fallback). The static
asset URL /static/v<version>/... now names the actual commit's version
alongside the boot marker, instead of a bare timestamp.
Fixes nginx/nginx.conf.template's versioned-mount location regex, which
matched digits only (^/static/v\d+/) and would have silently dropped the
immutable, max-age=31536000 cache header for every asset in production
once the version segment carried a dot or hyphen. Verified live against a
running instance that the header still applies to the new URL shape.
Updates README, devplacepy/static/js/CLAUDE.md, and the
/docs/static-caching.html page to describe the new APP_VERSION/BOOT_ID
composition.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TwLhnueWrsK15wrieXE5m7
asyncio.run() opens and tears down its own event loop per call, bypassing
the shared background loop tests/conftest.py's run_async uses to refresh
the snapshot cache after each coroutine. Switch provision.py's editor_ready
and view tests to run_async for consistency with the rest of the async
test suite.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TwLhnueWrsK15wrieXE5m7
pyproject.toml's version has never moved past the 1.0.0 scaffold value
across 368 commits; there was no bump mechanism at all, Claude-driven or
otherwise. Add .githooks/pre-commit (stdlib Python, no dependencies) that
increments the patch version on every commit and stages it automatically,
deferring to a deliberate version edit already staged in the same commit
and skipping merge commits. Wire it in via `make install` (git config
core.hooksPath .githooks) so it activates for every clone without
requiring any change to existing workflows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TwLhnueWrsK15wrieXE5m7
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.
- 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
- 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
- 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_grade`, `featured`, `featured_locked`, `landing_locked`, `author`, `article_published`, `image_url`, `has_unique_image` columns to news table
- Extend `news_images` schema with `alt_text`, `phash`, `width`, `height`, `is_placeholder` columns
- Create `idx_news_featured` index for efficient featured queries
- Update admin toggle endpoints to set `featured_locked`/`landing_locked` when manually toggling
- Propagate `featured` and `image_url` fields through landing page, news list, and detail page rendering
- Update `get_featured_news` to return `featured` and `image_url` fields
- Document in AGENTS.md the full zero-maintenance pipeline: image perceptual hashing, placeholder detection, AI grading on cleaned text, reliability gate, effective score computation, and post-loop landing rotation
- Update README.md service description to reflect automatic image comparison and landing rotation capabilities
- Clarify docs_api.py summaries that toggling featured/landing now locks articles from auto-rotation
The lifespan context manager replaces the deprecated `@app.on_event("startup")` pattern, centralizing initialization logic for data directories, database, certificates, and all service registrations. It also conditionally starts background services based on the `DEVPLACE_DISABLE_SERVICES` environment variable and acquires a service lock for worker coordination. Additionally, the `pyproject.toml` now suppresses three specific deprecation and syntax warnings during test runs.
Add a new `/dbapi` router package providing a generic database API over `dataset`, restricted to admin sessions, admin API keys, and the internal gateway key. Includes:
- `tables.py`: list all tables and inspect table schemas
- `crud.py`: full CRUD operations (GET, POST, PATCH, DELETE) with soft-delete awareness, born-live inserts, `?include_deleted`, `.../restore`, and `?hard=true` purge
- `query.py`: validated read-only SELECT execution via sqlglot parsing, classification, and EXPLAIN dry-run; async query jobs with WebSocket streaming via `DbApiJobService`
- `nl.py`: natural-language-to-SQL conversion using the platform AI gateway with re-prompting until validation passes
Also register `DbApiJobService` and `PubSubService` in the service manager, add `DBAPI_DIR` to config data paths, and force cleartext `http://` connections to HTTP/1.1 in `curl_transport` to fix large request failures against uvicorn's HTTP/1.1-only internal gateway.
Implement a multi-agent deep web research subsystem including CLI commands for pruning expired jobs and clearing all artifacts, database tables for sessions/messages/URL cache with indexes, config paths for chroma storage, and internal embed URL for vector operations.
Add SEO_REPORTS_DIR to config, STATIC_VERSION for cache-busting, seo prune/clear CLI subcommands, tools router with seo job endpoints, and boot-versioned static URLs in Dockerfile and Makefile
Split the monolithic test directory into three tiers (unit, api, e2e) with a path-mirroring directory structure. Added corresponding Makefile targets (test-unit, test-api, test-e2e) and updated all documentation references (CLAUDE.md, README.md, testing-cicd.html, testing-framework.html, testing-make.html) to reflect the new layout and naming conventions.
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
Add coverage tooling to CI workflow, Makefile, and pyproject.toml dependencies.
Introduce new test suites for push notification helpers (base64 encoding, HKDF,
authorization JWT) and utility functions (safe_next, strip_html, mention extraction,
badge/xp awarding, milestone checks) with a session-scoped local_db fixture.
- Introduce push notification module with VAPID certificate generation, Web Push encryption, and subscription management
- Add push registration table index and database schema for storing user subscriptions
- Integrate push notification dispatch into existing notification creation flow via async background tasks
- Include PWA manifest, service worker, and install prompt UI elements in base template
- Register new push router and ensure certificate initialization on application startup
- Add configuration variables for VAPID private/public key file paths and subscription contact email
- Update JavaScript application entry point with PushManager and PwaInstaller modules
- Extend top navigation bar with hidden install and push enable buttons for authenticated users
- Add .dockerignore, Dockerfile, and docker compose targets to Makefile for containerized deployment
- Implement gists router with CRUD operations, database schema, and polymorphic comment/vote reuse
- Create attachment upload system with thumbnail generation, MIME detection, and storage path management
- Add attachments_prune CLI command to clean orphaned attachment records and files
- Introduce rate limiting middleware with 60 requests per minute window
- Add custom 404 and 500 error handlers with SEO-optimized template responses
- Extend database initialization with gists and attachments indexes plus upload site settings defaults
- Update load_comments to include attachment mapping for comment resources
- Register gists and uploads routers in main application and update AGENTS.md documentation
Implement avatar generation via local proxy at `/avatar/{style}/{seed}` that fetches from DiceBear 9.x API with in-memory caching and fallback to initial-based SVG. Add avatar style selection dropdown to signup form and profile edit page, storing `avatar_style` in user records. Update comment rendering to support nested threading with parent-child relationships in post view. Add `avatar_url` and `avatar_styles` template globals, register avatar router in main app, and include avatar CSS classes for rounded image display.
Add complete project skeleton including .gitignore, Makefile, AGENTS.md, config, database init with indexes, main app with router mounting for auth/feed/posts/comments/projects/profile/messages/notifications/votes, Pydantic models for signup/login/post/comment/project/message/profile, and auth router with signup/login page rendering and form handling.