OpenIQ is unapologetically developed entirely with AI, with minimal human oversight.
A self-hosted guild-management application for Black Desert guilds. Django with SQLite or PostgreSQL powers independent domain modules; the browser UI uses HTML/CSS/JavaScript.
Status: locally validated platform with fixtures, reviewed imports and optional external adapters. A calibrated TCP/PCAP decoder is implemented and tested with synthetic captures; its historical calibration is not verified against the current BDO patch. The Discord bot has connected and synced commands on the Pi Swarm, while OAuth, delivery workflows, and Twitch still need live acceptance. See the feature state and task register for precise boundaries and planned work.
See the production runbook for Discord application setup, first-run onboarding and daily operations.
For the compact single-host Compose example and the production Swarm stack example, see Docker and Swarm deployment. Both examples are credential-free templates; keep their copied environment files private.
cd /home/user/src/openiq
cp .env.example .env
# Configure Discord OAuth, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS and your TLS proxy.
docker compose up --build -dOpen your configured HTTPS address and sign in through Discord. Normal member
login is Discord-only. The openiq-data named volume persists the database and
signing key across container replacement. Existing host data is not copied into
the image or container.
# Inspect status and logs.
docker compose ps
docker compose logs --tail=100 web
# Enable the optional continuous scheduler (delivery follows its explicit switch).
docker compose --profile jobs up -d
# Set RUN_DISCORD_BOT=1 in .env after setting its token and enabling delivery,
# then restart web.
docker compose up -d web
# Stop containers while retaining data.
docker compose downFor Docker without Compose:
docker build -t openiq:local .
docker run -d --name openiq -p 127.0.0.1:8765:8000 \
-v openiq-data:/data --env-file .env openiq:localThe application runs as UID 10001, includes Tesseract and uses Gunicorn/WhiteNoise to serve the app and static assets. It does not require a host Python installation. .dockerignore excludes the host database, signing key, environment files, Git metadata, virtual environment and screenshots.
Copy .env.example to .env to customize the deployment. Set the Discord OAuth
variables before normal use. ALLOW_LOCAL_LOGIN=1 exposes the password form for
development fixtures only (also set DEBUG=1, HTTPS=0, and an explicit demo
password if enabling SEED_DEMO=1); leave it disabled for a guild installation. HTTPS=1
enables secure cookies and HTTPS redirects when deployed behind TLS; set
TRUST_PROXY=1 only for a trusted reverse proxy that controls forwarded headers.
Compose enables one break-glass Django backend account by default. Each web-container start creates or rotates its random password and writes the /admin/ credential to /data/.backend-admin.json with mode 0600; startup logs contain only the file path. Retrieve it through private operator access with docker compose exec web cat /data/.backend-admin.json. The scheduler never rotates it. Set ENABLE_BACKEND_ADMIN=0 to disable the account and remove that file on the next web start, or change its stable username with BACKEND_ADMIN_USERNAME. This account is for backend recovery and inspection, not normal guild membership.
The Docker container hosts the platform. The desktop/live-interface capture process remains on the game host; offline PCAP parsing can also run in the image. No privileged container or host-network mode is required for the dashboard.
cd /home/user/src/openiq
python3 -m venv .venv
.venv/bin/pip install -r requirements.lock
.venv/bin/python manage.py migrate
ALLOW_LOCAL_LOGIN=1 .venv/bin/python manage.py seed_demo
ALLOW_LOCAL_LOGIN=1 .venv/bin/python manage.py runserver 127.0.0.1:8765 --noreloadOpen http://127.0.0.1:8765/. Demo accounts:
| Username | Role | Default password |
|---|---|---|
demo |
Owner | prototype-local-2026 |
officer |
Admin | prototype-local-2026 |
member |
Member, linked to Juniper | prototype-local-2026 |
Set DEMO_PASSWORD before the first seed to choose a different demo password. Seeding is idempotent and does not reset existing credentials. This is a localhost development server, not a production deployment. Use createsuperuser for Django administration if needed.
- Members: add/edit a family, set class, add vacation/notes, toggle exception, or switch to the draggable group board.
- History: record a war with editable participant rows; import
fixtures/scores.csvthrough Review scores and finalize the review. Change exclusions and alliance sharing separately. - Signups: join a capacity-limited team; overflow is waitlisted. Withdraw or drag members between teams. Save presets, repeat an event, lock/archive it and preview its Discord card.
- Performance: configure thresholds, create a lead, assign and resolve mentoring. Link an event to its recorded war for no-show reconciliation.
- Gear: update gear and inspect history; deletion clears the current display without losing history. Add reviewed rival snapshots for guild rankings.
- Live War: start a session, generate a synthetic fight, import
fixtures/combat.jsonl, or pastefixtures/ikusa.loginto Import IKUSA text log with the date/timezone. Save, replay, link a war and enable/revoke a public recap. - Alliance: as
demo, invite Silver Meridian, switch guild and accept. Only explicitly shared wars contribute. - Community: open/reply/close tickets; apply/review recruitment; generate welcomes, reminders, summaries, rolls and the enhancement minigame. Notifications stay as previews until explicit delivery is enabled.
- Settings: configure roles, channels, schedules, create ticket categories and application forms, run scheduled work, and inspect the audit trail.
Real OCR uses the system tesseract executable (sudo apt install tesseract-ocr if absent). Upload cropped names/stat panels in alternating pairs. Misaligned or ambiguous results require correction; they never silently finalize a war. Gear OCR recognizes labeled AP/AAP/DP text and always requires review. The checked-in dark score panels are deterministic synthetic BDO-style regression inputs, not authentic game screenshots; regenerate them with scripts/generate_ocr_fixtures.py.
Each module exposes handle(guild, action, payload, role, user) and uses shared transactional dispatch. Browser routes and Discord commands call the same service boundary.
guilds/modules/roster.py: roster, groups, class changes, identity links, notes, vacations, merge.wars.py,analytics.py,intelligence.py: reviewed scores, war history, metrics, awards, character lookup records and opponent profiles.events.py,coaching.py,gear.py,alliances.py: independent guild domain workflows.community.py,operations.py,adminops.py: support/recruitment, utilities, jobs, settings and access.integrations.py,logformat.py,guilds/capture.py: OCR, roster HTML, Twitch and event-file adapters.commands.py,discord_auth.py,delivery.py: local command routing, optional OAuth and explicit notification delivery.
Records have a relational guild/kind/key envelope, unique constraints and module-owned JSON payloads. External AI, Twitch and roster reads are prepared before acquiring the writer lock; access, role, guild revision and configuration are checked again before committing their results. Local mutations and audit entries share a short transaction. War correction history records changed rows rather than scanning every war around unrelated actions. This targets small guild installations; PostgreSQL has native snapshot/restore and concurrency integration tests. Larger deployments still need workload-specific validation.
The bot has connected and synced commands on the Pi Swarm. End-to-end OAuth, delivery, and recovery acceptance remains tracked in the task register.
# All registered commands are constructed without a network connection.
.venv/bin/python manage.py runbot --check
# Read-only remote checks; requires the bot token but sends no messages.
.venv/bin/python manage.py bot_diagnostics --guild 1
# Exercise commands locally.
.venv/bin/python manage.py local_command guildstats --guild 1 --user demo
.venv/bin/python manage.py local_command 'reminder list' --guild 1 --user member
# Process scheduled jobs once; sends only with ENABLE_DISCORD_DELIVERY=1.
.venv/bin/python manage.py tick
# Tail a normalized event log into an existing live session.
.venv/bin/python manage.py capture fixtures/combat.jsonl --session SESSION_ID --once
# Optional desktop file-capture companion (requires tkinter).
.venv/bin/python scripts/capture_desktop.pyDiscord OAuth needs DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, and DISCORD_REDIRECT_URI (default http://127.0.0.1:8765/auth/discord/callback/). Configure guild server_id and role IDs. User OAuth requests profile, guild-list and own guild-membership read scopes. Role refresh fails closed. In production, /login/ presents a Discord sign-in landing page; password login is available only when ALLOW_LOCAL_LOGIN=1. Django's separate /admin/ login remains available for the managed recovery administrator.
The bot runs as a supervised child of the web container. Set
DISCORD_BOT_TOKEN, ENABLE_DISCORD_DELIVERY=1, and RUN_DISCORD_BOT=1, then
restart web. The root compose.yaml still has a legacy discord profile
and does not forward the sync-mode variables to web; use the embedded-bot
examples in DEPLOYMENT.md when configuring command sync.
Do not enable both bot runtimes. Task 15 tracks this configuration gap.
The embedded bot shares the exact SQLite database and signing-key context with
the application, registers the command tree, and a bot failure restarts the
container rather than leaving the website silently degraded. The default
DISCORD_SYNC_GLOBAL=1 publishes global commands. For a staging server, set it
to 0 and set DISCORD_SYNC_GUILD to the numeric server ID for immediate,
guild-scoped updates. The two sync modes are mutually exclusive. deliver ID only
previews an outbox item; deliver ID --send requires delivery enablement and a
numeric target channel. Remote behavior is covered with controlled mocks but
still needs the staging-guild launch check before a real guild depends on it.
Slash commands use native typed fields instead of a generic arguments object.
Member, war, reminder and assignment selections offer guild-scoped autocomplete;
Discord account/channel fields use native pickers. Dates use ISO text with an
explicit timezone offset for timestamps. Reviewed roster names accept commas or
newlines. /config accepts a named configuration section and that section's JSON
object. Large responses are private JSON attachments rather than truncated text.
Twitch uses TWITCH_CLIENT_ID and TWITCH_ACCESS_TOKEN; without them the demo directory is explicitly labeled as fixture data.
Read the task workflow before starting work. For ordinary
changes, run the relevant Django test modules and python manage.py check.
Run node --check static/app.js when changing that file, and opt into browser
tests for affected UI behavior. Documentation-only changes need link and
command review, not the complete release gate. Linux validation is sufficient
for web/server changes; live desktop capture and other OS-facing behavior must
be validated on each supported target platform.
The full gate is a release diagnostic, not a prerequisite for every commit or PR. Its 100% coverage threshold remains unchanged.
Install requirements-dev.txt, Tesseract, Node, Chromium (python -m playwright install chromium) and the pinned accessibility tool (npm ci). Run the complete offline gate with python scripts/release_gate.py; it uses disposable data and emits release-report.json. Docker Compose is required for configuration validation (a standalone executable can be supplied with --compose PATH). Live-host checks remain separate.
Individual checks:
.venv/bin/python manage.py test
.venv/bin/python manage.py check
.venv/bin/python manage.py runbot --check
.venv/bin/python scripts/verify_ocr.py
.venv/bin/python manage.py verify_imports --output import-report.json
.venv/bin/python scripts/verify_packets.py
node --check static/app.jsBrowser checks use optional playwright (pip install -r requirements-dev.txt, then playwright install chromium). With the server running, execute scripts/browser_smoke.py. The script uses the default Playwright browser location; set PLAYWRIGHT_BROWSERS_PATH if you installed browsers elsewhere. Screenshots are in docs/dashboard.png and docs/mobile.png. Install the pinned accessibility engine with npm ci; opt-in dashboard/onboarding checks run with OPENIQ_BROWSER_TEST=1 python manage.py test guilds.test_ux_browser guilds.test_onboarding_browser.
Operator maintenance uses python manage.py maintain OPERATION --guild ID --actor OWNER.
Operations are export (--output private.json), remove_user (--username USER),
relink_discord (--username USER --member MEMBER_ID --discord-id ID),
delete_guild (--confirmation GUILD_NAME), and cleanup (--days 90). Mutations
preview by default and require --apply. Removing guild access preserves other
guild memberships; the local account is removed only when no memberships remain
and it is not a backend administrator. Cleanup removes expired capture/adoption
credentials and old audit/completed outbox records, preserving finalized wars
and their correction history. Exports are private and exclude credentials.
/healthz/ checks web-process liveness. /readyz/ and python manage.py diagnostics
check database access, migrations, writable storage and optional process heartbeats.
Set REQUIRED_PROCESSES=scheduler when the scheduler profile is enabled. The web
entrypoint automatically requires the bot heartbeat when RUN_DISCORD_BOT=1; a
heartbeat older than two minutes fails readiness. Set OPENIQ_VERSION to the deployed commit
or release. Reports contain status flags rather than credentials or data paths.
Owners can set retention through the settings API with capture_days,
import_days, recap_days, and summary_days (0 disables expiry). Preview with
python manage.py retention --guild ID; add --apply to clear expired server
capture events/import rows, revoke public recaps and expire retained summaries.
Age is measured from record creation. Live sessions and finalized wars are
preserved. OCR upload images are processed in memory and not retained. Capture
source files on separate machines require their own operator cleanup policy.
Officers can choose Export war package in History to download the war,
participant identities, linked records and correction history. The equivalent CLI
is python manage.py export_war --guild ID --war WAR_ID --user OFFICER --output war.json.
The output contains private guild data; the CLI refuses to overwrite an existing file.
Database settings and maintenance operations use a pluggable backend layer; domain queries use Django ORM. SQLite remains the supported default. See database extension guide for adapter contracts, PostgreSQL configuration, native snapshot/restore tests, and deployment-specific validation.
SQLite database: db.sqlite3; generated signing key: .secret-key. Both are excluded from Git. No credentials belong in source control.
Create a consistent online backup without stopping the services:
python manage.py backup --output /path/to/private/backups --keep 7 --timeout 120
# In Compose, use the operator-mounted backup directory:
docker compose exec web python manage.py backup --output /backups --keep 7Each timestamped snapshot includes db.sqlite3, the active .secret-key, and a
SHA-256 manifest. The command validates SQLite integrity before publishing the
snapshot and retains the newest requested count. Directories use mode 0700 and
files use 0600. Backups contain private guild data and credentials; store them in
an operator-controlled location outside Git and copy them off the application
disk. External Discord/Twitch secrets supplied through environment variables
need separate operator backups. The web container schedules snapshots only when
an operator mounts its backup directory; see backup operations and restore drill.
The same guide documents the checked, staged restore command and its offline
overwrite/recovery requirements.
Supported workflows: feature matrix. Calculations, API behavior and compatibility sources: application contracts.
The decoder is independently implemented from the public IKUSA field-calibration contract. The bundled offsets are historical (2023-04-19) and use a documentation-only server network. They are suitable for the synthetic fixture, not an assertion of current game compatibility.
.venv/bin/python manage.py packet_capture \
--calibration fixtures/calibration-historical.json \
--pcap fixtures/combat-synthetic.pcapFor an actual game session, supply a verified current calibration and explicit server network/interface selection. packet_capture --interfaces INTERFACE --calibration FILE --session SESSION_ID provides live capture through Scapy; Windows uses Npcap, while Linux requires capture permission. Live traffic has not been captured during development.
The local capture-release prototype can build and checksum-verify portable source packages:
.venv/bin/python manage.py release_capture --output /tmp/openiq-release
.venv/bin/python manage.py update_capture /tmp/openiq-release/manifest.json /tmp/openiq-capture-install --installThese are local source releases, not a published Windows executable distribution. The installer validates archive paths and checksums and replaces the installed tree atomically. A remote release service would additionally need signed manifests and platform packaging.
Optional AI text generation uses a local Ollama server when OLLAMA_MODEL is set, with OLLAMA_URL defaulting to http://127.0.0.1:11434. Without a model it clearly identifies its deterministic offline fallback.
The release gate requires 100% statement and branch coverage for the Python application and management commands. The gate report records the current test count and any skipped platform-specific tests. Coverage excludes test files and generated migrations. The C tracer is selected explicitly because Python 3.14's default monitoring tracer reported false missing branches for compact exception paths.
python -m pip install -r requirements-dev.txt
python -m coverage run manage.py test
python -m coverage report
python -m coverage html
python scripts/browser_smoke.pyThe coverage report fails below 100%. Browser smoke tests separately exercise all 12 dashboard sections and representative data-entry workflows against the running container. This is not a claim of 100% JavaScript coverage or live-service parity: Discord/Twitch/Ollama calls are mocked, and packet tests use synthetic captures.
To build and inspect a portable capture source release locally:
python manage.py release_capture --release-version 0.1.0 --output /tmp/openiq-release
python manage.py update_capture /tmp/openiq-release/manifest.json /tmp/openiq-capture --installThe Community tab can preview a private ticket-channel plan. Configure tickets.bot_user_id, tickets.staff_role, and optional tickets.category_id through the settings API. The ticket author must have a roster entry linked to both their local account and Discord ID. Ticket categories can override the staff role.
python manage.py ticket_channel TICKET_ID --guild GUILD_ID --user demoThe command previews by default. Adding --send requires ENABLE_DISCORD_DELIVERY=1 and DISCORD_BOT_TOKEN; it creates or updates a private channel, synchronizes transcript messages, and makes closed tickets read-only for their author. Officers can reopen tickets without losing replies, then synchronize again to restore sending permission. The bot needs the corresponding Discord channel/message permissions, including reading message history and embedding links. Remote behavior is tested with mocks only. Retry recovers uncertain channel creation using its topic marker and uncertain message creation using its delivery footer. If no matching remote object can be found, delivery stops for operator inspection rather than creating duplicates. Preserve those markers and run a single delivery worker per guild.
Welcome cards support native Discord role buttons. Set welcome.roles to allowed labels (for example ["Raider", "Social"]) and welcome.role_ids to the corresponding Discord role IDs. Preview a welcome card for a linked member, then use the existing opt-in outbox delivery command to post it. The bot processes button selections only for the intended member or an officer; local role selection remains available without Discord. Remote updates require Manage Roles and unmanaged roles below the bot's highest role. Set welcome.replace_selection=true to remove earlier roles granted by OpenIQ when selecting a new role; unrelated Discord roles remain intact. Requests are idempotent, so retry after a partial failure to finish reconciliation. HTTP failures do not mark the local selection as successful.