Add multi-project support: projects entity, picker home page, project context

Projects become the top-level container; SOPs and Work Packages belong to one.

Backend:
- New projects table + CRUD (/api/projects).
- sops.project_id (FK, cascade) and work_packages.project_id added;
  list/latest/metrics endpoints accept a project_id filter.

Front end (now under html/):
- project-data.js: shared API-first ProjectData adapter with localStorage
  fallback + active-project helpers.
- Home page: removed "About This Suite"; added a Project picker (create /
  use sample / select). Tool cards stay hidden until a project is active and
  carry &project=<id>; hero shows the active project.
- Suite reads ?project, resolves it, shows it in the header, and prefills the
  SOP project fields; passes &project into the WP-creator iframe.
- WP creator stamps projectId onto saved packages.

SOP/WP localStorage is not yet namespaced per project (next step).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 15:39:45 -07:00
parent 4d111d608d
commit 3c40b58ff8
9 changed files with 437 additions and 39 deletions

View File

@@ -43,6 +43,7 @@ const EXAMPLE_PKG = {"number":"WP02-1P-ELEC-Conduit","status":"Issue","subject":
// ── STATE ────────────────────────────────────────────────────────────────────
let SOP=null, editingId=null, numberDirty=false;
let activeProjectId=''; // set at boot from ?project=<id>; stamped onto saved WPs for the API
let pkgMaterials=[], pkgAttach=[], pkgConstraints=[], pkgSignoffs=[], pkgHolds=[], pkgWorkSteps=[], pkgAssets=[];
let pkgOverrides={}; // {fieldId: reason} for SOP-locked fields that were edited
let numberDims={}; // {Sector:'', Discipline:''} dimensions that build the WP number (comment 3)
@@ -631,6 +632,7 @@ function collectPackage(){
const prev=editingId?savedPackages.find(p=>p.id===editingId):null; // carry instance/split linkage across edits
return {
id: editingId || ('wp_'+Date.now().toString(36)+Math.random().toString(36).slice(2,5)),
projectId: (prev&&prev.projectId) || activeProjectId || '',
instanceOf: prev?prev.instanceOf:undefined, instanceLabel: prev?prev.instanceLabel:undefined,
parentNumber: prev?prev.parentNumber:undefined, split: prev?prev.split:undefined, children: prev?prev.children:undefined,
number:gv('wp_number'), status:getRadio('status')||'Draft', subject:gv('wp_subject'),
@@ -1016,6 +1018,7 @@ loadStore();
// and prefer the SOP the Suite just completed (persisted to localStorage).
const params = new URLSearchParams(location.search);
if(params.get('embedded')) document.body.classList.add('embedded');
activeProjectId = params.get('project') || (typeof ProjectData!=='undefined' && ProjectData.getActiveId && ProjectData.getActiveId()) || '';
try {
const raw = localStorage.getItem('wp_suite_sop');
if(raw){