Backend Aug 22-25: AI analysis, welcome-packet templating, LetterStream, DocuSeal, staff RBAC + tier gate

- analysis.py: deterministic claim scorer + /analyze /approve /letter /advance-tier endpoints (auto-runs on intake)
- packet.py + packet_fields.json: welcome-packet templating engine (6 onboarding docs, field catalog)
- letterstream.py + letters.py: certified-mail send pipeline + letter lifecycle (webhook verified)
- docuseal.py: DocuSeal signing integration
- staff.py/models.py/schema.sql/auth.py: approval actor from staff key, tier gate (APPROVED+ACTIVE+onboarding docs), onboarding_docs table
- frontend/: dependency-free static portal (intake, magic-link login/verify, dashboard)
- landing-mockups/: 4 design-stance mockups + favicons
- legal/: aup/privacy/sms-terms/terms HTML
- docs/: letter-queue scope, letterstream API contract, 6 welcome-packet templates
- review-dre-landing-2026-08-21.md: 3-variant landing feedback sprint
- compliance/DRE_Compliance_Manual.md: updated

Source synced from deployed /opt/dre-portal/app/ (was 4 days ahead of git).
This commit is contained in:
root
2026-08-26 02:26:33 -04:00
parent 7a5603b495
commit 7a62b0b340
46 changed files with 11004 additions and 35 deletions
+208
View File
@@ -0,0 +1,208 @@
/* ==========================================================================
DRE Customer Portal — shared JS helpers
Dependency-free. Relative /api/* calls only (same-origin; Caddy proxies).
========================================================================== */
(function (global) {
"use strict";
var SESSION_KEY = "dre_session_token";
/**
* Perform a JSON fetch against the API.
* @param {string} path - relative API path, e.g. "/api/claims"
* @param {object} opts - { method, body, auth, headers }
* Returns a Promise resolving to { ok, status, data } where data is the
* parsed JSON body (success payload or {error:{code,message}}).
*/
function apiFetch(path, opts) {
opts = opts || {};
var headers = Object.assign({}, opts.headers || {});
var fetchOpts = { method: opts.method || "GET", headers: headers };
if (opts.body !== undefined && !(opts.body instanceof FormData)) {
headers["Content-Type"] = "application/json";
fetchOpts.body = JSON.stringify(opts.body);
} else if (opts.body instanceof FormData) {
fetchOpts.body = opts.body; // browser sets multipart boundary
}
if (opts.auth) {
var token = getSessionToken();
if (token) {
headers["Authorization"] = "Bearer " + token;
}
}
return fetch(path, fetchOpts)
.then(function (res) {
return res
.json()
.catch(function () {
return {};
})
.then(function (data) {
return { ok: res.ok, status: res.status, data: data };
});
})
.catch(function (err) {
return {
ok: false,
status: 0,
data: {
error: {
code: "network_error",
message: "Could not reach the server. Check your connection and try again.",
},
},
};
});
}
function getErrorMessage(result, fallback) {
if (result && result.data && result.data.error && result.data.error.message) {
return result.data.error.message;
}
return fallback || "Something went wrong. Please try again.";
}
function getErrorCode(result) {
if (result && result.data && result.data.error && result.data.error.code) {
return result.data.error.code;
}
return null;
}
function getSessionToken() {
try {
return localStorage.getItem(SESSION_KEY);
} catch (e) {
return null;
}
}
function setSessionToken(token) {
try {
localStorage.setItem(SESSION_KEY, token);
} catch (e) {
/* localStorage unavailable; session will not persist across reload */
}
}
function clearSessionToken() {
try {
localStorage.removeItem(SESSION_KEY);
} catch (e) {
/* ignore */
}
}
function requireSessionOrRedirect(loginUrl) {
var token = getSessionToken();
if (!token) {
window.location.href = loginUrl || "login.html";
return null;
}
return token;
}
function formatCentsUSD(cents) {
var n = typeof cents === "number" ? cents : parseInt(cents, 10) || 0;
return "$" + (n / 100).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatDate(iso) {
if (!iso) return "--";
try {
var d = new Date(iso);
if (isNaN(d.getTime())) return iso;
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
} catch (e) {
return iso;
}
}
function formatDateTime(iso) {
if (!iso) return "--";
try {
var d = new Date(iso);
if (isNaN(d.getTime())) return iso;
return d.toLocaleString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
} catch (e) {
return iso;
}
}
function formatBytes(n) {
if (!n && n !== 0) return "";
if (n < 1024) return n + " B";
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
return (n / (1024 * 1024)).toFixed(1) + " MB";
}
function showAlert(el, message, isVisible) {
if (!el) return;
el.textContent = message || "";
if (isVisible === false) {
el.classList.remove("is-visible");
} else {
el.classList.add("is-visible");
}
}
function hideAlert(el) {
if (!el) return;
el.classList.remove("is-visible");
el.textContent = "";
}
function setLoading(button, loading, loadingText, normalText) {
if (!button) return;
if (loading) {
button.disabled = true;
button.dataset.originalText = button.dataset.originalText || button.innerHTML;
button.innerHTML = '<span class="dre-spinner"></span> ' + (loadingText || "Please wait...");
} else {
button.disabled = false;
button.innerHTML = normalText || button.dataset.originalText || button.innerHTML;
}
}
function badgeClass(status) {
return "dre-badge dre-badge-" + (status || "").toLowerCase();
}
// Escape helper for any spot where we must build markup with dynamic text.
// Prefer textContent everywhere; this exists only as a defensive fallback.
function escapeHtml(str) {
var div = document.createElement("div");
div.textContent = str === undefined || str === null ? "" : String(str);
return div.innerHTML;
}
global.DRE = {
SESSION_KEY: SESSION_KEY,
apiFetch: apiFetch,
getErrorMessage: getErrorMessage,
getErrorCode: getErrorCode,
getSessionToken: getSessionToken,
setSessionToken: setSessionToken,
clearSessionToken: clearSessionToken,
requireSessionOrRedirect: requireSessionOrRedirect,
formatCentsUSD: formatCentsUSD,
formatDate: formatDate,
formatDateTime: formatDateTime,
formatBytes: formatBytes,
showAlert: showAlert,
hideAlert: hideAlert,
setLoading: setLoading,
badgeClass: badgeClass,
escapeHtml: escapeHtml,
};
})(window);