From b8750e50c5aa51dfb376b2b5d8cdc3fec39b0bc5 Mon Sep 17 00:00:00 2001 From: TheRON Date: Sat, 13 Jun 2026 08:09:39 -0400 Subject: [PATCH] Initial push --- hubzilla/addon/dsc01/DSC-RENDERER-SPEC.md | 191 ++++++++++++++++++++ hubzilla/addon/dsc01/config.json.template | 6 + hubzilla/addon/dsc01/dsc01.php | 57 +++--- hubzilla/addon/dsc01/dsc01_renderer.php | 201 ++++++++++++++++++++++ hubzilla/addon/dsc01/dsc01_spool.php | 153 ++++++++++++++++ 5 files changed, 587 insertions(+), 21 deletions(-) create mode 100644 hubzilla/addon/dsc01/DSC-RENDERER-SPEC.md create mode 100644 hubzilla/addon/dsc01/config.json.template create mode 100644 hubzilla/addon/dsc01/dsc01_renderer.php create mode 100644 hubzilla/addon/dsc01/dsc01_spool.php diff --git a/hubzilla/addon/dsc01/DSC-RENDERER-SPEC.md b/hubzilla/addon/dsc01/DSC-RENDERER-SPEC.md new file mode 100644 index 0000000..1bca45f --- /dev/null +++ b/hubzilla/addon/dsc01/DSC-RENDERER-SPEC.md @@ -0,0 +1,191 @@ +# DSC-01 Renderer Spec — Diagnostic Surface Categories + +**Version:** 1.0 +**Companion to:** VS-RENDERER-SPEC.md (Vital Signs) +**Source taxonomy:** DSC-development-map.md + +--- + +## 1. Purpose + +dsc01 presents the ten Active Diagnostic Surface Categories (DSC-01 through +DSC-10) as a **homeowner-applied checklist** attached to a case record. It is +architecturally distinct from vs01: + +| | vs01 (Vital Signs) | dsc01 (Categories) | +|---|---|---| +| Cardinality | One record per association | Many records per association (one per case) | +| Authored by | Operator defines schema; participants/professionals/operator fill perspectives | Operator defines taxonomy only; homeowner selects | +| Record shape | Per-VS-code form submission | Single record: multi-select DSC codes + narrative | +| Mutability | Public-record perspective may be immutable after submit | Each case record is its own append-only entry | +| Storage | vs01 spool → orchestrator | dsc01 spool → orchestrator (separate receiver/records file) | + +The ten DSC schema files under `diagnostic-categories/` are **read-only +reference data** to the renderer. Homeowners never create, edit, or remove +DSC codes, fields, or categories — that is operator-only, done by editing +the schema JSON files and pushing through the standard tarball workflow. + +--- + +## 2. Schema file shape + +Each `diagnostic-categories/DSC-0X-*.json` contains a single `_meta` block: + +```json +{ + "_meta": { + "code": "DSC-01", + "title": "Assessments", + "cai_reference": "Assessments", + "homeowner_relevance": "Critical", + "development_status": "Active", + "summary": "...", + "vital_sign_dependencies": [ + { "code": "VS-01", "relevance": "..." } + ], + "diagnostic_questions": [ "...", "..." ] + } +} +``` + +No `fields`, `perspectives`, or `_meta.form` keys — there is nothing for a +homeowner to fill in per category. The `diagnostic_questions` array is +**homeowner-facing**: shown inline in the checklist UI to help a homeowner +decide whether a category applies to their situation. + +--- + +## 3. Record shape (dsc01 spool envelope) + +One record represents one homeowner's self-assessment for one case. It is +a standalone record type — it does **not** embed into or extend a vs01 +record. It carries a reference back to the relevant association's VS +profile via `association_slug`, the same slug key used in +`vs01/config.json`. + +```json +{ + "addon": "dsc01", + "contract_version": "1.0", + "association_slug": "kingsrow-wdca", + "association_channel_id": "123", + "submitted_at": "2026-06-13T00:00:00+00:00", + "standing": "participant", + "dsc_codes": ["DSC-01", "DSC-07"], + "narrative": "Free-text description of the homeowner's situation." +} +``` + +Field notes: + +- `dsc_codes` — array of selected DSC-0X codes, in any order, at least one + required. +- `narrative` — single free-text field for the whole record (not per-code). + Optional but encouraged. +- `standing` — result of `dsc01_access_state()` at submission time + (`operator` / `participant` / `professional` / `public`), recorded for + provenance. Public submissions are rejected before reaching the spool + (see Access section below). +- `association_slug` / `association_channel_id` — pulled from + `vs01/config.json`, same as vs01's envelope. This is the cross-reference + that lets the corpus place a DSC record against the association's VS + profile without embedding one record type inside the other. + +--- + +## 4. Access model + +dsc01 reuses `vs01_access_state()`'s logic by reading the same +`vs01/config.json` associations/groups config — already implemented in +`dsc01_access_state()`. No separate access config file for dsc01. + +- **public** — may read the category list, diagnostic questions, and the + checklist UI, but POST submissions are rejected (access wall shown, + same pattern as vs01's `vs01_access_wall()`). +- **participant** — may submit a DSC record for the association they're a + Privacy Group member of. +- **professional** — read access; submission behavior TBD (not blocking for + v1; treat as participant for now unless told otherwise). +- **operator** — full access, plus (future) a manage/review view over + submitted records, mirroring vs01's `vs01_render_manage()` stub. + +--- + +## 5. Routes + +- `/dsc01` — index. Lists the ten DSC categories with title + one-line + summary, similar to vs01's `vs01_render_index()` association list, but + here listing categories rather than associations. +- `/dsc01/{association_slug}` — landing page for that association. Shows + the checklist form (all 10 categories, each with diagnostic questions + expandable/inline) plus the narrative field and submit button. +- `/dsc01/{association_slug}` (POST) — submission handler. Validates + `dsc_codes` (at least one selected), builds the envelope (Section 3), + POSTs to orchestrator via `dsc01_post_to_orchestrator()`. +- `/dsc01/{association_slug}/manage` — operator-only, future. Review queue + for submitted DSC records (mirrors vs01's TMP-review stub). + +No per-code routes (no `/dsc01/{slug}/DSC-01`) — unlike vs01, there is no +per-category form to navigate to. + +--- + +## 6. Checklist UI + +For each of the 10 categories, in DSC-01..10 order: + +```html +
+ +
+ Diagnostic questions +
    +
  • Was the assessment properly authorized by the Board...?
  • + ... +
