remarkbox/CLAUDE.md
russell@unturf.com 66964f736a Add spam prevention, superuser system, and LLM relevance checking
- 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
2026-02-02 09:19:16 -05:00

236 lines
8.8 KiB
Markdown

# Claude Code Configuration
## Project Setup
**IMPORTANT**: Before starting any work on a repository:
1. Check for a `CLAUDE.md` file in the repository root
2. Check for a `CLAUDE.md` file in parent directories (we often work across repos on localhost)
3. Read and follow all instructions in those files
4. These project-specific instructions override default Claude Code behavior
5. Look for conventions around commits, testing, code style, and workflows
6. If working across multiple repositories, respect the conventions from each repo's CLAUDE.md
## Commit Attribution
When creating git commits, use clean, simple commit messages:
```
Commit message here.
```
**Do NOT include:**
- `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
- `Co-Authored-By: Claude <noreply@anthropic.com>`
- Any fake corporate entities as co-authors
**Only attribute real humans** as co-authors when collaborating.
## Database Migrations (Alembic)
When adding new columns or modifying the database schema:
1. **Backup SQLite first**: `cp data/remarkbox.sqlite data/remarkbox.sqlite.bak`
2. **Add the column to the model** in `remarkbox/models/`
3. **Generate migration**: `alembic -c development.ini revision --autogenerate -m "description"`
4. **Clean up migration**: Remove extra autogenerated changes, keep only the new field
5. **Run migration**: `alembic -c development.ini upgrade head`
## Ticket System
Tracked issues live in `docs/tickets/`. Start every session by reading the index:
```bash
cat docs/tickets/index.md
```
- **Index**: `docs/tickets/index.md` is the master list. Always update it when creating or closing tickets.
- **Numbering**: Sequential. Next number = highest existing + 1.
- **Workflow**: Set status to `in-progress` when starting, `resolved` when done. Update `index.md` to match.
- **New tickets**: If you find a bug or get a feature request, create a new ticket file and add it to the index.
- **Sources**: Tickets reference community threads from `meta.remarkbox.com` and `faq.remarkbox.com` by UUID.
## Remarkbox API and Python Client
Remarkbox has a JSON API at `/api/v1/`. You can use it to read and write threads
on production as timehexon. The session cookie is saved at `~/.config/remarkbox/cookies.txt`.
### Quick start (from a Python script in the scratchpad or inline)
```python
import os, sys
sys.path.insert(0, "/home/fox/git/remarkbox/remarkbox/api")
from remarkbox_client import RemarkboxClient
c = RemarkboxClient(
"https://my.remarkbox.com",
cookie_file=os.path.expanduser("~/.config/remarkbox/cookies.txt"),
)
# Read
threads = c.list_threads("meta.remarkbox.com")
thread = c.get_thread("9f970183-ffaf-11f0-b565-040140774501")
node = c.get_node(node_id)
profile = c.get_profile()
ver = c.version()
# Write (authenticated)
result = c.create_thread(namespace="meta.remarkbox.com", title="Title", data="Body")
result = c.reply(parent_node_id, data="Reply body")
c.edit_node(node_id, data="Updated body")
c.edit_node(node_id, title="Updated title") # title only for root nodes
c.update_profile("new-display-name")
# Moderate (authenticated, moderator or owner)
c.disable_node(node_id)
c.enable_node(node_id)
c.approve_node(node_id)
c.lock_node(node_id)
c.unlock_node(node_id)
c.delete_node(node_id) # permanent, moderator only
```
### Key details
- **Client source**: `remarkbox/api/remarkbox_client.py` (stdlib only, no pip)
- **API docs**: `docs/api.md`
- **Identity**: Authenticated as `timehexon@unturf.com` (display name: `timehexon`)
- **Journey thread**: `9f970183-ffaf-11f0-b565-040140774501` on `meta.remarkbox.com` -- update this after finishing work
- **Content limit**: 500,000 characters (~128k tokens)
- **Rate limits**: 120 reads/min, 30 writes/min, 1 thread creation per 7 min (wait if you hit 429)
### Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/version` | Deployed git commit hash |
| GET | `/api/v1/threads?namespace=X` | List threads |
| GET | `/api/v1/threads/{id}` | Thread with replies |
| POST | `/api/v1/threads` | Create thread (1 per 7 min limit) |
| POST | `/api/v1/threads/{id}/replies` | Reply to thread |
| GET | `/api/v1/nodes/{id}` | Single node |
| PATCH | `/api/v1/nodes/{id}` | Edit node (data, title, disabled, approved, locked) |
| DELETE | `/api/v1/nodes/{id}` | Delete node permanently (moderator only) |
| POST | `/api/v1/auth/login` | Send OTP to email |
| POST | `/api/v1/auth/verify` | Verify OTP |
| GET | `/api/v1/user/profile` | Get profile |
| PATCH | `/api/v1/user/profile` | Update display name |
| GET | `/api/v1/clients/python` | Download Python client |
| GET | `/api/v1/admin/namespaces` | List all namespaces (superuser only) |
| GET | `/api/v1/admin/recent-nodes?days=7` | Recent nodes network-wide (superuser only) |
### Authentication
The saved cookie should work indefinitely. If it expires, you'll need an OTP:
```python
c.login("timehexon@unturf.com")
# Ask the user for the 6-digit code from their email
c.verify("timehexon@unturf.com", "123456")
```
### Functional test
Run the full idempotent test suite against production:
```bash
env/bin/python remarkbox/api/functional_test.py https://my.remarkbox.com meta.remarkbox.com timehexon@unturf.com --name timehexon
```
This exercises all endpoints and updates the journey thread with results.
## Superuser (Global Moderator)
Users with `is_superuser=True` can moderate across all namespaces. This bypasses the
normal namespace-scoped `is_moderator()` check. The admin UI is at `/topsecret/users`
where you can promote/demote users by email.
Bootstrap the first superuser via the database script:
```bash
env/bin/python scripts/promote_superuser.py --ini development.ini --email timehexon@unturf.com
```
After that, use the web UI at `/topsecret/users` or the existing topsecret admin pages
(all guarded by `@super_fly_required` which checks `is_superuser`).
Admin client methods:
```python
c.admin_list_namespaces() # list all namespaces
c.admin_recent_nodes(days=7) # recent nodes network-wide
```
## Spam Prevention
Spam detection runs automatically on `POST /api/v1/threads` and `POST /api/v1/threads/{id}/replies`.
Superusers bypass all spam checks.
**Scoring signals**: link density, known spam patterns, duplicate content, new account velocity,
IP reputation (disabled post count), content length anomalies.
**Thresholds** (configurable in `.ini`):
- `spam.hard_threshold = 0.8` -- reject with 403
- `spam.soft_threshold = 0.5` -- allow but set `approved=False` (held for moderation)
**Thread creation rate limit**: 1 new thread per 7 minutes per user/IP via the API.
This does not affect browser users or replies.
**Spam hunting scripts** (require superuser cookie):
```bash
# Scan recent posts for spam
python scripts/spam/scan.py --days=7 --threshold=0.3
# Scan and output JSON
python scripts/spam/scan.py --json --threshold=0.5
# Bulk disable flagged posts
python scripts/spam/scan.py --json --threshold=0.8 | python scripts/spam/disable_spam.py --from-json
# Disable specific nodes
python scripts/spam/disable_spam.py node-uuid-1 node-uuid-2
```
## Production Rules
**NEVER run direct SQL or raw database commands on production.** No `sqlite3`, no `UPDATE`, no `DELETE`, no direct file edits on the production database. Ever. If the API doesn't support what you need, add the endpoint first, push it, then use the client.
**ALL production changes go through the API client.** Use `RemarkboxClient` with the saved cookie at `~/.config/remarkbox/cookies.txt`. This ensures authentication, audit trails, and proper ORM handling.
**tmux-hosts is read-only.** You may use `tmux-hosts` to read logs, check processes, and investigate issues. You may NOT use it to modify data, run SQL, edit files, or restart services.
```python
# RIGHT: Use the API client
c = RemarkboxClient("https://my.remarkbox.com", cookie_file="~/.config/remarkbox/cookies.txt")
c.disable_node(node_id)
# WRONG: Never do this
# sqlite3 /opt/remarkbox/my.remarkbox.com.sqlite "UPDATE rb_node SET disabled=1 WHERE id='...'"
```
If a moderation operation is not yet supported by the API, the correct workflow is:
1. Add the endpoint to `remarkbox/api/views.py`
2. Add the method to `remarkbox/api/remarkbox_client.py`
3. Push, wait for deploy
4. Use the client
## Deployment Status
After pushing, check if the deploy is live by hitting the version endpoint:
```bash
curl -s https://my.remarkbox.com/api/v1/version
# {"version": "5a10e15"}
```
Compare the returned commit hash against `git rev-parse --short HEAD` to confirm
the latest code is deployed.
You can also check GitLab pipeline status:
```bash
# Get pipeline status via API (replace PIPELINE_ID)
curl -s "https://git.unturf.com/api/v4/projects/engineering%2Fremarkbox%2Fremarkbox/pipelines/PIPELINE_ID"
# Or view in browser:
# https://git.unturf.com/engineering/remarkbox/remarkbox/-/pipelines
```
The pipeline status will show `"status":"success"` when deployment is complete.