From 27ff894f684eb90a87f5eee660df56f0351e8211 Mon Sep 17 00:00:00 2001 From: Magent release tooling Date: Tue, 22 Sep 2026 15:12:29 +1200 Subject: [PATCH] feat(release): prepare first public preview --- .dockerignore | 1 + .gitattributes | 1 + .gitea/ISSUE_TEMPLATE/bug_report.md | 40 +++++ .gitea/ISSUE_TEMPLATE/feature_request.md | 26 +++ .gitea/ISSUE_TEMPLATE/installation_help.md | 29 ++++ .gitea/PULL_REQUEST_TEMPLATE.md | 17 ++ CHANGELOG.md | 36 ++++ CLAUDE.md | 21 +++ CODE_OF_CONDUCT.md | 23 +++ CONTRIBUTING.md | 65 ++++++++ Dockerfile | 10 +- README.md | 183 ++++++++++++--------- SECURITY.md | 15 +- SUPPORT.md | 31 ++++ THIRD_PARTY_NOTICES.md | 27 +++ backend/app/build_info.py | 7 +- backend/tests/test_backend_quality.py | 4 +- backend/tests/test_issue_acceptance.py | 2 +- backend/tests/test_newsletters.py | 6 +- docs/DOCKER.md | 8 +- docs/ENVIRONMENT.md | 4 + docs/LOCAL_DEVELOPMENT.md | 4 +- docs/NATIVE_INSTALL.md | 2 +- docs/REDDIT_LAUNCH_POST.md | 96 +++++++++++ docs/images/setup-wizard.webp | Bin 0 -> 41152 bytes frontend/app/ui/branding.test.tsx | 6 +- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- setup.sh | 43 +++++ 29 files changed, 610 insertions(+), 103 deletions(-) create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea/ISSUE_TEMPLATE/installation_help.md create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SUPPORT.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 docs/REDDIT_LAUNCH_POST.md create mode 100644 docs/images/setup-wizard.webp create mode 100755 setup.sh diff --git a/.dockerignore b/.dockerignore index faa48c8..46c6932 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ !Dockerfile !.dockerignore !LICENSE +!THIRD_PARTY_NOTICES.md !backend/ !backend/requirements.txt !backend/app/ diff --git a/.gitattributes b/.gitattributes index aa6354f..e3133b7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,7 @@ *.png binary *.jpg binary *.jpeg binary +*.webp binary *.gif binary *.ico binary *.pdf binary diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d18094c --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Report reproducible behavior that looks wrong +title: "[Bug] " +labels: "" +--- + +## What happened? + + + +## What did you expect? + + + +## How can we reproduce it? + +1. +2. +3. + +## Environment + +- Magent version/tag: +- Container image digest, if applicable: +- Installation method (Compose, Portainer, docker run, native): +- Host OS and CPU architecture: +- Browser and version: +- Reverse proxy, if applicable: +- Connected app and version involved: + +## Sanitized diagnostics + + + +- [ ] I searched existing issues and the troubleshooting guides. +- [ ] I reproduced this on the current public preview. +- [ ] I removed passwords, tokens, API keys, cookies, email addresses, private URLs and personal data. +- [ ] I did not attach an `.env`, database, backup or generated secrets file. +- [ ] This is not a security vulnerability. Security reports follow `SECURITY.md`. diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..11c78ef --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,26 @@ +--- +name: Feature request +about: Suggest an improvement to Magent +title: "[Feature] " +labels: "" +--- + +## Problem + + + +## Desired outcome + + + +## Current workaround + + + +## Relevant integration + + + +## Additional context + + diff --git a/.gitea/ISSUE_TEMPLATE/installation_help.md b/.gitea/ISSUE_TEMPLATE/installation_help.md new file mode 100644 index 0000000..e1fa2cb --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/installation_help.md @@ -0,0 +1,29 @@ +--- +name: Installation help +about: Ask for help with a documented installation path +title: "[Install] " +labels: "" +--- + +## Goal + + + +## Method and environment + +- Installation guide followed: +- Host OS and CPU architecture: +- Docker/Compose/Portainer/Python/Node versions, as applicable: +- Browser-facing URL scheme (`http` or `https`; redact private hostnames if needed): +- Reverse proxy, if applicable: + +## Where it stopped + + + +## Checks already tried + +- [ ] I checked the matching troubleshooting table. +- [ ] I confirmed the container/service health and retained the same data path. +- [ ] I removed credentials, tokens, cookies, private URLs and personal data. +- [ ] I did not attach an `.env`, database, backup or generated secrets file. diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..95d7f27 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## What and why + + + +## Validation + + + +- [ ] Backend tests pass or are not affected. +- [ ] Frontend tests, lint, format, typecheck and build pass or are not affected. +- [ ] Documentation and changelog are updated where needed. +- [ ] New behavior has regression coverage. +- [ ] No credentials, user data, databases, backups or private deployment details are included. + +## Compatibility + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c657614 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +This file records public Magent releases. The in-application changelog contains +the shorter user-facing highlights. + +## Unreleased + +- No unreleased changes recorded. + +## 0.1.0-preview.1 — 2026-09-22 + +First public preview. + +### Included + +- Guided first-install setup for the administrator, application URL and optional integrations. +- Request discovery, submission and status tracking across Seerr and Arr services. +- Jellyfin playback links, issue reporting and repair follow-up. +- Jellystat viewing insights, monthly reports and optional email recaps. +- Local/Jellyfin sign-in, invitations, account linking and feature controls. +- Encrypted configuration/database backups with staged, restart-only restore. +- Docker Compose, Docker CLI, Portainer, native Linux and foreground setup guides. +- Hardened non-root container defaults and a documented release smoke harness. + +### Known limits + +- The published image is validated for Linux/amd64 only. +- One writable SQLite instance and one backend worker are supported; HA is not. +- Native Linux services have static systemd validation, not broad distribution certification. +- This preview does not promise an LTS window or security-response SLA. + +### Upgrade note + +Existing installations must retain their data mount and signing/encryption keys. +Create an encrypted backup, record the running image digest, and follow the +[upgrade guidance](docs/DOCKER.md#updates-and-rollback). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bdf7b35 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# Repository guide + +Magent combines a FastAPI backend, Next.js frontend and SQLite database. Read +`CONTRIBUTING.md` before changing code and `docs/INSTALLATION.md` before changing +deployment behavior. + +## Important paths + +- `backend/app/`: API, integrations, setup, persistence and background work. +- `frontend/app/`: application routes and interface components. +- `backend/tests/` and frontend `*.test.*`: synthetic regression coverage. +- `compose.yml`: public prebuilt-image deployment. +- `Dockerfile`: pinned multi-stage production build. + +## Working rules + +- Never read, commit or print real `.env`, database, backup or credential data. +- Keep SQLite to one backend worker and one writable application instance. +- Preserve origin checks, CSP nonces, non-root execution and persistent key/data paths. +- Add regression tests and run the checks in `CONTRIBUTING.md`. +- Use synthetic fixtures; do not call live media services from tests. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3439a1b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,23 @@ +# Community Code of Conduct + +Magent's community should be practical, welcoming and safe for people with +different backgrounds and levels of experience. + +## Expected behavior + +- Be respectful and specific when discussing code, ideas or mistakes. +- Assume good intent while accepting corrections and evidence. +- Give useful context and protect other people's privacy. +- Welcome questions without mocking a person's experience level. +- Keep feedback about the work, not the person. + +Harassment, threats, discrimination, sexualized attention, deliberate exposure +of private information, and repeated disruptive behavior are not acceptable. + +Maintainers may edit or remove contributions and temporarily or permanently +restrict participation when needed to protect the community. Non-sensitive +conduct concerns can be raised through the issue tracker. The public preview +does not yet publish a monitored private conduct-reporting channel; do not put +private evidence or personal information in a public issue. + +This code applies in project spaces and when representing the project elsewhere. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..333e0f9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Contributing to Magent + +Thanks for helping improve Magent. Small, reviewable changes with a clear reason +are the easiest to test and merge. + +## Before opening an issue + +- Use [SUPPORT.md](SUPPORT.md) to choose the right report type. +- Search existing issues and read the relevant installation guide. +- Remove credentials, tokens, email addresses, database content and private URLs. +- Use [SECURITY.md](SECURITY.md) for vulnerabilities; do not publish exploit details. + +## Development setup + +Use Python 3.14 and Node.js 24. Follow +[the foreground development guide](docs/LOCAL_DEVELOPMENT.md), use synthetic +data, and never connect tests to someone else's services. + +## Quality checks + +Backend: + +```sh +python -m venv .venv +. .venv/bin/activate +pip install -r backend/requirements-dev.txt +python -m unittest discover -s backend/tests -p 'test_*.py' +python scripts/check_environment_docs.py +``` + +Frontend: + +```sh +cd frontend +npm ci +npm test +npm run lint +npm run format:check +npm run typecheck +npm run build +``` + +Container changes should also pass both isolated smoke modes: + +```sh +docker build -t magent:review . +bash scripts/ci_container_smoke.sh magent:review +MAGENT_SMOKE_MANAGED=true bash scripts/ci_container_smoke.sh magent:review +``` + +The smoke harness creates disposable containers, networks and volumes. Do not +aim it at a live instance or reuse a production volume. + +## Pull requests + +1. Base public work on the `release` branch. +2. Keep each pull request focused and explain the user-visible reason. +3. Add or update tests for behavior changes. +4. Update documentation and `CHANGELOG.md` when behavior or setup changes. +5. State exactly which checks ran and any platform limits or skipped checks. +6. Do not commit generated builds, `.env` files, logs, databases, backups or secrets. + +By contributing, you agree that your contribution is licensed under the project's +[MIT License](LICENSE). Participation must follow the +[Code of Conduct](CODE_OF_CONDUCT.md). diff --git a/Dockerfile b/Dockerfile index 25adbb5..7c57ba3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,6 +54,8 @@ RUN node --version ARG MAGENT_UID=1000 ARG MAGENT_GID=1000 +ARG MAGENT_VERSION=0.1.0-preview.1 +ARG MAGENT_REVISION=unknown RUN addgroup -g ${MAGENT_GID} magent \ && adduser -D -u ${MAGENT_UID} -G magent -s /sbin/nologin magent \ && install -d -o magent -g magent -m 0700 /app/data \ @@ -76,12 +78,16 @@ COPY --chown=magent:magent --from=frontend-builder /frontend/.next/static /app/f COPY --chown=magent:magent --from=frontend-builder /frontend/public /app/frontend/public COPY docker/supervisord.conf /etc/supervisor/conf.d/magent.conf -COPY LICENSE /usr/share/licenses/magent/LICENSE +COPY --chown=magent:magent LICENSE /usr/share/licenses/magent/LICENSE +COPY --chown=magent:magent THIRD_PARTY_NOTICES.md /usr/share/licenses/magent/THIRD_PARTY_NOTICES.md COPY --from=frontend-builder /licenses /usr/share/licenses/magent/frontend LABEL org.opencontainers.image.title="Magent" \ org.opencontainers.image.description="Self-hosted media requests, issues and viewing insights" \ - org.opencontainers.image.licenses="MIT" + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.source="https://git.amslabs.net/Rephl3x/Magent-Public" \ + org.opencontainers.image.version="${MAGENT_VERSION}" \ + org.opencontainers.image.revision="${MAGENT_REVISION}" USER magent:magent diff --git a/README.md b/README.md index 0972732..6ad31a2 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,111 @@ # Magent -Self-hosted media requests, viewing stats and issue management for Jellyfin, -Seerr, Sonarr, Radarr and related services. Magent combines a Python/FastAPI API, -a Next.js frontend and SQLite. Run the prebuilt non-root container or install -the Python and Node.js services directly—Portainer is optional. +Magent is a self-hosted home for media requests, viewing insights and issue +follow-up. It sits in front of Jellyfin, Seerr, Sonarr, Radarr and related apps, +then turns their technical state into a clearer experience for the people using +your server. -## Install +**Public preview:** `0.1.0-preview.1` · [Source code](https://git.amslabs.net/Rephl3x/Magent-Public) · +[Docker Hub](https://hub.docker.com/r/rephl3xnz/magent) · [MIT license](LICENSE) -Start with the [installation guide](docs/INSTALLATION.md) to choose a method: +![Magent's first-run setup wizard](docs/images/setup-wizard.webp) -| Method | Instructions | -| --- | --- | -| Docker Compose or `docker run` | [Docker installation](docs/DOCKER.md) | -| Portainer | [Single-file stack](docs/PORTAINER.md) | -| Linux without Docker | [Native install and systemd services](docs/NATIVE_INSTALL.md) | -| Windows/macOS/Linux foreground | [Source installation and development](docs/LOCAL_DEVELOPMENT.md) | +Magent does not store or stream media, replace your existing applications, or +provide content. You bring and operate those services; Magent connects to them. -For a fresh **Docker Compose** install, download [compose.yml](compose.yml) into -its own deployment directory and run: +## Why Magent? + +Media automation is powerful, but its status messages are written for operators. +Magent gives users one place to request something, understand what is happening, +see when it is ready, report a problem and follow the response. + +- Search for and submit movie or TV requests through Seerr. +- Follow a request across approval, Arr monitoring, download and Jellyfin availability. +- Open ready titles in Jellyfin and report playback or library problems. +- View private Jellystat-powered viewing insights and monthly reports. +- Offer local or Jellyfin sign-in, invite-only registration and per-user features. +- Manage integrations, users, branding, notifications and diagnostics from the UI. +- Export an encrypted backup of configuration, SQLite data and optional artwork. + +## Current scope + +- The published container is validated on **Linux/amd64**. +- Magent is a single-instance application using one writable SQLite database. It + is not an HA, Kubernetes or multi-replica deployment. +- Native Windows foreground operation has been smoke-tested. Linux systemd files + are statically validated; macOS instructions are reviewed but not runtime-tested. +- This is an early public release. Pin an immutable image tag or digest, retain + backups, and test upgrades before using them on your main instance. + +See [release notes](CHANGELOG.md) and [installation support boundaries](docs/INSTALLATION.md#platform-and-support-boundaries). + +## Quick start with Docker Compose + +Docker Compose is the recommended first installation. Create an empty directory, +download the public Compose file, and start Magent: ```sh +curl --fail --location --output compose.yml \ + https://git.amslabs.net/Rephl3x/Magent-Public/raw/branch/release/compose.yml +docker compose -f compose.yml -p magent pull +docker compose -f compose.yml -p magent up -d --wait --wait-timeout 120 +docker compose -f compose.yml -p magent exec --user magent magent \ + python -m app.container_bootstrap setup-token +``` + +PowerShell: + +```powershell +Invoke-WebRequest -Uri 'https://git.amslabs.net/Rephl3x/Magent-Public/raw/branch/release/compose.yml' -OutFile compose.yml docker compose -f compose.yml -p magent pull docker compose -f compose.yml -p magent up -d --wait --wait-timeout 120 -docker compose -f compose.yml -p magent ps docker compose -f compose.yml -p magent exec --user magent magent python -m app.container_bootstrap setup-token ``` -Keep that directory/project name and its persistent volume unchanged for updates. -This uses `rephl3xnz/magent:latest` with no environment variables, local source -build or Dockerfile. Portainer users can paste the same file into a Docker -Standalone stack instead. Native users follow their separate guide for explicit -environment configuration and service management. +Open `http://SERVER_IP:3000/setup`, enter the console-only token and confirm the +browser-facing URL. Then create the first administrator and connect the apps you +use. Optional integrations can be skipped. The managed image creates and retains +its own private keys; this path needs no `.env` or Dockerfile. -**Image availability:** the managed-install image is published on Docker Hub. -Only Linux/amd64 has been validated. `latest` is mutable; record the resolved -image digest before updating, or pin an immutable release tag. +If you cloned the repository, the optional helper exposes the same operations: +`./setup.sh check`, `./setup.sh install`, and `./setup.sh token`. Read a script +before running it; the documented Compose commands remain the canonical path. -1. Wait for the container to become healthy and retrieve the token using the - command above (or the Portainer console instructions in its guide). -2. Open the Docker host's address on port 3000. Confirm the browser-facing URL - in setup and use the token to create the first administrator. - The **Get setup token** button shows the console instructions and lets you - copy the command; it never reveals the token to public visitors. -3. Connect your apps, choose preferences and finish setup. Optional apps can - be skipped. Save an encrypted backup afterwards. +For public access, configure DNS and HTTPS before creating the administrator. +Do not forward the plain HTTP port directly to the internet. -Keep the Compose security block unchanged. Database storage is fixed at -`/app/data/magent.db` and API docs are disabled in managed installs. CORS and -cookie security follow the confirmed URL. Use HTTPS before public access. +## Other installation methods -See [all installation methods](docs/INSTALLATION.md), -[all environment options](docs/ENVIRONMENT.md), -[backup and restore](docs/installation-and-recovery.md) and -[advanced installation/upgrades](docs/PUBLIC_RELEASE.md). -Existing installations must retain their original data volume and signing/ -encryption keys; this fresh-install template is not an automatic migration. +| Method | Guide | +| --- | --- | +| Docker Compose | [Complete Docker guide](docs/DOCKER.md#docker-compose) | +| Docker CLI without Compose | [`docker run` guide](docs/DOCKER.md#docker-cli-without-compose) | +| Portainer | [Paste-and-deploy stack](docs/PORTAINER.md) | +| Build the container from source | [Source-build override](docs/DOCKER.md#build-the-container-from-source) | +| Native Linux | [Python/Node and systemd](docs/NATIVE_INSTALL.md) | +| Windows, macOS or Linux foreground | [Local/source installation](docs/LOCAL_DEVELOPMENT.md) | -## Build and test +Start with the [installation overview](docs/INSTALLATION.md) if you are unsure. +The [environment reference](docs/ENVIRONMENT.md) documents every supported +variable, while [backup and recovery](docs/installation-and-recovery.md) covers +portable exports, offline snapshots and safe restore. -The source tree contains everything needed to build the application: +### Existing installations + +Keep the same Compose project name, data mount and signing/encryption keys. The +fresh-install named volume is not an automatic migration of an existing bind +mount. Record your working image digest and make an encrypted backup before an +upgrade. Never use `docker compose down -v` on data you want to keep. + +## Build and test from source ```sh +git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent-Public.git +cd Magent-Public docker compose -f compose.yml -f compose.build.yml -p magent up -d --build ``` -For a disposable verification run, without touching an existing installation: - -```sh -docker build -t magent:review . -bash scripts/ci_container_smoke.sh magent:review -MAGENT_SMOKE_MANAGED=true bash scripts/ci_container_smoke.sh magent:review -``` - -Unit checks require Python 3.14 and Node 24: +The full quality gate uses Python 3.14 and Node.js 24: ```sh python -m venv .venv @@ -86,37 +119,37 @@ npm test npm run lint npm run format:check npm run typecheck +npm run build ``` -On Windows, activate `.venv\Scripts\Activate.ps1` instead. Do not point tests -at live services or use production credentials. +On Windows, use `.venv\Scripts\Activate.ps1`. Tests must use synthetic data and +must never point at live media services. -## How it is organised +## Project layout -- `backend/app/routers/`: authenticated API endpoints and administration. -- `backend/app/clients/`: media-service clients; `services/`: request states, - synchronisation, notifications, setup and encrypted backups. -- `backend/app/db.py` and `schema_migrations.py`: SQLite persistence/migrations. -- `frontend/app/`: pages and shared interface components; `frontend/proxy.ts`: - browser security headers and request nonces. +- `backend/app/`: FastAPI routes, service clients, request state, authentication, + setup, notifications and encrypted backups. +- `frontend/app/`: Next.js pages, shared UI, account portal and administration. - `backend/tests/` and frontend `*.test.*`: synthetic regression tests. -- `Dockerfile` and `docker/`: multi-stage build and process supervision. -- `compose.yml`: prebuilt-image install; `compose.build.yml`: source override. -- `deploy/native/`: example Linux service units and backend/frontend configuration. +- `compose.yml`: prebuilt-image install; `compose.build.yml`: source-build override. +- `deploy/native/`: example Linux service units and private configuration templates. +- `docs/`: installation, environment, HTTPS, backup, upgrade and rollback guidance. -Requests are cached from Seerr, joined to collector/download/library evidence, -normalised into a user-facing state and displayed by the frontend. App settings -are stored in SQLite; sensitive settings are encrypted with installation-specific -keys. Integrations are optional and are configured through the setup wizard. +The `release` branch is a deliberately small public export. It excludes private +deployment automation, credentials, runtime data and internal Git history. -This `release` branch intentionally excludes internal deployment scripts, -environment files, runtime data, development reports and prior Git history. -It contains no workflow that automatically deploys or publishes an image. +## Help, feedback and contributions -## Contributing and security +- Read [SUPPORT.md](SUPPORT.md) before opening an installation or bug report. +- See [CONTRIBUTING.md](CONTRIBUTING.md) for development and pull-request checks. +- Report security concerns using [SECURITY.md](SECURITY.md); never post secrets or + exploit details in a public issue. +- Community participation follows [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). -Keep changes focused, add regression tests and run the checks above. Never -commit tokens, database exports, backups or real user information. -See [SECURITY.md](SECURITY.md) for reporting guidance and deployment precautions. +Magent was developed through an iterative, AI-assisted workflow and then checked +with automated regression tests, isolated container smoke tests and a sanitized +public-release pass. Treat generated code like any other code: inspect it, test +it and report anything that does not make sense. -Licensed under [MIT](LICENSE). Third-party dependency licences remain applicable. +Magent is licensed under the [MIT License](LICENSE). Dependencies and base-image +components retain their own licenses; see [third-party notices](THIRD_PARTY_NOTICES.md). diff --git a/SECURITY.md b/SECURITY.md index 6b0003f..96690a7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,11 +4,16 @@ Do not post passwords, access tokens, encryption keys, database exports, backup files or live exploit details in public issues, discussions or container logs. -Use the repository hosting platform's private vulnerability-reporting feature -if the release owner has enabled it. Otherwise contact the maintainer privately -through the platform where you obtained this release before sending sensitive -details. This repository does not currently advertise a dedicated reporting -address; the release owner must establish one before a broad public launch. +Non-sensitive security hardening reports may be filed at +https://git.amslabs.net/Rephl3x/Magent-Public/issues/new after removing all +deployment details and secrets. The public preview does not yet publish a +monitored confidential-reporting channel. Do not put embargoed vulnerabilities, +live exploits or sensitive evidence in a public issue. If a report cannot be +made safely in public, retain it until a private contact is listed here. + +Reports are handled on a best-effort basis. The project does not promise a +specific response time or security-support window. The newest public preview is +the only version considered for security fixes unless a release note says otherwise. Include the image tag/digest, affected version, a minimal reproduction using synthetic data, and the security impact. Remove deployment credentials and diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..44d0f81 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,31 @@ +# Support + +Magent is an early public release maintained on a best-effort basis. There is no +paid support or guaranteed response time. + +## Start here + +- Installation choice: [docs/INSTALLATION.md](docs/INSTALLATION.md) +- Docker and Compose: [docs/DOCKER.md](docs/DOCKER.md) +- Portainer: [docs/PORTAINER.md](docs/PORTAINER.md) +- Native Linux: [docs/NATIVE_INSTALL.md](docs/NATIVE_INSTALL.md) +- Configuration: [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md) +- Backup and restore: [docs/installation-and-recovery.md](docs/installation-and-recovery.md) + +## Ask for help or report a problem + +Use the matching issue template at +https://git.amslabs.net/Rephl3x/Magent-Public/issues/new. + +Include the Magent version/tag and image digest, CPU architecture, installation +method, browser, exact reproduction steps, expected result and sanitized logs. +Say whether the problem survives a normal restart. Never attach `.env` files, +databases, backups, generated secret files, API keys, passwords, access tokens, +private URLs or personal viewing data. + +Feature requests should describe the problem and intended user, not only a +proposed implementation. Integration requests should link to the upstream +application's public API documentation. + +For a vulnerability, follow [SECURITY.md](SECURITY.md) instead of opening a +detailed public report. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..8d7504a --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,27 @@ +# Third-party notices + +Magent's original source is licensed under the [MIT License](LICENSE). Third-party +software and assets retain their own copyright and license terms. + +The exact JavaScript dependency graph and resolved versions are recorded in +`frontend/package-lock.json`. Production runtime packages include Next.js, +React, React DOM, Sharp and their transitive dependencies. Their license terms +come from the installed package metadata and license files, not from fields in +the lockfile. The release build collects discoverable production package +license and notice files under `/usr/share/licenses/magent/frontend`. + +The exact direct Python versions are recorded in `backend/requirements.txt` and +`docker/requirements-runtime.txt`. Python package metadata and license files are +installed with those packages and remain subject to their respective terms. + +The release container also contains components from its pinned Python, Node.js +and Alpine base images. Those components are not relicensed by Magent. The image +includes this notice, Magent's MIT license, Node.js's license, installed Python +package license files, discoverable production frontend license/notice files, +and any license files supplied by the Alpine packages in the runtime image under +`/usr/share/licenses`. + +Before redistributing a modified image, regenerate and review its software bill +of materials and preserve every applicable license, copyright and notice file. +Lockfiles are the version inventory; this summary is not a replacement for the +complete license texts supplied by each dependency. diff --git a/backend/app/build_info.py b/backend/app/build_info.py index 8593f44..b0780b6 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,2 +1,5 @@ -BUILD_NUMBER = "0803262237" -CHANGELOG = '2026-09-19|Initial minimal release source snapshot' +BUILD_NUMBER = "0.1.0-preview.1" +CHANGELOG = """2026-09-22|First public preview +2026-09-22|Guided setup for Docker, Portainer and native installs +2026-09-22|Encrypted backup and staged restore +2026-09-22|Requests, viewing insights, invites and issue follow-up""" diff --git a/backend/tests/test_backend_quality.py b/backend/tests/test_backend_quality.py index 3cd4c6d..819d5c9 100644 --- a/backend/tests/test_backend_quality.py +++ b/backend/tests/test_backend_quality.py @@ -515,7 +515,7 @@ class SiteInfoTests(unittest.TestCase): "site_banner_tone": "warning", "site_banner_background_color": "#123ABC", "site_banner_border_color": "red", - "site_login_message": "Use your Grizzlyflix account to sign in.", + "site_login_message": "Use your ExampleFlix account to sign in.", }) with patch.object(site_router, "get_runtime_settings", return_value=runtime): @@ -523,7 +523,7 @@ class SiteInfoTests(unittest.TestCase): self.assertEqual(info["banner"]["backgroundColor"], "#123abc") self.assertIsNone(info["banner"]["borderColor"]) - self.assertEqual(info["login"]["message"], "Use your Grizzlyflix account to sign in.") + self.assertEqual(info["login"]["message"], "Use your ExampleFlix account to sign in.") class SiteSettingValidationTests(unittest.IsolatedAsyncioTestCase): diff --git a/backend/tests/test_issue_acceptance.py b/backend/tests/test_issue_acceptance.py index 005e0e6..0c340d3 100644 --- a/backend/tests/test_issue_acceptance.py +++ b/backend/tests/test_issue_acceptance.py @@ -41,7 +41,7 @@ class IssueAcceptanceTests(TempDatabaseMixin, unittest.IsolatedAsyncioTestCase): content = email.await_args.kwargs self.assertEqual(content["subject"], f"Ready to try again? Magent issue #{item['id']}") self.assertIn("affected content in Jellyfin", content["body_text"]) - self.assertNotIn("grizzlyflix", content["body_html"].lower()) + self.assertNotIn("exampleflix", content["body_html"].lower()) self.assertIn("YES — it works", content["body_html"]) self.assertIn("NO — still broken", content["body_html"]) self.assertIn(f"/issues/confirm/{item['id']}#yes", content["body_html"]) diff --git a/backend/tests/test_newsletters.py b/backend/tests/test_newsletters.py index 623b8dd..2610ac5 100644 --- a/backend/tests/test_newsletters.py +++ b/backend/tests/test_newsletters.py @@ -93,7 +93,7 @@ class NewsletterConsentTests(NewsletterFixture, unittest.IsolatedAsyncioTestCase rendered = sender.call_args.args[1] self.assertEqual(rendered['subject'], 'Confirm your Magent newsletter subscription') self.assertIn('NEW IN YOUR LIBRARY', rendered['body_html']) - self.assertNotIn('grizzlyflix', rendered['body_html'].lower()) + self.assertNotIn('exampleflix', rendered['body_html'].lower()) self.assertNotIn('Arrival', rendered['body_html']) url = re.search(r'https://[^\s]+', rendered['body_text']).group(0) self.assertEqual(urlsplit(url).path, '/newsletter-subscription') @@ -393,7 +393,7 @@ class NewsletterDeliveryTests(NewsletterFixture, unittest.IsolatedAsyncioTestCas self.assertEqual(draft['intro'], custom_intro) def test_generic_email_template_preserves_custom_subject_and_intro(self): - custom_subject = 'Grizzlyflix weekend discoveries' + custom_subject = 'ExampleFlix weekend discoveries' custom_intro = 'Welcome to our own community.' rendered = template.render( {**content(), 'subject': custom_subject, 'intro': custom_intro}, {}, @@ -406,7 +406,7 @@ class NewsletterDeliveryTests(NewsletterFixture, unittest.IsolatedAsyncioTestCas self.assertIn('Welcome to our own <media> community.', rendered['body_html']) self.assertIn('Watch on Jellyfin', rendered['body_html']) self.assertIn('Explore Jellyfin', rendered['body_html']) - self.assertNotIn('grizzlyflix', rendered['body_html'].lower()) + self.assertNotIn('exampleflix', rendered['body_html'].lower()) async def test_weekly_worker_collects_once_and_delivers_to_confirmed_subscriber(self): now = datetime.now(timezone.utc) diff --git a/docs/DOCKER.md b/docs/DOCKER.md index d8e7722..9656727 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -64,7 +64,7 @@ container in either case, not a native Windows-container or macOS application. ### 1. Save the deployment file Create a new directory for this installation and save the release branch's -[raw compose.yml](https://git.amslabs.net/Rephl3x/Magent/raw/branch/release/compose.yml) +[raw compose.yml](https://git.amslabs.net/Rephl3x/Magent-Public/raw/branch/release/compose.yml) there. Keep your customized copy for future operations instead of overwriting it when updating. You need only this file for the prebuilt image. @@ -73,7 +73,7 @@ Linux, macOS or WSL: ```sh mkdir magent-install cd magent-install -curl --fail --location --output compose.yml https://git.amslabs.net/Rephl3x/Magent/raw/branch/release/compose.yml +curl --fail --location --output compose.yml https://git.amslabs.net/Rephl3x/Magent-Public/raw/branch/release/compose.yml ``` PowerShell: @@ -81,7 +81,7 @@ PowerShell: ```powershell New-Item -ItemType Directory -Path magent-install Set-Location magent-install -Invoke-WebRequest -Uri 'https://git.amslabs.net/Rephl3x/Magent/raw/branch/release/compose.yml' -OutFile compose.yml +Invoke-WebRequest -Uri 'https://git.amslabs.net/Rephl3x/Magent-Public/raw/branch/release/compose.yml' -OutFile compose.yml ``` Alternatively, open the raw link in your browser and save the plain text as @@ -236,7 +236,7 @@ It requires the full source tree and Docker build support; the prebuilt-image installation does not. Use a separate checkout directory: ```sh -git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent.git Magent-source +git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent-Public.git Magent-source cd Magent-source docker compose -f compose.yml -f compose.build.yml -p magent config --quiet docker compose -f compose.yml -f compose.build.yml -p magent build magent diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 37f4cc4..bbfe9bd 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -238,6 +238,8 @@ consumer; builder values and supervisor values are not user-editable app setting | `PYTHONUNBUFFERED` | Image: `1` | Flush Python stdout/stderr without buffering. | | `MAGENT_UID` | Build argument: `1000` | Image runtime user's UID. Not a runtime environment switch; rebuilding with a different UID also requires matching volume/tmpfs ownership. | | `MAGENT_GID` | Build argument: `1000` | Image runtime user's GID; same ownership caveat as UID. | +| `MAGENT_VERSION` | Build argument: `0.1.0-preview.1` | OCI image version label. Release builds set this to their public version; it does not change runtime data. | +| `MAGENT_REVISION` | Build argument: `unknown` | OCI image source-revision label. Release builds set this to the full source commit. | ## Manual Compose and test tooling @@ -249,6 +251,8 @@ verification tools, not the zero-input public Compose install. | `MAGENT_IMAGE` | Required by `docker-compose.hub.yml` | Explicit published image tag/digest for manual-secret installations. | | `MAGENT_BIND_ADDRESS` | `127.0.0.1` | Manual template frontend bind address. | | `MAGENT_HTTP_PORT` | `3000` | Manual template frontend host port. | +| `MAGENT_COMPOSE_FILE` | `compose.yml` | Optional `setup.sh` Compose filename override. Use a trusted local file. | +| `MAGENT_PROJECT_NAME` | `magent` | Optional `setup.sh` Compose project name. Keep it stable to retain the same named volume. | | `PYTHON_BIN` | `python3` | Interpreter used by the backend quality gate. | | `MAGENT_IMAGE_MAX_MB` | `350` | Container smoke-test unpacked image size budget in MiB. | | `MAGENT_SMOKE_MANAGED` | `false` | Test generated managed secrets when true, or synthetic manual keys when false. | diff --git a/docs/LOCAL_DEVELOPMENT.md b/docs/LOCAL_DEVELOPMENT.md index f372171..7827f1f 100644 --- a/docs/LOCAL_DEVELOPMENT.md +++ b/docs/LOCAL_DEVELOPMENT.md @@ -15,7 +15,7 @@ connect a development checkout to production credentials/databases. Linux/macOS (Bash/zsh): ```sh -git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent.git magent +git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent-Public.git magent cd magent python3.14 -m venv .venv .venv/bin/python -m pip install -r backend/requirements.txt @@ -24,7 +24,7 @@ python3.14 -m venv .venv Windows (PowerShell, with Python's `py` launcher installed): ```powershell -git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent.git magent +git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent-Public.git magent Set-Location magent py -3.14 -m venv .venv & .\.venv\Scripts\python.exe -m pip install -r backend\requirements.txt diff --git a/docs/NATIVE_INSTALL.md b/docs/NATIVE_INSTALL.md index ade28f2..7da013e 100644 --- a/docs/NATIVE_INSTALL.md +++ b/docs/NATIVE_INSTALL.md @@ -47,7 +47,7 @@ sudo useradd --system --user-group --create-home --home-dir /var/lib/magent --sh sudo install -d -o magent -g magent -m 0755 /opt/magent sudo install -d -o root -g root -m 0700 /etc/magent sudo install -d -o magent -g magent -m 0700 /var/lib/magent/data -sudo -u magent git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent.git /opt/magent +sudo -u magent git clone --branch release --single-branch https://git.amslabs.net/Rephl3x/Magent-Public.git /opt/magent sudo -u magent git -C /opt/magent rev-parse HEAD sudo -u magent python3.14 -m venv /opt/magent/.venv sudo -u magent /opt/magent/.venv/bin/python -m pip install -r /opt/magent/backend/requirements.txt diff --git a/docs/REDDIT_LAUNCH_POST.md b/docs/REDDIT_LAUNCH_POST.md new file mode 100644 index 0000000..ee41796 --- /dev/null +++ b/docs/REDDIT_LAUNCH_POST.md @@ -0,0 +1,96 @@ +# Reddit launch post + +For `r/vibecoding`, use the **Vibe-Coded Projects** category/flair and retain the +build-process section below. The community's current self-promotion guidance says +project posts need educational detail about tools, workflow and lessons rather +than only a link. Recheck its rules immediately before posting. Add sanitized +screenshots if available; never show real users, domains, libraries, service URLs +or credentials. + +## Suggested title + +I vibe-coded Magent: a self-hosted request, stats and issue portal for Jellyfin stacks + +## Alternative titles + +- Magent is ready for testers — Docker, Portainer and native installs +- Sharing Magent: one place for media requests, viewing stats and issue follow-up + +## Post + +Hey everyone, + +I’ve been vibe-coding a self-hosted project called **Magent**, and its first +public preview is ready for people to inspect, install and break: + +https://git.amslabs.net/Rephl3x/Magent-Public + +Magent is the user-facing layer I wanted around a Jellyfin media stack. It brings +requests, request progress, viewing stats and issue follow-up into one place, +while Jellyfin, Seerr, Sonarr, Radarr and the other connected apps keep doing the +jobs they already do. + +What it currently includes: + +- Search for and submit movie or TV requests through Seerr +- A plain-English request timeline across approval, Arr, downloads and Jellyfin +- “Watch now” links when something is available +- Issue reporting and repair follow-up +- Personal Jellystat viewing insights and monthly reports +- Local/Jellyfin sign-in, invitations and per-user feature controls +- Encrypted backup and restore for configuration and the SQLite database + +It does **not** provide, store or stream media. You need to operate and connect +your own services. + +Docker Compose is the recommended setup, but the repository also documents +Portainer, plain `docker run`, source-built containers, native Linux/systemd and +foreground Windows/macOS/Linux installs. A fresh managed Docker install creates +its own private keys and setup token, then walks you through the administrator +and app connections in the browser. + +Docker image: https://hub.docker.com/r/rephl3xnz/magent + +This is deliberately labelled a public preview. The container is currently +validated on Linux/amd64, Magent expects one writable SQLite instance, and it is +not presented as an HA/Kubernetes deployment. The repo documents HTTPS, reverse +proxies, origins/cookies, backups, upgrades and rollback rather than pretending +those details disappear. + +The project grew through an iterative AI-assisted/vibe-coding workflow, followed +by regression tests, isolated container smoke tests and a separate sanitization +pass for the public repository. I’m sharing the source under MIT because I want +the implementation to be inspectable, not just the result. + +### How I built it + +My workflow was much closer to directing a small engineering loop than writing +one giant prompt. I described a user problem, had Codex work across the FastAPI, +Next.js and Docker layers, then reviewed the result in a running Beta instance. +Screenshots and real failure reports became the next round of requirements. + +The useful discipline was making every vibe pass leave evidence behind. Backend +behavior got regression tests, interface changes got browser checks, and the +container got a disposable smoke harness covering fresh setup, login, security +headers, assets, encrypted backup/restore, restart and recreation. I also used +the ECC Codex plugin as a structured security/release review rather than treating +“it works on my stack” as a release gate. + +The biggest release lesson was that a Git branch is not a privacy boundary. My +first idea was a sanitized public branch alongside private Beta/Prod branches. +Before launch I caught that every branch and old tag in a public repository is +still public, so I split the three-commit sanitized export into its own repository. +That was more valuable than another round of UI polish. + +The most useful early feedback would be: + +- where setup assumes too much knowledge +- installation steps that are unclear or wrong on your platform +- integrations that behave differently on another stack +- request, stats or issue flows that confuse real users +- backup or restore problems + +Please use synthetic examples and sanitized logs in public reports—no tokens, +databases, backup files or private service details. + +Repository: https://git.amslabs.net/Rephl3x/Magent-Public diff --git a/docs/images/setup-wizard.webp b/docs/images/setup-wizard.webp new file mode 100644 index 0000000000000000000000000000000000000000..2c9cade0c16fb8f2535e46a5189e8acf1e561a69 GIT binary patch literal 41152 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 { expect(html).toContain("Welcome to our library"); expect(html).toContain("A custom message"); expect(html).toContain("Local help"); - expect(html).not.toMatch(/grizzlyflix/i); + expect(html).not.toMatch(/exampleflix/i); expect(html).not.toContain(">Beta<"); }); @@ -25,13 +25,13 @@ describe("portable default branding", () => { expect(html).toContain("MAGENT"); expect(html).toContain("Your media member portal"); - expect(html).not.toMatch(/grizzlyflix/i); + expect(html).not.toMatch(/exampleflix/i); }); it("explains the Jellyfin integration without a deployment-specific service name", () => { const html = renderToStaticMarkup(); expect(html).toContain("Jellyfin is where you watch them."); - expect(html).not.toMatch(/grizzlyflix/i); + expect(html).not.toMatch(/exampleflix/i); }); }); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cec6d4b..f118616 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "magent-frontend", - "version": "0803262237", + "version": "0.1.0-preview.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magent-frontend", - "version": "0803262237", + "version": "0.1.0-preview.1", "dependencies": { "next": "16.3.5", "react": "19.2.4", diff --git a/frontend/package.json b/frontend/package.json index 76c55fa..8395e47 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "0803262237", + "version": "0.1.0-preview.1", "scripts": { "dev": "next dev", "build": "next build", diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..1de02c4 --- /dev/null +++ b/setup.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env sh +set -eu + +compose_file="${MAGENT_COMPOSE_FILE:-compose.yml}" +project_name="${MAGENT_PROJECT_NAME:-magent}" + +usage() { + echo "Usage: ./setup.sh check|install|status|token|logs" + echo "Run this script from a directory containing the public compose.yml." +} + +compose() { + docker compose -f "$compose_file" -p "$project_name" "$@" +} + +case "${1:-}" in + check) + docker version >/dev/null + docker compose version >/dev/null + compose config --quiet + echo "Docker and compose.yml are ready." + ;; + install) + compose config --quiet + compose pull + compose up -d --wait --wait-timeout 120 + compose ps + echo "Open http://SERVER_IP:3000/setup, then run: ./setup.sh token" + ;; + status) + compose ps + ;; + token) + compose exec --user magent magent python -m app.container_bootstrap setup-token + ;; + logs) + compose logs --tail=150 magent + ;; + *) + usage + exit 2 + ;; +esac