+
+
+``` + +After the 10 categories, a single `'; + $out .= ''; + + $out .= '
'; + $out .= ''; + $out .= '
'; + $out .= ''; + + $out .= ''; + return $out; +} + +// --------------------------------------------------------------------------- +// RENDER — READ-ONLY CHECKLIST (public access) +// --------------------------------------------------------------------------- + +function dsc01_render_checklist_readonly($categories) { + $out = '
'; + foreach ($categories as $code => $meta) { + $title = dsc01_h($meta['title'] ?? $code); + $out .= '
'; + $out .= '' . dsc01_h($code) . ' — ' . $title; + + if (!empty($meta['diagnostic_questions'])) { + $out .= '
'; + $out .= 'Diagnostic questions'; + $out .= '
    '; + foreach ($meta['diagnostic_questions'] as $q) { + $out .= '
  • ' . dsc01_h($q) . '
  • '; + } + $out .= '
'; + $out .= '
'; + } + + $out .= '
'; + } + $out .= '
'; + return $out; +} + +// --------------------------------------------------------------------------- +// RENDER — MANAGE (stub, operator only) +// --------------------------------------------------------------------------- + +function dsc01_render_manage($association_slug) { + $raw = @file_get_contents('addon/vs01/config.json'); + $cfg = $raw ? json_decode($raw, true) : []; + $assoc = $cfg['associations'][$association_slug] ?? []; + $name = dsc01_h($assoc['name'] ?? $association_slug); + + $out = '
'; + $out .= '

