add production readiness and recovery assets
This commit is contained in:
@@ -3,8 +3,12 @@ RUN addgroup -S app && adduser -S -G app app
|
||||
WORKDIR /srv
|
||||
COPY index.html /srv/index.html
|
||||
COPY styles.css /srv/styles.css
|
||||
COPY config.js /srv/config.js
|
||||
COPY asset-manifest.json /srv/asset-manifest.json
|
||||
COPY app.js /srv/app.js
|
||||
COPY healthz /srv/healthz
|
||||
COPY health.html /srv/health.html
|
||||
COPY error.html /srv/error.html
|
||||
RUN chown -R app:app /srv
|
||||
USER app
|
||||
EXPOSE 8080
|
||||
|
||||
+24
-5
@@ -4,14 +4,25 @@ Self-contained static frontend for the Prospect Platform API. There is no bundle
|
||||
|
||||
## Configure and run
|
||||
|
||||
The API base is configurable before `app.js` runs:
|
||||
The public runtime configuration is loaded from `config.js` before `app.js`. It contains no credentials and may safely be replaced during deployment:
|
||||
|
||||
```html
|
||||
<script>window.API_BASE = 'http://127.0.0.1:8000';</script>
|
||||
<script src="app.js"></script>
|
||||
```js
|
||||
window.__PROSPECT_CONFIG__ = Object.freeze({ apiBase: 'https://api.example.invalid', assetVersion: 'phase-15' });
|
||||
```
|
||||
|
||||
If not set, the UI uses `localStorage.prospect_api_base` when present; otherwise it targets the same origin. The dashboard requires an authenticated API session and shows the login screen until `/api/v1/auth/me` succeeds.
|
||||
If `apiBase` is empty, the UI uses `window.API_BASE`, then `localStorage.prospect_api_base` when present, and otherwise targets the same origin. The dashboard requires an authenticated API session and shows the login screen until `/api/v1/auth/me` succeeds. Do not put tokens, passwords, or private keys in `config.js`.
|
||||
|
||||
`asset-manifest.json` records the public entrypoints, cache-busting version, and SHA-256 digests for release verification. The HTML references the static assets with the `phase-15` version query string; update those references and regenerate the manifest when changing the release version.
|
||||
|
||||
## Deployment readiness checks
|
||||
|
||||
Serve this directory from the intended static-server root, then run:
|
||||
|
||||
```sh
|
||||
node scripts/smoke-deployment.mjs http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
The smoke script checks HTTP delivery for the manifest-listed assets, health/error pages, `healthz`, expected markers, and common hardcoded-secret patterns. It validates static readiness only; it does not deploy the app or prove API/production availability.
|
||||
|
||||
## Phase 3 UI contract
|
||||
|
||||
@@ -133,3 +144,11 @@ Every suggestion must display its evidence citations, tenant-scoped evidence IDs
|
||||
AI output must be visibly labeled **AI suggestion — human review required** and remain read-only until an authorized human explicitly approves it. Approval must show the proposed change, citations/hash, freshness, tenant scope, and safe reason; rejection and expiry must be available. The UI must require re-review when the evidence hash or policy version changes and must display partial/failed approval rather than implying persistence. Approval does not authorize contact or verification.
|
||||
|
||||
No Phase 13 control may send email/SMS, probe SMTP, create a campaign, schedule follow-up, alter pipeline/interactions/outcomes as if communication occurred, merge records, acquire a domain, or perform autonomous CRM/outreach actions. The browser must not hide or export suppressed data as eligible, and exports/reports must retain safe AI provenance and redaction labels where applicable. Production remains limited until browser/API tests cover citations and hash mismatch, redaction, fallback boundaries, approval/rejection, stale/conflicting evidence, suppression precedence, tenant non-disclosure, and no-autonomy controls; the current Compose stack has no configured AI provider.
|
||||
|
||||
## Phase 15 deployment and readiness
|
||||
|
||||
The web image is a portable static server: it runs as a non-root user, serves only the files copied into `/srv`, and exposes `/healthz`. Virtualmin is responsible for DNS, HTTPS certificates, reverse-proxy routing, firewall rules, and any access control around the site. Set the API base deliberately for the deployed origin; do not put credentials or provider secrets in HTML, JavaScript, local storage, image layers, or `.env` files. `CORS_ORIGINS` must exactly match the approved HTTPS origin rather than a broad wildcard.
|
||||
|
||||
`/healthz` is an unauthenticated process/liveness check. API `/api/v1/health/ready` checks SQLite readiness, but neither endpoint proves tenant authorization, backup validity, or external dependencies. Route traffic only after the web and API containers report `healthy`, the HTTPS proxy reaches the intended containers, and an authenticated browser/API smoke test succeeds. The browser must never be used to test or initiate outbound prospect/provider traffic; `AUTOMATED_OUTREACH_ENABLED=false` remains visible as the no-send default.
|
||||
|
||||
For releases, validate the exact static image and API image together, capture image digests and configuration revision, and retain the prior pair for rollback. If a schema/data migration is involved, the API owner must complete backup/restore and migration validation before the web image is promoted. The current client has no service-worker cache or migration logic; stale browser tabs must be refreshed after a release, and Virtualmin/CDN caching must not serve an old API contract indefinitely. SQLite, HTTP-only local Compose, lack of a readiness endpoint, and lack of a production asset/CDN pipeline are explicit limitations.
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
/* ProspectOS frontend MVP. Configure before loading with window.API_BASE = 'http://127.0.0.1:8000'; */
|
||||
(() => {
|
||||
'use strict';
|
||||
const API_BASE = (window.API_BASE || localStorage.getItem('prospect_api_base') || '').replace(/\/$/, '');
|
||||
const publicConfig = window.__PROSPECT_CONFIG__ || {};
|
||||
const API_BASE = (publicConfig.apiBase || window.API_BASE || localStorage.getItem('prospect_api_base') || '').replace(/\/$/, '');
|
||||
const endpoint = (path) => `${API_BASE}${path}`;
|
||||
let prospects = [], selectedId = null, selectedDetail = null, currentUser = null;
|
||||
let page = 1, pageSize = 10, hasNextPage = false, savedFilters = [], reviewQueue = [], selectedReviewIds = new Set();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"version": "phase-15",
|
||||
"entrypoints": ["config.js", "app.js", "styles.css"],
|
||||
"publicAssets": ["index.html", "health.html", "error.html", "healthz"],
|
||||
"integrity": {
|
||||
"config.js": "sha256-20f3020432436dcccdbfc86fd56a6a6a49b71fc512a1e434187a5ddc134fda1c",
|
||||
"app.js": "sha256-fc12f49012bb1329ffdd7bdcf655e9ab9097eaf1e0cc73cd0442b19fd57a0374",
|
||||
"styles.css": "sha256-7340dccc648fa917fb497ceeba7286d9c4712b6552960de54cef759cdbda6de4",
|
||||
"index.html": "sha256-37a9d5e2a81941c3c9f9bd3f42edf33a40565bf197cf675389d250dd60eb69db",
|
||||
"health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81",
|
||||
"error.html": "sha256-f3cc28d2dfc9e8af112d0257c6ee47b9e9146e5da8590a754dd589bf43702aaf",
|
||||
"healthz": "sha256-dc51b8c96c2d745df3bd5590d990230a482fd247123599548e0632fdbf97fc22"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* Public, non-secret runtime configuration. Replace this file at deploy time if needed. */
|
||||
window.__PROSPECT_CONFIG__ = Object.freeze({
|
||||
apiBase: '',
|
||||
assetVersion: 'phase-15'
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>ProspectOS · Temporarily unavailable</title><style>body{font:16px system-ui,sans-serif;margin:4rem auto;max-width:42rem;padding:0 1.5rem;color:#172033;background:#f7f8fb}main{background:#fff;border:1px solid #e7eaf1;border-radius:12px;padding:2rem}h1{color:#b84d55}a{color:#6756e8}</style></head><body><main><p>ProspectOS frontend</p><h1>Something went wrong</h1><p>This page could not be loaded. Please try again or return to the <a href="/">workspace</a>.</p></main></body></html>
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>ProspectOS · Healthy</title><style>body{font:16px system-ui,sans-serif;margin:4rem auto;max-width:42rem;padding:0 1.5rem;color:#172033;background:#f7f8fb}main{background:#fff;border:1px solid #e7eaf1;border-radius:12px;padding:2rem}h1{color:#16845b}code{background:#e5f7ef;padding:.15rem .35rem;border-radius:4px}</style></head><body><main><p>ProspectOS frontend</p><h1>Ready</h1><p>Static asset delivery is available. API availability is checked separately by the application.</p><p><code>health.html</code></p></main></body></html>
|
||||
+3
-2
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ProspectOS · Pipeline intelligence</title>
|
||||
<meta name="description" content="Prospect discovery and review dashboard">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="styles.css?v=phase-15">
|
||||
</head>
|
||||
<body>
|
||||
<section class="login-screen" id="loginScreen" aria-labelledby="loginTitle">
|
||||
@@ -117,6 +117,7 @@
|
||||
<div class="merge-dialog" id="mergeDialog" hidden role="dialog" aria-modal="true" aria-labelledby="mergeDialogTitle">
|
||||
<div class="merge-dialog-card"><div class="panel-heading"><div><p class="eyebrow">REVIEW REQUIRED</p><h2 id="mergeDialogTitle">Confirm merge</h2></div><button class="icon-button" id="cancelMergeBtn" type="button" aria-label="Close merge confirmation">×</button></div><p id="mergeDialogCopy"></p><div class="merge-warning"><strong>This action is reversible.</strong> The merge will be recorded in history and can be reversed later.</div><p id="mergeDialogMessage" class="form-message" role="alert" aria-live="polite"></p><div class="merge-dialog-actions"><button class="button ghost" id="cancelMergeBtnSecondary" type="button">Cancel</button><button class="button primary" id="confirmMergeBtn" type="button">Confirm merge</button></div></div>
|
||||
</div>
|
||||
<script src="app.js"></script>
|
||||
<script src="config.js?v=phase-15"></script>
|
||||
<script src="app.js?v=phase-15"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const base = (process.argv[2] || 'http://127.0.0.1:8080').replace(/\/$/, '');
|
||||
const manifestPath = resolve(process.cwd(), 'asset-manifest.json');
|
||||
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
||||
const assets = [...new Set([...(manifest.entrypoints || []), ...(manifest.publicAssets || [])])];
|
||||
const failures = [];
|
||||
const secretPattern = /(api[_-]?key|secret|token|password|private[_-]?key|authorization)\s*[:=]\s*["'][^"']+/i;
|
||||
|
||||
async function check(path, expected = {}) {
|
||||
const response = await fetch(`${base}/${path}`);
|
||||
const body = await response.text();
|
||||
if (!response.ok) failures.push(`${path}: HTTP ${response.status}`);
|
||||
if (expected.contentType && !response.headers.get('content-type')?.includes(expected.contentType)) failures.push(`${path}: unexpected content type`);
|
||||
if (expected.marker && !body.includes(expected.marker)) failures.push(`${path}: missing marker ${expected.marker}`);
|
||||
if (expected.integrity) {
|
||||
const digest = `sha256-${createHash('sha256').update(body).digest('hex')}`;
|
||||
if (digest !== expected.integrity) failures.push(`${path}: integrity mismatch`);
|
||||
}
|
||||
if (secretPattern.test(body)) failures.push(`${path}: possible hardcoded secret`);
|
||||
console.log(`${response.ok ? 'PASS' : 'FAIL'} ${path} (${response.status})`);
|
||||
}
|
||||
|
||||
const expectations = {
|
||||
'index.html': { contentType: 'text/html', marker: 'ProspectOS' },
|
||||
'health.html': { contentType: 'text/html', marker: 'Ready' },
|
||||
'error.html': { contentType: 'text/html', marker: 'Something went wrong' },
|
||||
healthz: { marker: 'ok' }
|
||||
};
|
||||
for (const asset of assets) await check(asset, { ...expectations[asset], integrity: manifest.integrity?.[asset] });
|
||||
if (failures.length) { console.error(`\n${failures.length} deployment smoke check(s) failed`); for (const failure of failures) console.error(`- ${failure}`); process.exitCode = 1; } else console.log(`\nDeployment smoke checks passed for ${base}`);
|
||||
Reference in New Issue
Block a user