Files
ma_proposal_bot/README.md
T
uhlwoogiandClaude Opus 5.5 5b7b73225f Run Proposal Bot as a standalone Docker container
Convert the app from the Windows/server2 share deployment to a Docker
stack deployed through Portainer behind Nginx Proxy Manager.

- Serve the Project Images library (it returned 403) from the mounted folder
- Make the InDesign export work in a container: load app images from disk,
  run Chromium without the sandbox, and wait for its full output
- Return error messages instead of stack traces; cap export request size
- Warn at startup about missing sign-in settings or an unwritable data folder
- compose.yaml: join NPM's network, take all settings from stack variables,
  require absolute host paths for data and the photo library
- Add .dockerignore, .env.example, and .gitignore
- Stop tracking data, Project Images, Backups, and InDesign Exports
- Remove the share publishing, Windows host, and browser migration scripts
- Rewrite the README for Portainer and NPM deployment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:45:35 +00:00

92 lines
5.3 KiB
Markdown

# Proposal Bot
Proposal Bot builds formatted fee proposals in the browser. It exports PDF, Word, and editable InDesign packages. It runs as a single Docker container; staff use it through a web browser.
## What lives where
| Folder | Purpose | Container access |
|---|---|---|
| `data/` | Saved projects, one JSON file per project with its uploaded images embedded. This is the live project store. | read/write |
| `Project Images/` | Photo library. Each subfolder is a typology (for example `MF High`, `Institutional`, `Headshot`) and appears as a filter on the Photos tab. | read only |
| `Backups/`, `InDesign Exports/` | Archives from before the Docker move. The app does not use them. | not mounted |
The image contains only the app code and Chromium (used by the InDesign export). Projects and photos stay on the host in the folders above, so rebuilding or replacing the container does not touch them.
## Deploy with Portainer
The stack has no published port. Nginx Proxy Manager (NPM) handles HTTPS and reaches the container by name over NPM's Docker network.
### 1. Put the project and photo folders on the Docker host
`data/` and `Project Images/` are not in git. Copy them from a working copy to the Docker host once, for example:
```
sudo mkdir -p /opt/proposal-bot
sudo rsync -a data "Project Images" /opt/proposal-bot/
sudo chown -R 1000:1000 /opt/proposal-bot/data # the container runs as UID 1000
```
### 2. Create the Microsoft Entra app registration
1. Create a web app registration for Proposal Bot with a client secret.
2. Register the callback `https://<proposal-bot-hostname>/auth/microsoft/callback`, using the hostname NPM will serve.
### 3. Create the stack
In Portainer, go to **Stacks > Add stack > Repository**:
- **Repository URL:** this repo's Gitea URL. Turn on **Authentication** and use a Gitea access token if the repo is private.
- **Compose path:** `compose.yaml`
- **Environment variables:** add every entry from `.env.example`. **Advanced mode** accepts the whole file pasted in.
- `NPM_NETWORK`: the network NPM's container is attached to. To find it, open NPM's container in Portainer and look at its network section, usually something like `npm_default`.
- `PROPOSAL_BOT_DATA_HOST_PATH` and `PROPOSAL_BOT_PROJECT_IMAGES_HOST_PATH`: the folders from step 1. Use absolute paths. A relative path would end up inside Portainer's copy of the repo and be lost on redeploy.
- `MICROSOFT_REDIRECT_URI`: exactly the callback from step 2.
Deploy the stack. Portainer builds the image on the host, which takes a few minutes the first time because it installs Chromium. The container should show **healthy**. Its logs show startup warnings, such as missing sign-in settings or a `data` folder the container cannot write to.
### 4. Add the NPM proxy host
- **Domain:** the Proposal Bot hostname
- **Forward:** scheme `http`, hostname `proposal-bot`, port `4181`
- **SSL tab:** request or choose a certificate, and turn on **Force SSL**
- **Advanced tab:** paste the lines below so long InDesign exports are not cut off after 60 seconds:
```
proxy_read_timeout 300s;
proxy_send_timeout 300s;
```
Open the HTTPS address, sign in, and confirm the Drafts list shows the existing projects.
### Updating
Push the code changes to Gitea, then click **Pull and redeploy** on the stack in Portainer. Turning on **GitOps updates** makes Portainer redeploy automatically after each push. Projects and photos live in the host folders, so redeploys leave them untouched.
### Without Portainer
Copy `.env.example` to `.env`, fill it in, and run `docker compose up -d --build`.
## Sign-in
Sign-in uses Microsoft Entra and the email allowlist in `PROPOSAL_BOT_ALLOWED_EMAILS` (comma-separated). Microsoft only accepts HTTPS callbacks, which NPM provides.
Sessions are kept in memory, so restarting or redeploying the container signs everyone out.
`PROPOSAL_BOT_AUTH_REQUIRED=false` turns sign-in off for a trial run. Anyone who can reach the site can then read and edit every project, so only do this on a trusted network.
## Day-to-day operation
- **Add photos:** copy them into a typology subfolder of `Project Images/` on the host (`.jpg`, `.png`, `.gif`, or `.webp`). They appear the next time someone opens the Photos tab. No restart is needed.
- **Back up:** the data folder is the only one the app writes. Back it up regularly, for example `tar czf proposal-bot-data-$(date +%F).tar.gz -C /opt/proposal-bot data`. Each file is written atomically, so copying while the app runs is safe.
- **Health check:** `GET /api/health` responds without sign-in.
The Drafts **Refresh** button loads coworkers' newly saved projects, and the list also refreshes when the browser regains focus. If two people edit the same project, the later save is rejected instead of overwriting the first. Personal template and preset preferences live in each person's browser; saved projects do not.
The per-project save limit is 100 MB. The largest current project is about 47 MB.
Run only one Proposal Bot container against a given `data` folder.
## Local development
Run `node server.js` (Node.js 22 or newer) and open `http://127.0.0.1:4181/`. Sign-in is off unless `PROPOSAL_BOT_AUTH_REQUIRED=true` is set. The InDesign export needs Chrome, Edge, or Chromium installed locally. Stop the Docker container first if it uses the same `data` folder.