Manage — ' . $name . '

'; + $out .= '

DSC record review forthcoming.

'; + $out .= '
'; + return $out; +} diff --git a/hubzilla/addon/dsc01/dsc01_spool.php b/hubzilla/addon/dsc01/dsc01_spool.php new file mode 100644 index 0000000..e4e3453 --- /dev/null +++ b/hubzilla/addon/dsc01/dsc01_spool.php @@ -0,0 +1,153 @@ +Please select at least one category.'; + $out .= dsc01_render_landing($association_slug, $access, $codes, $narrative); + return $out; + } + + // Build spool envelope + $cfg_raw = @file_get_contents('addon/vs01/config.json'); + $cfg = $cfg_raw ? json_decode($cfg_raw, true) : []; + $assoc = $cfg['associations'][$association_slug] ?? []; + + $envelope = dsc01_build_spool_envelope( + $codes, + $narrative, + $association_slug, + $assoc['channel_id'] ?? '', + $access + ); + + $result = dsc01_post_to_orchestrator($envelope); + + if ($result === true) { + $assoc_name = dsc01_h($assoc['name'] ?? $association_slug); + return '
+
+ Your checklist for ' . $assoc_name . ' has been submitted. + + Return to ' . $assoc_name . ' + +
+
'; + } + + $out = '
+ Submission failed. Please try again or contact the operator. +
' . dsc01_h($result) . ' +
'; + $out .= dsc01_render_landing($association_slug, $access, $codes, $narrative); + return $out; +} + +// --------------------------------------------------------------------------- +// SPOOL ENVELOPE +// --------------------------------------------------------------------------- + +function dsc01_build_spool_envelope($dsc_codes, $narrative, $association_slug, $channel_id, $standing) { + return [ + 'addon' => 'dsc01', + 'contract_version' => '1.0', + 'association_slug' => $association_slug, + 'association_channel_id' => (string) $channel_id, + 'submitted_at' => date('c'), + 'standing' => $standing, + 'dsc_codes' => $dsc_codes, + 'narrative' => $narrative, + ]; +} + +// --------------------------------------------------------------------------- +// ORCHESTRATOR POST +// --------------------------------------------------------------------------- + +function dsc01_post_to_orchestrator($envelope) { + $config = dsc01_load_config(); + $receiver_url = $config['receiver_url'] ?? ''; + $node_token = $config['node_token'] ?? ''; + + if (!$receiver_url) { + logger('dsc01_spool: receiver_url not configured'); + return 'Orchestrator receiver URL not configured.'; + } + + $payload = json_encode($envelope); + if ($payload === false) { + logger('dsc01_spool: failed to encode envelope'); + return 'Failed to encode submission.'; + } + + $ch = curl_init($receiver_url); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'X-Node-Token: ' . $node_token, + ], + ]); + + $response = curl_exec($ch); + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curl_error = curl_error($ch); + curl_close($ch); + + if ($curl_error) { + logger('dsc01_spool: curl error: ' . $curl_error); + return 'Network error: ' . $curl_error; + } + + if ($http_code === 200 || $http_code === 201) { + return true; + } + + logger('dsc01_spool: orchestrator returned HTTP ' . $http_code . ': ' . $response); + return 'Orchestrator error (HTTP ' . $http_code . ').'; +} + +// --------------------------------------------------------------------------- +// CONFIG +// --------------------------------------------------------------------------- + +function dsc01_load_config() { + $path = 'addon/dsc01/config.json'; + $raw = @file_get_contents($path); + if ($raw === false) return []; + $data = json_decode($raw, true); + return (json_last_error() === JSON_ERROR_NONE) ? $data : []; +}