Store SOPs and Work Packages in the DB (shared across users)

Reintegrates C-West8's "storing data in DB instead of client only"
(commit e5102446 on shared-data) on top of the BIM / per-package work.
localStorage becomes a per-browser cache; the server is authoritative.

- project-data.js: pullProject() hydrates the apps' existing localStorage
  keys from the API on load; pushSOP()/pushWP()/removeWP() write through
  on save/delete. WPs store the whole flat object in `data`, so BIM
  fields, kind, and projectLinks round-trip intact.
- index.html: home pulls the project before showing SOP status; feedback
  loads from /api/comments (server-authoritative, local fallback).
- work-package-suite-app.js: pull-then-restore on boot; completeSOP
  pushes the SOP to the server.
- wp-creation-app.js: save/duplicate/issue/setStatus push; delete/clear
  remove; boot pulls from the server first, then boots off the cache.
- server/app.py: /api/sops and /api/wps take full=true to return the
  data JSON for one-request hydration (list stays lean by default).

Co-Authored-By: C-West8 <125926137+C-West8@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 10:58:33 -07:00
parent ca4176ac00
commit afdc815fb4
5 changed files with 188 additions and 31 deletions

View File

@@ -595,7 +595,10 @@
if(info) info.innerHTML = `<div class="proj-active">✓ Active project: <strong>${esc(active.name||'')}</strong>${active.number?' ('+esc(active.number)+')':''}
&nbsp;<button class="link-like" onclick="clearActiveProject()">change</button></div>`;
reflectSOPStatus(active);
// Pull the project's shared SOP/WPs from the server into the local cache
// first, so the SOP "Complete / Review" status reflects what other users did.
if(ProjectData.pullProject){ ProjectData.pullProject(active.id).then(()=>reflectSOPStatus(active)).catch(()=>reflectSOPStatus(active)); }
else reflectSOPStatus(active);
}
function clearActiveProject(){ ProjectData.setActive(null); renderProjectPicker(); applyActiveProject(); }
@@ -704,22 +707,36 @@
r.readAsText(f);
}
function loadComments() {
const saved = localStorage.getItem('wp_suite_index_comments');
if (saved) allComments = JSON.parse(saved);
function renderComments() {
const list = document.getElementById('comments-list');
if (allComments.length === 0) {
list.innerHTML = '<div style="color: var(--cds-text-secondary); font-style: italic; font-size: 12px;">No feedback yet. Be the first to share!</div>';
} else {
list.innerHTML = allComments.map(c => `
<div class="comment-item">
<div class="comment-meta"><strong>${c.name}</strong> • ${c.timestamp}</div>
<div class="comment-text">${c.text.replace(/</g,'&lt;').replace(/>/g,'&gt;')}</div>
<div class="comment-meta"><strong>${(c.name||'Anonymous').replace(/</g,'&lt;')}</strong> • ${c.timestamp||''}</div>
<div class="comment-text">${(c.text||'').replace(/</g,'&lt;').replace(/>/g,'&gt;')}</div>
</div>
`).join('');
}
}
function loadComments() {
// Server is authoritative (so feedback is shared across users); fall back to
// the local cache if the API is unreachable.
const saved = localStorage.getItem('wp_suite_index_comments');
if (saved) { try { allComments = JSON.parse(saved) || []; } catch(e) { allComments = []; } }
renderComments();
fetch('/api/comments?source=home_feedback', { headers: { 'Accept': 'application/json' } })
.then(r => r.ok ? r.json() : null)
.then(rows => {
if (Array.isArray(rows)) {
allComments = rows.map(c => ({ name: c.author, text: c.text, timestamp: c.created_at ? new Date(c.created_at).toLocaleString() : '' }));
renderComments();
}
})
.catch(() => {});
}
</script>
</body>
</html>