# DevPlace - The Developer Social Network

Server-rendered social network for developers. FastAPI backend serving Jinja2 templates with ES6 interactivity. Avatar generation via Multiavatar (local, no network). SQLite via `dataset` with WAL mode and concurrency tuning.

[![CI](https://git.example.com/api/badges/DevPlace/ci/status.svg)](.gitea/workflows/test.yaml)

## Quick start

```bash
make install        # pip install -e .
make dev            # uvicorn --reload on port 10500
make test           # Playwright integration + unit tests, headless, fail-fast
make test-headed    # same tests in visible browser
make demo           # full-journey GUI demo (headed)
```

Open `http://localhost:10500`.

## Stack

| Layer | Technology |
|-------|-----------|
| Backend | Python 3.13+, FastAPI, Uvicorn (single worker) |
| Templates | Jinja2 (server-side rendered) |
| Frontend | Pure ES6 JavaScript, one class per file |
| Database | SQLite via `dataset` (auto-sync schema, `uid` PKs, WAL mode, 30s busy timeout) |
| Auth | Session cookies, SHA256+SALT via passlib |
| Avatars | Multiavatar (local SVG generation, no external API, <5ms) |
| Validation | `hawk` (Python/JS/CSS/HTML) |
| Load testing | Locust (locustfile.py) |

## Project structure

```
devplacepy/
  main.py             # FastAPI app, router registration
  config.py           # Settings from env vars + .env
  database.py         # dataset connection, index creation
  templating.py       # Shared Jinja2 environment + globals
  avatar.py           # Multiavatar generation, URL builder
  utils.py            # Password hashing, session mgmt, time_ago
  models.py           # Pydantic schemas
  routers/            # One file per domain (auth, feed, posts, ...)
  templates/          # Jinja2 HTML templates
  static/css/         # Page-specific CSS files
  static/js/          # Application.js (ES6 module)
  services/           # Background service framework (base, manager, news)
```

## Routes

| Prefix | Purpose |
|--------|---------|
| `/auth` | Signup, login, logout, forgot/reset password |
| `/feed` | Post feed with topic/tab filtering (public) |
| `/news` | Developer news listing, detail page with comments |
| `/posts` | Post detail, creation |
| `/comments` | Comment creation, deletion |
| `/projects` | Project listing, creation |
| `/profile` | Profile view, editing |
| `/messages` | Direct messaging |
| `/notifications` | Notification list, mark read |
| `/votes` | Upvote/downvote on posts, comments, projects |
| `/follow` | Follow/unfollow users |
| `/avatar` | Multiavatar proxy with in-memory cache |
| `/bugs` | Bug reports listing, creation |
| `/services` | Background service monitoring (status, logs) |
| `/admin` | Admin panel (user management, news curation, settings) |
| `(none)` | `/robots.txt`, `/sitemap.xml` (SEO) |

## Configuration

| Env var | Default | Purpose |
|---------|---------|---------|
| `DEVPLACE_DATABASE_URL` | `sqlite:///devplace.db` | Database connection string |
| `SECRET_KEY` | hardcoded fallback | Session signing key |

## Background Services

`devplacepy/services/` provides a generic async service framework. Services start on server boot, run at configurable intervals, and are gracefully cancelled on shutdown.

### Service framework

- **`BaseService`** - abstract class with async run loop, `deque(maxlen=20)` log buffer, `name`, `interval_seconds`
- **`ServiceManager`** - singleton that registers, starts, and stops all services
- **`NewsService`** - fetches news from `news.app.molodetz.nl/api`, grades with AI, stores articles >= threshold

### Adding a service

Create a class extending `BaseService`, override `run_once()`, register in `main.py`:
```python
service_manager.register(YourService())
```
The service appears at `/services` with live status and log tail.

### News service

Fetches news from a configurable API, grades each article via AI, stores ALL articles in the `news` table (nothing is silently skipped). Articles with grade >= the configurable threshold are auto-published; the rest go to draft. Admin can manually publish/draft, toggle for landing page appearance, and delete. Images are extracted from article URLs.

Configuration via admin site settings:

| Setting key | Default | Purpose |
|-------------|---------|---------|
| `news_grade_threshold` | `7` | Minimum AI grade for auto-publish |
| `news_api_url` | `https://news.app.molodetz.nl/api` | News source |
| `news_ai_url` | `https://openai.app.molodetz.nl/v1/chat/completions` | AI grading endpoint |
| `news_ai_model` | `molodetz` | AI model |

News articles have detail pages at `/news/{slug}` with full comment support (same component as posts/projects). The landing page can display curated articles toggled from admin.

CLI: `devplace news clear` - delete all news from local database.

## Database

SQLite via `dataset` with production-oriented pragmas set on every connection:

```sql
PRAGMA journal_mode=WAL;      -- concurrent readers + writers
PRAGMA synchronous=NORMAL;     -- safe with WAL, faster than FULL
PRAGMA busy_timeout=30000;     -- wait 30s instead of failing on lock
PRAGMA cache_size=-8000;       -- 8MB page cache
PRAGMA temp_store=MEMORY;      -- temp tables in memory
PRAGMA mmap_size=268435456;    -- 256MB memory map for reads
```

All indexes are created via `CREATE INDEX IF NOT EXISTS` wrapped in try/except - safe to run on every startup regardless of table state.

## Testing

- **148 tests** across 14 files: Playwright integration + unit tests
- Playwright (NOT pytest-playwright plugin - conflicts, uninstall it)
- Server starts as subprocess on port 10501 with isolated temp database
- Test users `alice_test` / `bob_test` seeded via HTTP at session start
- Tests stop at first failure (`-x` flag)
- Failure screenshots auto-save to `/tmp/devplace_test_screenshots/`
- Headed mode: `PLAYWRIGHT_HEADLESS=0 make test`

### Key test patterns

- Every `page.goto()` and `page.wait_for_url()` uses `wait_until="domcontentloaded"` - avatar images don't block test execution
- Session-scoped browser context with per-test cookie clearing
- `page.locator(...).wait_for(state="visible")` preferred over bare selectors

## Avatars

Uses [Multiavatar](https://github.com/multiavatar/multiavatar-python) - generates deterministic SVG avatars locally from a seed string (the username). No external API calls, no network dependency. Generation takes <5ms. Results are cached in-memory (cleared on server restart).

Avatar URL format: `/avatar/multiavatar/{username}?size={size}`

## CI/CD

Gitea Actions workflow at `.gitea/workflows/test.yaml`:
- Runs on push/PR to main
- Sets up Python 3.13, installs dependencies + Playwright
- Validates all source files with `hawk`
- Runs all tests with fail-fast
- Uploads failure screenshots as artifacts

## Feature workflow

1. Implement the feature (router + template + CSS + JS)
2. `hawk .` - validate all source files
3. `make test` - run all tests (fail-fast)
4. Add Playwright tests in `tests/test_*.py` for new functionality
5. For visual features: `falcon take --output /tmp/verify.png && falcon describe /tmp/verify.png`
6. Update `AGENTS.md` and `README.md` if new conventions were introduced

## License

MIT - DevPlace
