Pyramid imports pkg_resources which was removed in setuptools 82.
Pin across requirements, Makefile, and CI scripts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Belt-and-suspenders: ensure setuptools is in the cached venv
before any Make target runs. Shell executor preserves env/
between builds but Python 3.12+ venvs lack setuptools.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The venv target is a no-op on cached CI runners where env/bin/activate
already exists. Move setuptools install into install-source-dev-and-test
and install-source-prod so it runs every time, regardless of venv cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The requirements.py3.txt change alone wasn't enough — pip treats
setuptools specially during editable installs and may not install
it as a runtime dependency. Installing it immediately after venv
creation ensures pkg_resources is available for Pyramid and theme
loading.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pkg_resources (used for theme entry point loading) lives in
setuptools, which Python 3.12+ no longer bundles in venvs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lock/unlock, watch/unwatch, disable/enable, verify, approve/deny
now return JSON for XHR requests and swap client-side without a
page reload. Falls back to normal POST + redirect when JS is off.
The dynamic CSS sets margin-left: -48px on .nested-avatar to hang
avatars into .node padding. In the preview container (no matching
padding, overflow:hidden), this pushed the avatar off-screen.
Use plain .avatar class for preview avatars instead.
Browsers do not render images inside display:none containers and
re-assigning src does not reliably trigger re-render. Store the
avatar URL in a data attribute and create the img element via
document.createElement after the container is already visible.
Browsers skip rendering images inside display:none containers.
Re-assign the src attribute after making the div visible to force
the browser to render the SVG data URI.
Move the preview author header (avatar + name + timestamp) inside the
preview div as a persistent child element that JS shows/hides, rather
than serializing it via innerHTML which was failing to render the image.
<template>.innerHTML has DocumentFragment quirks. Use a plain
hidden div instead — its innerHTML is read directly from the
normal DOM to clone the avatar and author header for previews.
Replace data-attribute approach with a <template> element inside the
reply form. The server renders the avatar img and author name once;
JS clones the HTML for each preview update. This fixes the missing
avatar in the live preview and handles anonymous name updates.
Reply form preview now wraps rendered markdown in node-like markup
showing the author's avatar, display name, and "just now" timestamp.
Authenticated users see their real avatar; anonymous users see their
typed name. Edit form previews are unaffected.
Instead of building minimal DOM in JS, the reply handler now renders
the full node HTML using the same Jinja2 macros (avatar, author,
date, actions, edit/reply forms) and returns it as node_html.
The JS inserts the pre-rendered fragment directly.
Reply forms now submit via fetch() when JS is available, inserting
the new comment into the DOM without a page reload. Falls back to
the traditional POST + redirect when JS is disabled or on any error.
Documents capability-driven presentation practice in CLAUDE.md.
Pending requests capped at 25 with total count and "more" link.
Recent threads filtered to last 30 days ordered by created (not changed).
New users capped at 50.
CSS grid layout with cards for: people waiting on us (pending
namespace requests), moderation queue, spam filter catches, new
humans this week, most alive namespaces, and recent threads.
Direct, honest copy throughout.
Update test to perform both steps: POST with DELETE to get OTP form,
then POST with OTP code to complete deletion. Add SMTP mock since
the OTP step now sends email.
The super_fly_required decorator was missing functools.wraps, causing
Pyramid to fail to properly register topsecret views. Added wraps to
all three view decorators for consistency.
- spam_score and spam_reason columns on Node model (alembic migration)
- spam_filter_enabled column on Namespace (owners can disable Hermes)
- check_spam always returns full result dict with score/signals/reason
- Spam data stored on every node for moderation display
- "(spam)" label on flagged nodes with hover showing Hermes explanation
- Spam moderation page at /ns/{namespace}/nodes?spam with bulk actions
- Bulk approve/disable for spam nodes with checkboxes
- Hermes reason shown prominently on spam page for each flagged node
- Namespace settings checkbox: "Enable Spam Filter (Hermes AI)"
- Namespace.spam_filter_enabled respected by LLM check gate
- spam_nodes property on Namespace for querying flagged nodes
- Unit tests for spam scoring, LLM mocked tests, live Hermes integration
tests covering embed mode, site mode, edge cases (44 new tests)
When thread_uri is given to POST /api/v1/threads, the API now uses
get_or_create_node_by_uri (same as the embed iframe) to create the
root node, then posts the comment as a child reply. This ensures
threads created via the API are structurally identical to those
created by the embed, so the iframe can find and display them.
Without thread_uri, behavior is unchanged (standalone root node).
- Add trailing slash to URI lookup in test_create_thread_with_uri
to match how get_or_create_uri stores the URL
- Create localhost namespace in TestAPIAuthenticatedEditing.setUp
since api_create_thread no longer auto-creates namespaces
The API was creating orphan root nodes with no Uri record, so
the embed iframe could never find them. Now when thread_uri is
provided, a Uri record is created and linked to the node — same
association the embed uses via get_or_create_node_by_uri.
Returns 409 if a thread already exists for the given URI.
POST /api/v1/threads now returns 404 if the namespace doesn't already
exist, instead of auto-creating it. Prevents phantom namespaces from
accumulating when callers mistype the namespace name.
get_or_create_namespace is still used for read-only endpoints (GET)
where lazy creation is acceptable.
- Thread creation rate limit: 1 per 7 min per user/IP via API
- is_superuser column on User model with alembic migration
- Superusers bypass namespace-scoped is_moderator() checks
- super_fly_required now checks is_superuser instead of hardcoded names
- /topsecret/users admin page to promote/demote superusers
- Spam scoring module with 6 signals (link density, patterns, duplicates,
new account velocity, IP reputation, content length)
- Hard threshold (0.8) rejects, soft threshold (0.5) holds for moderation
- LLM relevance checking via Hermes (hermes.ai.unturf.com) on every
message when enabled, including embed mode parent page URL context
- Admin API endpoints: GET /api/v1/admin/namespaces, recent-nodes
- Spam hunting scripts: scan.py, disable_spam.py, promote_superuser.py
- Updated Python client with admin methods
- Add explicit click handler on suggestions so clicking navigates
to the existing thread instead of interacting with the form
- Fix duplicate id="thread_title_input" in forms.j2 edit macro
(renamed to edit_title_input to avoid conflict with create form)
"Do you have an API?" now correctly matches "API testing journey"
by stripping stop words (do, you, have, an) and punctuation, then
matching remaining keywords with OR logic across titles and content.
Search now splits query into keywords and matches each against both
title and data fields (not just title prefix). Also searches child
node content and returns parent threads for matches.