Initial push
This commit is contained in:
191
hubzilla/addon/dsc01/DSC-RENDERER-SPEC.md
Normal file
191
hubzilla/addon/dsc01/DSC-RENDERER-SPEC.md
Normal file
@@ -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
|
||||
<div class="dsc01-category">
|
||||
<label>
|
||||
<input type="checkbox" name="dsc_codes[]" value="DSC-01">
|
||||
<strong>DSC-01</strong> — Assessments
|
||||
</label>
|
||||
<details class="dsc01-questions">
|
||||
<summary>Diagnostic questions</summary>
|
||||
<ul>
|
||||
<li>Was the assessment properly authorized by the Board...?</li>
|
||||
...
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
```
|
||||
|
||||
After the 10 categories, a single `<textarea name="narrative">` for the
|
||||
free-text case description, then submit.
|
||||
|
||||
`vital_sign_dependencies` are **not** shown in the homeowner checklist UI in
|
||||
v1 — they are reference data for professional/operator views (future:
|
||||
cross-link from a DSC record back to the association's VS profile to show
|
||||
which VS conditions are most relevant given the selected DSC codes).
|
||||
|
||||
---
|
||||
|
||||
## 7. Spool / orchestrator
|
||||
|
||||
`dsc01_spool.php` mirrors `vs01_spool.php`'s `*_post_to_orchestrator()`
|
||||
pattern:
|
||||
|
||||
- Reads `receiver_url` / `node_token` from `dsc01/config.json` (separate
|
||||
config keys from vs01 — see `config.json.template`).
|
||||
- POSTs the JSON envelope (Section 3) via curl, same header shape
|
||||
(`Content-Type: application/json`, `X-Node-Token`).
|
||||
- On success, shows a confirmation with a link back to the association
|
||||
landing (`vs01/{slug}` or `dsc01/{slug}`, TBD which is more useful as the
|
||||
"return to" target — likely `vs01/{slug}` since that's the association's
|
||||
primary page).
|
||||
- On failure, shows the same alert-danger pattern as vs01.
|
||||
|
||||
`records_file` (host-only, heredoc-created, never committed) is reserved
|
||||
for a future local append-only log if the orchestrator round trip needs a
|
||||
local fallback/cache — not required for v1 if the orchestrator receiver is
|
||||
reliable. Document only; do not implement unless requested.
|
||||
|
||||
---
|
||||
|
||||
## 8. Out of scope for this pass
|
||||
|
||||
- Per-DSC-code detail pages.
|
||||
- Operator manage/review UI (stub only, mirroring vs01's placeholder).
|
||||
- Cross-linking rendered VS dependencies into the UI.
|
||||
- Incubator (DSC-11..16, DSC-24) and Monitor categories — not represented
|
||||
as schema files until promoted to Active per DSC-development-map.md.
|
||||
6
hubzilla/addon/dsc01/config.json.template
Normal file
6
hubzilla/addon/dsc01/config.json.template
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"_note": "Copy to config.json. Do not commit config.json — it contains secrets and installation-specific values. Access-state (associations/groups) is NOT duplicated here — dsc01 reads addon/vs01/config.json as the single source of truth for that.",
|
||||
"receiver_url": "REPLACE — orchestrator receiver endpoint, e.g. https://orchestrator1.internal.diagnostics.kane-il.us/receive-dsc",
|
||||
"node_token": "REPLACE — shared secret for node-to-orchestrator authentication",
|
||||
"records_file": "REPLACE — absolute path to dsc01_records.json on the host, e.g. /var/www/hubzilla/addon/dsc01/dsc01_records.json"
|
||||
}
|
||||
@@ -3,13 +3,16 @@
|
||||
/**
|
||||
* Name: DSC01 DSC Categories
|
||||
* Description: Public civic diagnostic — the legal surfaces where HOA governance disputes manifest.
|
||||
* Version: 0.2.0
|
||||
* Version: 0.3.0
|
||||
* MinVersion: 11.0
|
||||
* MaxVersion: 12.0
|
||||
*/
|
||||
|
||||
use Zotlabs\Extend\Widget;
|
||||
|
||||
require_once 'addon/dsc01/dsc01_renderer.php';
|
||||
require_once 'addon/dsc01/dsc01_spool.php';
|
||||
|
||||
function dsc01_module() {}
|
||||
|
||||
function dsc01_load() {
|
||||
@@ -101,7 +104,7 @@ function dsc01_access_state($association_slug = '') {
|
||||
intval($prof_gid),
|
||||
dbesc($observer)
|
||||
);
|
||||
if ($r) return 'participant';
|
||||
if ($r) return 'professional';
|
||||
}
|
||||
|
||||
return 'public';
|
||||
@@ -127,7 +130,7 @@ function dsc01_access_wall($association_slug = '') {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CONTENT
|
||||
// CONTENT ROUTER
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dsc01_content() {
|
||||
@@ -139,38 +142,50 @@ function dsc01_content() {
|
||||
}
|
||||
|
||||
$association_slug = argv(1) ?? '';
|
||||
$sub_route = strtolower(argv(2) ?? '');
|
||||
|
||||
// Index — list the ten DSC categories
|
||||
if (!$association_slug) {
|
||||
return dsc01_render_index();
|
||||
}
|
||||
|
||||
$raw = @file_get_contents('addon/vs01/config.json');
|
||||
$cfg = $raw ? json_decode($raw, true) : [];
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !isset($cfg['associations'][$association_slug])) {
|
||||
return dsc01_render_not_found();
|
||||
}
|
||||
|
||||
$access = dsc01_access_state($association_slug);
|
||||
|
||||
// dsc01 is public — access wall only gates submission, not reading
|
||||
// Manage route — operator only (stub)
|
||||
if ($sub_route === 'manage') {
|
||||
if ($access !== 'operator') {
|
||||
return dsc01_access_wall($association_slug);
|
||||
}
|
||||
return dsc01_render_manage($association_slug);
|
||||
}
|
||||
|
||||
// POST — submission handler
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($access === 'public') {
|
||||
return dsc01_access_wall($association_slug);
|
||||
}
|
||||
// TODO: handle POST submission
|
||||
return dsc01_access_wall($association_slug);
|
||||
if (!dsc01_verify_csrf()) {
|
||||
return '<div class="alert alert-danger">Invalid form token. Please reload and try again.</div>';
|
||||
}
|
||||
return dsc01_handle_post($association_slug, $access);
|
||||
}
|
||||
|
||||
return dsc01_render_main($access);
|
||||
// Association landing — checklist
|
||||
return dsc01_render_landing($association_slug, $access);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RENDER
|
||||
// NOT FOUND
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dsc01_render_main($access) {
|
||||
$out = '<div class="dsc01-content">';
|
||||
$out .= '<div class="dsc01-header mb-3">';
|
||||
$out .= '<h2>Categories</h2>';
|
||||
$out .= '<p class="text-muted">The legal surfaces where HOA governance disputes manifest, organized from the homeowner\'s perspective.</p>';
|
||||
$out .= '</div>';
|
||||
|
||||
// TODO: render DSC categories
|
||||
|
||||
$out .= '<div class="dsc01-placeholder text-muted fst-italic">Content forthcoming.</div>';
|
||||
$out .= '</div>';
|
||||
|
||||
return $out;
|
||||
function dsc01_render_not_found() {
|
||||
return '<div class="dsc01-content"><div class="alert alert-warning">Association not found.</div></div>';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
201
hubzilla/addon/dsc01/dsc01_renderer.php
Normal file
201
hubzilla/addon/dsc01/dsc01_renderer.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DSC-01 Renderer
|
||||
* Renders the ten DSC category checklist. See DSC-RENDERER-SPEC.md.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SCHEMA LOADING
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dsc01_load_categories() {
|
||||
$dir = 'addon/dsc01/diagnostic-categories';
|
||||
$out = [];
|
||||
$files = @glob($dir . '/DSC-*.json');
|
||||
if (!$files) return $out;
|
||||
sort($files);
|
||||
foreach ($files as $file) {
|
||||
$raw = @file_get_contents($file);
|
||||
if ($raw === false) continue;
|
||||
$data = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) continue;
|
||||
$meta = $data['_meta'] ?? null;
|
||||
if (!$meta || empty($meta['code'])) continue;
|
||||
$out[$meta['code']] = $meta;
|
||||
}
|
||||
ksort($out);
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RENDER — INDEX (category overview)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dsc01_render_index() {
|
||||
$categories = dsc01_load_categories();
|
||||
|
||||
$out = '<div class="dsc01-content">';
|
||||
$out .= '<div class="dsc01-header mb-3">';
|
||||
$out .= '<h2>Diagnostic Surface Categories</h2>';
|
||||
$out .= '<p class="text-muted">The legal surfaces where HOA governance disputes manifest, organized from the homeowner\'s perspective. Select an association to apply this checklist to a case.</p>';
|
||||
$out .= '</div>';
|
||||
|
||||
if (empty($categories)) {
|
||||
$out .= '<div class="dsc01-placeholder text-muted fst-italic">No categories defined.</div>';
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
$out .= '<nav class="dsc01-category-nav" aria-label="Diagnostic Surface Categories">';
|
||||
$out .= '<ul class="list-group list-group-flush">';
|
||||
foreach ($categories as $code => $meta) {
|
||||
$title = dsc01_h($meta['title'] ?? $code);
|
||||
$summary = dsc01_h($meta['summary'] ?? '');
|
||||
$out .= '<li class="list-group-item">';
|
||||
$out .= '<strong>' . dsc01_h($code) . '</strong> — ' . $title;
|
||||
if ($summary) {
|
||||
$out .= '<p class="small text-muted mb-0">' . $summary . '</p>';
|
||||
}
|
||||
$out .= '</li>';
|
||||
}
|
||||
$out .= '</ul></nav>';
|
||||
|
||||
// Association picker — list associations from vs01/config.json
|
||||
$raw = @file_get_contents('addon/vs01/config.json');
|
||||
$cfg = $raw ? json_decode($raw, true) : [];
|
||||
$associations = $cfg['associations'] ?? [];
|
||||
|
||||
if (!empty($associations)) {
|
||||
$out .= '<div class="dsc01-association-picker mt-4">';
|
||||
$out .= '<h3>Apply this checklist</h3>';
|
||||
$out .= '<ul class="list-group">';
|
||||
foreach ($associations as $slug => $assoc) {
|
||||
$name = dsc01_h($assoc['name'] ?? $slug);
|
||||
$url = z_root() . '/dsc01/' . dsc01_h($slug);
|
||||
$out .= '<li class="list-group-item"><a href="' . $url . '">' . $name . '</a></li>';
|
||||
}
|
||||
$out .= '</ul></div>';
|
||||
}
|
||||
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RENDER — ASSOCIATION LANDING (checklist form)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dsc01_render_landing($association_slug, $access, $submitted_codes = [], $submitted_narrative = '') {
|
||||
$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);
|
||||
|
||||
$categories = dsc01_load_categories();
|
||||
|
||||
$out = '<div class="dsc01-content">';
|
||||
$out .= '<div class="dsc01-header mb-3">';
|
||||
$out .= '<h2>' . $name . '</h2>';
|
||||
$out .= '<p class="text-muted">Diagnostic Surface Categories checklist. Select every category that applies to your situation.</p>';
|
||||
$out .= '</div>';
|
||||
|
||||
if ($access === 'public') {
|
||||
$out .= dsc01_render_checklist_readonly($categories);
|
||||
$out .= '<div class="alert alert-info mt-3">';
|
||||
$out .= 'DSC Categories are public. ';
|
||||
$out .= '<a href="https://directory.diagnostics.kane-il.us/channel/theron">Complete the SASE process</a> ';
|
||||
$out .= 'to submit a checklist for this association.';
|
||||
$out .= '</div>';
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
$form_url = z_root() . '/dsc01/' . dsc01_h($association_slug);
|
||||
|
||||
$out .= '<form method="post" action="' . $form_url . '" class="dsc01-form" novalidate>';
|
||||
$out .= dsc01_csrf_token();
|
||||
|
||||
foreach ($categories as $code => $meta) {
|
||||
$checked = in_array($code, $submitted_codes, true) ? ' checked' : '';
|
||||
$title = dsc01_h($meta['title'] ?? $code);
|
||||
|
||||
$out .= '<div class="dsc01-category">';
|
||||
$out .= '<label class="dsc01-category-label">';
|
||||
$out .= '<input type="checkbox" name="dsc_codes[]" value="' . dsc01_h($code) . '"' . $checked . '> ';
|
||||
$out .= '<strong>' . dsc01_h($code) . '</strong> — ' . $title;
|
||||
$out .= '</label>';
|
||||
|
||||
if (!empty($meta['diagnostic_questions'])) {
|
||||
$out .= '<details class="dsc01-questions">';
|
||||
$out .= '<summary>Diagnostic questions</summary>';
|
||||
$out .= '<ul>';
|
||||
foreach ($meta['diagnostic_questions'] as $q) {
|
||||
$out .= '<li>' . dsc01_h($q) . '</li>';
|
||||
}
|
||||
$out .= '</ul>';
|
||||
$out .= '</details>';
|
||||
}
|
||||
|
||||
$out .= '</div>';
|
||||
}
|
||||
|
||||
$out .= '<div class="dsc01-narrative mt-3">';
|
||||
$out .= '<label for="dsc01_narrative"><strong>Describe your situation</strong></label>';
|
||||
$out .= '<textarea id="dsc01_narrative" name="narrative" class="form-control" rows="6">'
|
||||
. dsc01_h($submitted_narrative) . '</textarea>';
|
||||
$out .= '</div>';
|
||||
|
||||
$out .= '<div class="mt-3">';
|
||||
$out .= '<button type="submit" class="btn btn-primary">Submit</button>';
|
||||
$out .= '</div>';
|
||||
$out .= '</form>';
|
||||
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RENDER — READ-ONLY CHECKLIST (public access)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dsc01_render_checklist_readonly($categories) {
|
||||
$out = '<div class="dsc01-checklist-readonly">';
|
||||
foreach ($categories as $code => $meta) {
|
||||
$title = dsc01_h($meta['title'] ?? $code);
|
||||
$out .= '<div class="dsc01-category">';
|
||||
$out .= '<strong>' . dsc01_h($code) . '</strong> — ' . $title;
|
||||
|
||||
if (!empty($meta['diagnostic_questions'])) {
|
||||
$out .= '<details class="dsc01-questions">';
|
||||
$out .= '<summary>Diagnostic questions</summary>';
|
||||
$out .= '<ul>';
|
||||
foreach ($meta['diagnostic_questions'] as $q) {
|
||||
$out .= '<li>' . dsc01_h($q) . '</li>';
|
||||
}
|
||||
$out .= '</ul>';
|
||||
$out .= '</details>';
|
||||
}
|
||||
|
||||
$out .= '</div>';
|
||||
}
|
||||
$out .= '</div>';
|
||||
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 = '<div class="dsc01-content">';
|
||||
$out .= '<h2>Manage — ' . $name . '</h2>';
|
||||
$out .= '<p class="text-muted fst-italic">DSC record review forthcoming.</p>';
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
153
hubzilla/addon/dsc01/dsc01_spool.php
Normal file
153
hubzilla/addon/dsc01/dsc01_spool.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DSC-01 Spool Handler
|
||||
* Validates POST submissions, builds the spool envelope,
|
||||
* and POSTs to the orchestrator receiver.
|
||||
* See DSC-RENDERER-SPEC.md — Record shape / Spool sections.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST HANDLER
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dsc01_handle_post($association_slug, $access) {
|
||||
$categories = dsc01_load_categories();
|
||||
$valid_codes = array_keys($categories);
|
||||
|
||||
$raw_codes = $_POST['dsc_codes'] ?? [];
|
||||
$codes = [];
|
||||
if (is_array($raw_codes)) {
|
||||
foreach ($raw_codes as $code) {
|
||||
$code = strtoupper(trim((string) $code));
|
||||
if (in_array($code, $valid_codes, true)) {
|
||||
$codes[] = $code;
|
||||
}
|
||||
}
|
||||
}
|
||||
$codes = array_values(array_unique($codes));
|
||||
|
||||
$narrative = isset($_POST['narrative'])
|
||||
? substr(strip_tags((string) $_POST['narrative']), 0, 8192)
|
||||
: '';
|
||||
|
||||
// Validation — at least one category required
|
||||
if (empty($codes)) {
|
||||
$out = '<div class="alert alert-danger"><strong>Please select at least one category.</strong></div>';
|
||||
$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 '<div class="dsc01-content">
|
||||
<div class="alert alert-success">
|
||||
Your checklist for ' . $assoc_name . ' has been submitted.
|
||||
<a href="' . z_root() . '/vs01/' . dsc01_h($association_slug) . '">
|
||||
Return to ' . $assoc_name . '
|
||||
</a>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$out = '<div class="alert alert-danger">
|
||||
Submission failed. Please try again or contact the operator.
|
||||
<br><small>' . dsc01_h($result) . '</small>
|
||||
</div>';
|
||||
$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 : [];
|
||||
}
|
||||
Reference in New Issue
Block a user