{"id":50488882,"url":"https://github.com/mosamlife/wpmgr","last_synced_at":"2026-06-12T02:01:18.883Z","repository":{"id":361912290,"uuid":"1251221821","full_name":"mosamlife/wpmgr","owner":"mosamlife","description":"Open-source, self-hostable WordPress fleet management: backups, updates, uptime monitoring, security, and image optimization for every site from one dashboard you own. A self-hosted MainWP and WP Remote alternative.","archived":false,"fork":false,"pushed_at":"2026-06-09T00:59:30.000Z","size":10642,"stargazers_count":52,"open_issues_count":2,"forks_count":7,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-06-09T02:25:58.756Z","etag":null,"topics":["backup","fleet-management","go","open-source","react","self-hosted","wordpress","wordpress-management"],"latest_commit_sha":null,"homepage":"https://wpmgr.app","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mosamlife.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE.md","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-27T11:11:06.000Z","updated_at":"2026-06-09T00:59:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mosamlife/wpmgr","commit_stats":null,"previous_names":["mosamlife/wpmgr"],"tags_count":38,"template":false,"template_full_name":null,"purl":"pkg:github/mosamlife/wpmgr","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mosamlife%2Fwpmgr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mosamlife%2Fwpmgr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mosamlife%2Fwpmgr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mosamlife%2Fwpmgr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mosamlife","download_url":"https://codeload.github.com/mosamlife/wpmgr/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mosamlife%2Fwpmgr/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34225351,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-12T02:00:06.859Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backup","fleet-management","go","open-source","react","self-hosted","wordpress","wordpress-management"],"created_at":"2026-06-02T01:00:24.458Z","updated_at":"2026-06-12T02:01:18.860Z","avatar_url":"https://github.com/mosamlife.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WPMgr\n\n**Open-source, self-hostable WordPress fleet management.**\n\nWPMgr lets you enroll, monitor, update, back up, and secure a fleet of WordPress sites from one dashboard — all running on infrastructure you control. The control plane is a Go binary with a React dashboard; a lightweight PHP plugin on each managed site handles the work. Everything between the agent and the control plane is Ed25519-signed.\n\n**v0.12.0** — first public release. Early but production-usable for self-hosters.\n\n---\n\n## Features\n\n### Fleet connection\n\n- **Live enrollment** — Add a site by URL; paste a one-time code into the agent plugin; the dashboard flips from \"Awaiting\" to \"Connected\" automatically, no refresh needed.\n- **Real-time connection state machine** — Six precise states (`pending_enrollment` → `connected` → `degraded` → `disconnected` → `revoked` → `archived`) replace a vague up/down flag. Every transition is written to auditable, hash-chained history.\n- **60 s heartbeat with auto-recovery** — A background sweeper degrades → disconnects silent sites within minutes. A returning agent auto-recovers without operator action.\n- **Fleet SSE stream** — One shared Server-Sent Events stream keeps the entire sites list live (status dots, last-seen counters) without polling. Cursor-based replay catches events missed while offline.\n- **One-click autologin to wp-admin** — Single-use, short-lived, audited EdDSA tokens; no shared passwords. Deep-links straight to Plugins or Themes, or log in as a specific user.\n- **Signed dashboard-to-agent revoke** — Revoke from the dashboard; the agent verifies a signed token on its next heartbeat and self-destructs. A man-in-the-middle on the heartbeat response cannot forge the teardown.\n- **Signed last-will on deactivate/uninstall** — Agent disconnects itself on deactivation (3 s best-effort); the timeout sweeper is the safety net if it never arrives.\n- **Re-enrollment under a stable identity** — Re-connecting a site keeps the same `site_id`, preserving all backup history, scan runs, and lifecycle generations.\n- **Archive / restore soft-delete** — Retire sites from the active view without losing their history; restore them later.\n- **Per-site sharing** — Share exactly one site with a collaborator, enforced by both Gin middleware and Postgres RESTRICTIVE RLS, without exposing the rest of the fleet.\n\n### Backups \u0026 restore\n\n- **Pure-PHP streaming DB dump** — Server-side cursor (`MYSQLI_USE_RESULT`), `REPEATABLE READ` snapshot, ~1 MiB batched INSERTs. No mysqldump binary, no shell access — works on locked-down managed hosts. Memory use is independent of database size.\n- **Pure-PHP streaming file archiver** — ZipArchive streaming (never loads file bodies into memory), rotated at 200 MiB / 55 k entries per part. Splits wp-content into per-component sequences (plugins, themes, uploads, other) for targeted restore.\n- **Content-addressed chunking with dedup** — Each artifact is chunked at ~4 MiB, BLAKE3-hashed, and deduplicated across snapshots. Only changed chunks re-upload on the next backup.\n- **Client-side age encryption** — Optional X25519 + ChaCha20-Poly1305 per-chunk encryption to the site's public recipient; the control plane stores only ciphertext and never holds a decryption key.\n- **Three backup destinations** — Control-plane-managed bucket, customer-owned S3-compatible bucket (agent never holds the credentials), or a local folder on the WordPress host.\n- **SQL inspection at backup time** — A streaming constant-memory scanner produces `sql-inspection.json` (charset, table prefix, per-table row/byte estimates, WordPress detection, `siteurl`/`home`/`db_version`) stored with every snapshot.\n- **Environment fingerprint** — `environment.json` captures PHP/MySQL/WordPress versions, plugin/theme slugs, table list, and size at snapshot time.\n- **Resumable watchdog-driven state machine** — Phases are checkpointed to a task row; a watchdog re-enters stalled backups up to 6 times. Long backups survive PHP time limits and FPM worker recycling without redoing finished work.\n- **Scheduled backups** — Hourly, every-N-hours, daily, weekly, or monthly cadences, run in the site's own WordPress timezone, jittered per site so the fleet doesn't fire simultaneously.\n- **Retention GC with monthly archives** — Rolling window + keep-last + monthly archive flag; shared chunks only deleted when their refcount reaches zero.\n- **Live SSE progress** — Phase-by-phase progress including chunk and byte counters streams to the dashboard in real time.\n- **Component-scoped restore** — Restore the whole site, just the database, just files, or fine-grained components (plugins, themes, uploads, wp-content). Compose with per-path or per-table lists.\n- **Atomic file restore** — Extract to a hidden staging directory, move live tree aside to a `.wpmgr-old-files-\u003cid\u003e/` rollback dir, rename staging into place. A crash never leaves a half-merged site.\n- **Online DB restore** — Import into `tmp\u003cid\u003e_`-prefixed tables while WordPress stays live; swap each table atomically via `DROP + RENAME` at the end. A failed import leaves the live database untouched.\n- **Resumable restore** — Same watchdog pattern as backup: persisted phase state, chunk-level download resume, mid-table URL-rewrite resume.\n- **Two-leg disk-free precheck** — Estimates required disk before touching anything; refuses with a GB-denominated message if there is not enough space.\n- **Self-preservation guards** — Never clobbers the running agent plugin, keystore, `wp-config.php`, `.htaccess`, or cache drop-ins; copies them forward from the live tree into staging before the swap.\n- **Path-traversal \u0026 integrity hardening** — Every downloaded chunk is BLAKE3-verified; every zip entry is traversal-checked with a canonical-path containment check against the staging root.\n- **Maintenance-mode windowing** — Drops WordPress's `.maintenance` file around the destructive swap; removes it and flushes object cache, OPcache, and rewrite rules on completion.\n\n### Updates\n\n- **Per-site available-updates inventory** — WordPress core, plugin, and theme update lists with current → new versions, active-first sorted.\n- **On-demand inventory refresh** — Force re-poll WP update transients via a signed CP→agent command; returns 409 with a clear message when a site is unreachable.\n- **Bulk fleet-wide update runs** — Target an explicit set of site IDs or a tag; expands to per-(site, item) tasks.\n- **Dry-run preview** — The bulk wizard defaults to `dry_run=true` so the first submit is a safe preview with exact version deltas.\n- **Pre-update snapshot + automatic health-check rollback** — The agent snapshots the component before updating; the CP health-probes the site after; a bad update is reverted automatically.\n- **WP-CLI-first with PHP upgrader fallback** — Works with or without WP-CLI; preserves active-plugin state on the PHP fallback path.\n- **Live SSE progress per run** — Task status transitions stream in real time; a snapshot-on-connect plus a 2 s poll safety net prevent stale \"Queued\" states.\n- **Post-update inventory auto-refresh** — Debounced per-site (30 s window) after every terminal task so the available-updates list self-heals.\n- **Argument-injection hardening** — Slugs and versions are validated on both the CP (`validateItems`) and agent (`sanitizeSlug`, `isValidVersion`) against a safe charset; shell metacharacters, `..` traversal, and flag separators are rejected.\n- **Per-tenant concurrency isolation** — Sharded River queues with a per-tenant running-task limit; one large fleet update cannot starve other tenants.\n- **Update run history** — Auditable trail of who updated what, when, outcome per site/item, with from/to version and timings.\n\n### Monitoring \u0026 health\n\n- **Active uptime monitoring** — SSRF-hardened probes classify up/down with a full timing breakdown: DNS, connect, TLS handshake, TTFB, total.\n- **Uptime % + latency charts** — Windowed reports over 7 d / 30 d / 90 d; downsampled server-side. Tenant-wide status summary across the whole fleet.\n- **TLS certificate tracking** — Expiry, issuer, and subject captured on every HTTPS probe; flags \"renew soon\" at \u003c 14 days.\n- **Pluggable time-series store** — Postgres (default, one row per probe) or ClickHouse (MergeTree, 90-day TTL) selectable at boot.\n- **Downtime/recovery alerts** — Transition-only (opens incident on N consecutive failures, closes on recovery). One downtime alert and one recovery alert per outage — never a flood.\n- **Alert channels** — Email (SMTP) and HMAC-SHA256-signed webhook. Both uptime and high-severity security events share one channel config.\n- **Full WordPress Site Health collection** — Ships the complete `WP_Debug_Data::debug_data()` dump (all native sections plus third-party plugin contributions from Yoast, WooCommerce, ACF, etc.) centrally.\n- **14-category extended diagnostics with fault isolation** — Identity, PHP, MySQL, filesystem, HTTP loopback, cron, themes, plugins, users, security constants, HTTPS, mail, performance, hosting — each in an isolated wrapper so one failing probe doesn't blank the screen.\n- **Leapfrog diagnostic signals** — Max-overdue WP-Cron age, OPcache hit-rate/memory, `site_as_of_hash` fingerprint (changes when any managed component moves), hosting platform detection, paid-plugin license presence (ACF Pro, Gravity Forms, Elementor Pro, Divi, and others).\n- **JIT directory sizes** — Fresh or cached (\u003c 6 h), computed via `du` or PHP fallback, annotated with method + computed-at timestamp. Never blank like the native Site Health screen.\n- **Privacy redaction** — Agent-side recursive walker redacts `admin_email`, `*_password`, `*_secret`, `*_token`, API keys, and auth salts before the diagnostics blob leaves the site.\n- **On-demand diagnostics refresh** — One click re-collects all categories and lands the data before the response returns.\n- **PHP error monitoring** — Error + shutdown handlers (including a must-use plugin that arms before other plugins boot, catching bootstrap-time fatals). Deduped by `md5(code:file:line:message)` with occurrence counters and gzip-compressed backtraces. Near-immediate ship on fatal.\n- **Per-site error config** — Level bitmask + fingerprint silence list pushed to the agent. Silencing a fingerprint deletes its existing rows immediately. Fatals always captured regardless of mask.\n- **Hash-chained activity log** — ~30 WordPress events (posts, comments, users, auth + failed logins, plugins, themes, core updates, terms, security-relevant options, WooCommerce order/product events) written as a SHA-256 chain. Control plane re-verifies byte-for-byte on ingest and on demand.\n- **Filtered, paginated activity feed** — Newest-first cursor pagination with filters by event type, object type, actor, severity, and time range. Per-row `chain_valid` flag; a `/verify` endpoint reports the first broken link if any row was altered.\n\n### Security\n\n- **Core file-integrity scan** — Resumable, cursor-paginated MD5 walk diffed against the official WordPress.org Checksums API for the site's exact version + locale.\n- **Three finding types** — `core_modified` (high), `core_missing` (medium), `core_unknown_injected` (high). Only flags within core paths; operator-mutated files (`wp-content/`, `wp-config.php`, cache drop-ins, etc.) are allow-listed to minimize false positives.\n- **Checksum caching** — 30-day positive TTL (releases are immutable), 6-hour negative TTL, transparent `en_US` locale fallback. Repeated fleet scans never hammer the public wp.org API.\n- **Finding triage** — Mark findings ignored (audited). Fetch raw file contents in-dashboard (server-gated: only stored findings, access audited) without SSH.\n- **Brute-force login protection** — Three escalating tiers per sliding window: captcha gate → per-IP temporary block → global site-wide block. Known-good bypass (recent success from same IP) avoids locking out legitimate admins.\n- **Three protection modes** — `disabled` (inert by default), `audit` (records/logs, never blocks), `protect` (enforces 403). Malformed config push falls back to safe defaults.\n- **Early-boot WAF IP firewall** — Must-use plugin (loads before any other plugin) checks deny CIDRs at the very start of WordPress boot; allow CIDRs and private/loopback IPs always win first. DB error fails open.\n- **Operator-controlled CIDR allow/deny** — IPv4 + IPv6, validated on the CP (`net.ParseCIDR`), binary `inet_pton` bitmask comparison on the agent. Spoof-resistant configurable real-IP header.\n- **Lockout safety rail** — Enabling protect mode with an empty allow-list auto-adds the operator's own IP as a `/32` or `/128`.\n- **Manual IP unblock** — Deletes the IP's failure rows, resetting its sliding-window counter while preserving success rows.\n- **Login-page whitelabeling** — Per-site logo URL, logo link, and message. URLs scheme-validated (http/https only); message run through a narrow `wp_kses` allowlist (no `script`/`style`/`iframe`/`on*`). Safe to inject CP-controlled content into `wp-login.php`.\n- **Hashed, role-scoped API keys** — `wpmgr_\u003cprefix\u003e_\u003csecret\u003e` format; only sha256 hash + prefix stored; secret shown once at creation; constant-time compare (`crypto/subtle`); per-key role flows through the full RBAC matrix.\n- **Per-site access enforcement** — Every scan, login-protection, login-brand, and unblock route requires `RequireSiteAccess`. Routes resolved by global ID (get-run, ignore-finding, fetch-file) re-resolve the object's real site and call `CanAccessSite` before reading or mutating.\n\n### Media optimization\n\n- **Dedicated cloud encoder** — Separate `media-encoder` container decodes from magic bytes (never trusted MIME) and re-encodes to AVIF (q50/speed8), WebP (q80), or re-optimized original. Animated GIFs → animated WebP.\n- **Optional, opt-in** — `docker compose --profile media up`; zero weight or native-library CVE surface on the core API for operators who don't use it.\n- **Per-image optimize and full reversible restore** — Originals archived on-site (`.wpmgr-original.*` rename); restore reverts every variant. A separate admin-gated delete-originals reclaims disk.\n- **No image bytes on the control plane** — Source and optimized bytes move agent-to-storage and encoder-to-storage via presigned URLs only. CP stores metadata rows only.\n- **`.htaccess` Accept-header fallback** — Serves the modern format only when the browser's `Accept` header advertises support; legacy twin is served otherwise. `Vary: Accept` for CDN correctness.\n- **Auto-optimize on upload** — Per-site opt-in hooks `wp_generate_attachment_metadata`, debounces (~25 s) batch pushes to the CP, and runs the existing optimize pipeline. Four stacked guards prevent self-optimization loops.\n- **Library sync + savings metrics** — Total assets, optimized/pending/failed/unsupported counts, total bytes saved across all variants including thumbnails.\n- **Serialization-safe DB URL rewrite** — Rewrites old-to-new media URLs across `post_content` and postmeta in a way safe for PHP-serialized data; recorded for reversible restore.\n- **Resilient per-variant encoding** — Per-variant failures never fail siblings; 50 MB / 100 megapixel source limits; 60 s per-encode timeout; size + magic-byte verification of downloaded outputs before write.\n\n### Team \u0026 access\n\n- **Multi-tenant organizations** — Each resource scoped to an org. Isolation enforced by Postgres Row-Level Security (`app.tenant_id` GUC) with the app role `NOSUPERUSER NOBYPASSRLS`.\n- **Four-role RBAC** — `owner \u003e admin \u003e operator \u003e viewer` with a discrete permission matrix. Privilege ceiling prevents granting a role higher than your own.\n- **Per-site collaborator sharing** — Outside users (no org membership) scoped to one site, enforced by both Gin middleware and RESTRICTIVE RLS. Blocked from all org-level actions.\n- **Tokenized invitations** — Single-use, 7-day-expiry SHA-256-hashed tokens bound to the invited email. Existing accounts must re-authenticate; a leaked link alone cannot log anyone in.\n- **Email/password auth with first-run bootstrap** — argon2id hashing; first signup creates the org + owner (then closes open registration). Login responses never reveal whether an email exists.\n- **OIDC / SSO** — OpenID Connect relying party with PKCE, state, and nonce. Account linking only when the IdP asserts the email is verified. Ships with a Dex container so SSO works locally.\n- **Tamper-evident audit log** — SHA-256 hash-chained, append-only (DB role denied `UPDATE`/`DELETE`). Covers logins, member/role changes, API-key changes, site lifecycle, sharing, autologin, media consent, updates. `/audit/verify` reports the first broken link.\n- **API keys** — Role-scoped, revocable, audited. Carries the same RBAC permission matrix as a user session.\n\n---\n\n## Architecture\n\n```\napps/api    — Go 1.26 + Gin control plane (modular monolith)\napps/web    — React 19 + TypeScript + Vite + TanStack dashboard\napps/agent  — PHP 8.1+ WordPress agent plugin (MIT)\n```\n\n**Data:** Postgres (primary + RLS) · Redis (sessions, cache, dedup) · S3-compatible object storage (backups, media) · ClickHouse (optional, uptime time-series)\n\n**Agent ↔ CP auth:** Ed25519 signed requests (canonical `METHOD\\nPATH\\nTIMESTAMP\\nNONCE\\nsha256(body)`) with per-request nonce + timestamp for anti-replay. CP→agent commands are short-lived Ed25519-signed JWTs scoped to one site and one operation.\n\nSee [docs/architecture.md](./docs/architecture.md) for a full system diagram.\n\n---\n\n## Quickstart (self-host)\n\nThe bundled compose brings up the full stack — control plane, dashboard, Postgres, Redis, and object storage — building the API and dashboard from source:\n\n```bash\ncp .env.example .env\n# Edit .env — at minimum set WPMGR_SESSION_SECRET, WPMGR_DB_PASSWORD, WPMGR_S3_SECRET_KEY\ndocker compose -f infra/docker-compose.yml up -d\ncurl localhost:8080/healthz   # {\"status\":\"ok\"}\n```\n\nOpen `http://localhost` in your browser — the first signup creates the owner account and closes open registration.\n\nInclude the optional media encoder with the `media` profile:\n\n```bash\ndocker compose -f infra/docker-compose.yml --profile media up -d\n```\n\n### Prebuilt container images\n\nThe `v0.12.0` control plane, dashboard, and (optional) media encoder are published on GitHub Container Registry — wire them into your own compose, Kubernetes, or Swarm for production:\n\n```bash\ndocker pull ghcr.io/mosamlife/wpmgr-api:v0.12.0\ndocker pull ghcr.io/mosamlife/wpmgr-web:v0.12.0\ndocker pull ghcr.io/mosamlife/wpmgr-media-encoder:v0.12.0   # optional\n```\n\n\u003e A turnkey \"pull-only\" compose (no local build) is on the near-term roadmap.\n\nFull install guide, env reference, and production hardening: [docs/install.md](./docs/install.md).\n\n---\n\n## Install the Agent\n\n1. Download `wpmgr-agent.zip` from the [GitHub Releases page](https://github.com/mosamlife/wpmgr/releases).\n2. In WordPress: **Plugins → Add New → Upload Plugin** → choose the zip → **Install Now → Activate**.\n3. Go to **Settings → WPMgr** and paste the enrollment code from the dashboard.\n\nThe agent requires PHP 8.1+ and WordPress 6.0+. It self-updates through the control plane's signed update channel.\n\nSee [docs/agent.md](./docs/agent.md) for WP-CLI install and configuration options.\n\n---\n\n## Roadmap\n\nThe following are accepted architectural decisions with no implementation yet:\n\n- **Fleet-wide backup browser** — Browse and filter snapshots across all sites from one view (route placeholder present, not built).\n- **Backup download** — No presigned-download endpoint or web UI; restore is the current recovery path.\n- **Scheduled update runs** — `scheduled_at` is stored and accepted; no deferred dispatcher yet (tasks enqueue immediately on create).\n- **CAPTCHA challenge on login block** — Login protection currently serves a static 403; no challenge/solve flow built.\n- **Plugin/theme content malware scanning** — The scan engine covers WordPress core checksums only; no signature or heuristic detection for wp-content files yet.\n- **Automatic restore rollback UI** — The `.wpmgr-old-files-\u003cid\u003e/` rollback directory is preserved; no operator-initiated rollback endpoint exposed.\n- **Helm chart / Terraform provider** — Stubs exist under `infra/`; not implemented.\n- **AI features** — `apps/api/internal/ai/` contains only a `.gitkeep` placeholder.\n\n---\n\n## Repository layout\n\n```\napps/\n  api/      Go control plane\n  web/      React dashboard\n  agent/    WordPress agent plugin\npackages/\n  openapi-client/   generated TypeScript API client\ninfra/\n  docker-compose.yml\n  Dockerfile.api · Dockerfile.web · Dockerfile.media-encoder\n  postgres/ · seaweedfs/ · nginx/ · dex/ · grafana/\ndocs/\n  install.md · agent.md · architecture.md · contributing.md · security.md · api.md · adr/\n```\n\n---\n\n## Development\n\n```bash\ncp .env.example .env\ndocker compose -f infra/docker-compose.yml up -d   # data plane\n\n# API\ncd apps/api \u0026\u0026 go run ./cmd/wpmgr\n\n# Web\ncd apps/web \u0026\u0026 pnpm dev\n\n# Regenerate OpenAPI client after schema changes\ngo generate ./internal/api/gen/...\npnpm -C packages/openapi-client generate\n```\n\nSee [docs/contributing.md](./docs/contributing.md) for the full dev setup, PR checklist, and ADR process.\n\n---\n\n## License\n\n| Component | License |\n|---|---|\n| Control plane + dashboard (`apps/api`, `apps/web`) | [AGPL-3.0-only](./LICENSE) |\n| WordPress agent plugin (`apps/agent`) | [MIT](./LICENSE-AGENT) |\n\n---\n\n## Links\n\n- [Install (self-host)](./docs/install.md)\n- [WordPress agent](./docs/agent.md)\n- [Architecture](./docs/architecture.md)\n- [API reference](./docs/api.md)\n- [Contributing](./docs/contributing.md)\n- [Security policy](./docs/security.md)\n- [Architecture decisions](./docs/adr/)\n- [GitHub Releases](https://github.com/mosamlife/wpmgr/releases)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmosamlife%2Fwpmgr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmosamlife%2Fwpmgr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmosamlife%2Fwpmgr/lists"}