- 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
236 lines
8.8 KiB
Markdown
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.
|