uhlwoogiandClaude Opus 5.5 d1c9b7bebb Log Microsoft's error when the sign-in token exchange fails
The AADSTS code and description were discarded, leaving only a generic
"could not verify" message. Log them (not to the browser) so a bad secret,
SPA platform, or redirect mismatch can be diagnosed from container logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:48:17 +00:00
2026-10-02 14:49:22 -05:00
2026-10-02 14:49:22 -05:00
2026-10-02 14:49:22 -05:00
2026-10-02 14:49:22 -05:00
2026-10-02 14:49:22 -05:00
2026-10-02 14:49:22 -05:00
2026-10-02 14:49:22 -05:00

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

NPMplus handles HTTPS. It runs with host networking, so the container publishes its port on the host's loopback address only (127.0.0.1:4181). NPMplus can reach that port; other machines cannot.

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 NPMplus 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.
    • PROPOSAL_BOT_PORT: leave at 4181 unless that port is already in use on the host.
    • 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 NPMplus proxy host

  • Domain: the Proposal Bot hostname

  • Forward: scheme http, hostname 127.0.0.1, port 4181 (or your PROPOSAL_BOT_PORT)

  • 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 NPMplus 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.

S
Description
No description provided
Readme
2.9 GiB
0 Stars 1 Watchers 0 Forks
Languages
JavaScript 55.4%
CSS 39.9%
PowerShell 2.6%
HTML 1.7%
Dockerfile 0.4%