Add Python/FastAPI + PostgreSQL backend (Phase 1)

- server/: FastAPI app with SQLAlchemy models for sops, work_packages, comments
- Endpoints for SOP/WP upsert+list+get+delete and comment create+list;
  /api/feedback kept as an alias so the existing client keeps working
- Portable across engines (PostgreSQL prod, SQLite dev fallback)
- requirements.txt, .env.example, and server/README.md (Postgres + systemd)
- NGINX now proxies /api/ to the API (replaces the Power Automate hop;
  comments persist to SQL)
- Rewrite DEPLOYMENT.md for the API + database architecture
- Add .gitignore for venv/.env/sqlite

Phase 2 (wire the client apps to the API) is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 10:58:52 -07:00
parent 3d4402c88c
commit a33b777aca
10 changed files with 563 additions and 155 deletions

View File

@@ -1,153 +1,57 @@
# Deployment & Feedback Collection
# Deployment
The Work Package Suite is a **static client-side app** — plain HTML, CSS, and
JavaScript. There is no build step and no database.
The Work Package Suite has two parts:
## Hosting it behind the firewall
- a **static front end** (plain HTML/CSS/JS — no build step), and
- a **Python API** (FastAPI) backed by **PostgreSQL**, which stores the project
SOPs, Work Packages, and comments so they are shared across users instead of
living in each person's browser.
Copy the whole folder to any internal web server and serve it over HTTP(S):
- **IIS / Apache / nginx** — drop the files in the site root. `index.html` is the
entry point.
- **SharePoint / network share** — works too, as long as the files are served
over `http(s)://` (not opened as `file://...`). Serving over HTTP makes
`localStorage` and the embedded Work Package Creator (an `<iframe>`) behave
reliably.
The app makes **no outbound internet calls** — the logo and all scripts are
local, and the previous Google-Fonts dependency has been removed (fonts now fall
back to system UI fonts). So it runs fully air-gapped behind a corporate
firewall.
## Where data lives
By default **everything is stored in each user's own browser** (`localStorage`):
the SOP configuration, the saved Work Packages, the usage logs, and all feedback
/ comments. This means:
- Data is **per-user and per-device** — it is not shared between people, and
clearing browser data erases it.
- Nothing is transmitted anywhere unless you enable central collection (below).
## Feedback collection
There are two layers, and they work together.
### 1. Export / Import (no server required — works today)
Every feedback surface has **Export** and **Import** buttons:
- Home page → *Leave Feedback* panel
- SOP Configuration → *Step Comments* (header button)
- Work Package Creator → *Comments* drawer
A reviewer clicks **Export** to download a JSON file and sends it to you; you
click **Import** on your machine to merge everyone's feedback together (imports
de-duplicate, so re-importing is safe). This needs zero infrastructure and works
behind any firewall.
### 2. Central auto-collection (optional — flip on when hosting is known)
To also gather every submission automatically into one place, set a single value
in [`feedback-config.js`](feedback-config.js):
```js
window.FEEDBACK_ENDPOINT = 'https://your-endpoint-url';
```
browser → NGINX ──serves──> static site (index.html, …)
└─proxy /api/─> Python API (uvicorn/gunicorn :8000) → PostgreSQL
```
When set, each submission is additionally `POST`ed as JSON to that URL (saving
locally still happens, so a failed/disabled endpoint never loses feedback). The
endpoint can be either of:
Everything runs inside your firewall; the app makes **no outbound internet
calls** (the logo and scripts are local and the old Google-Fonts dependency was
removed).
#### Option A — a small backend on your host
Any server that can run code and append the request body to a file you can
download. Example (Node/Express):
## 1. Front end (NGINX)
```js
const express = require('express');
const fs = require('fs');
const app = express();
app.use(express.json());
app.post('/feedback', (req, res) => {
fs.appendFileSync('feedback.jsonl', JSON.stringify(req.body) + '\n');
res.sendStatus(204);
});
app.listen(8080);
```
Copy the project files to a web root and serve them over HTTPS. The provided
[`nginx-wp-suite.conf`](nginx-wp-suite.conf) serves the static files and proxies
`/api/` to the Python API. Set `server_name`, the `ssl_certificate` paths, and
`root`, then `sudo nginx -t && sudo systemctl reload nginx`.
Each line of `feedback.jsonl` is one submission; download it anytime. (PHP/
Python/ASP.NET equivalents are a few lines too.)
Serving over real HTTP(S) (not `file://`) also makes the embedded Work Package
Creator (`<iframe>`) and any browser-side caching behave reliably.
#### Option B — Internal NGINX reverse proxy → Power Automate (the chosen setup)
## 2. API + database
The browser posts to a **same-origin** path `/api/feedback`; NGINX forwards that
to the Power Automate trigger. This avoids CORS entirely and keeps the secret
trigger URL off the client. `FEEDBACK_ENDPOINT` is already set to
`/api/feedback`, and [`nginx-wp-suite.conf`](nginx-wp-suite.conf) contains the
full server block.
Full setup — PostgreSQL, the systemd service, and the endpoint reference — is in
[`server/README.md`](server/README.md). In short:
**On the NGINX box (one-time, server admin):**
1. Copy the project files to the web root (e.g. `/var/www/wp-suite`).
2. Install [`nginx-wp-suite.conf`](nginx-wp-suite.conf) (e.g. into
`/etc/nginx/conf.d/`), and set `server_name`, the `ssl_certificate` paths
(internal cert), and the `root`.
3. In the `location = /api/feedback` block, replace the `proxy_pass` URL and the
`Host` header with your real trigger URL / region host (keep the full query
string incl. `sig=`).
4. `sudo nginx -t && sudo systemctl reload nginx`.
1. Create the `wpsuite` Postgres database/user.
2. `pip install -r server/requirements.txt` into a venv.
3. Set `DATABASE_URL` and run the API as a systemd service on `127.0.0.1:8000`.
4. Tables are created automatically on first start.
> Two NGINX details that matter: `proxy_ssl_server_name on;` (SNI is required for
> `*.logic.azure.com` or the TLS handshake fails) and the `Host` header set to
> the Azure region host. Both are already in the provided config.
Interactive API docs are at `/api/docs` once it's running.
**In Power Automate:**
1. Create a flow with the **"When an HTTP request is received"** trigger.
2. Set its **Request Body JSON Schema** to:
## 3. Comments / feedback
```json
{
"type": "object",
"properties": {
"app": { "type": "string" },
"page": { "type": "string" },
"submittedAt": { "type": "string" },
"type": { "type": "string" },
"name": { "type": "string" },
"author": { "type": "string" },
"text": { "type": "string" },
"step": { "type": "integer" },
"view": { "type": "string" },
"timestamp": { "type": "string" },
"ts": { "type": "string" },
"id": { "type": "string" },
"clientId": { "type": "string" }
}
}
```
> `name` is used by the home/SOP forms, `author` by the Work Package Creator.
> Map both into one "Submitted by" column with an expression like
> `coalesce(triggerBody()?['name'], triggerBody()?['author'])`.
3. Add an action — **Create item** (SharePoint list) or **Add a row into a
table** (Excel / Dataverse) — mapping the fields above.
4. Save; copy the generated **HTTP POST URL** into `web.config`
(`POWER_AUTOMATE_TRIGGER_URL`).
5. A Power App (or just the list/Excel) reads that store to show live comments.
Every feedback surface (home *Leave Feedback*, SOP *Step Comments*, WP *Comments*)
posts to `/api/feedback`, which the API stores in the `comments` table. The
**Export / Import** buttons remain as an offline fallback — a reviewer can export
a JSON file and someone can import/merge it — but with the API running, comments
are collected centrally with no manual steps.
Chain: `browser → /api/feedback (NGINX proxy) → Power Automate → SharePoint/Dataverse → Power App`.
> The earlier Power Automate route is **no longer needed** — comments go straight
> to Postgres. If you still want a Power App view, point a Power App at the
> Postgres `comments` table via the on-prem data gateway, or have a flow read the
> table; no change to this app is required.
The "downloadable file" is then just the Excel/SharePoint list, viewable live or
exported — all inside your corporate cloud.
### CORS note
If the endpoint is on a **different origin** than the site, it must return CORS
headers allowing the site's origin (e.g.
`Access-Control-Allow-Origin: https://wp-suite.yourcompany.local`). A Power
Automate HTTP trigger and a same-host backend both handle this cleanly; a
same-origin backend needs no CORS at all.
## Feedback payload shape
Each POST body looks like:
### Comment payload shape
```json
{
@@ -161,4 +65,17 @@ Each POST body looks like:
}
```
`type` is one of `home_feedback`, `sop_step_comment`, or `wp_review_comment`.
`type` is one of `home_feedback`, `sop_step_comment`, or `wp_review_comment`. The
API maps `name`/`author` → the comment author and keeps any extra fields in the
row's `extra` JSON column.
## Data model (PostgreSQL)
| Table | Holds | Key columns |
|-------|-------|-------------|
| `sops` | project SOP baselines | `name`, `number`, `complete`, `data` (full SOP JSON) |
| `work_packages` | individual IWPs | `sop_id`, `number`, `subject`, `type`, `status`, `data` (full WP JSON) |
| `comments` | feedback from any page | `source`, `sop_id`, `wp_id`, `step`, `author`, `text` |
The complete client document is stored verbatim in each row's `data` column;
frequently-listed fields are promoted to real columns for filtering.