254 lines
11 KiB
Markdown
254 lines
11 KiB
Markdown
# Association Channel Model
|
|
|
|
**Project:** kane-diagnostics
|
|
**Version:** 1.0
|
|
**Status:** Settled — the decisions in this document govern all three addons and all future addons in this project
|
|
|
|
---
|
|
|
|
## The Core Decision
|
|
|
|
Each association in the diagnostic system is a first-class Hubzilla entity — a channel with its own identity, its own privacy groups, and its own membership. Every diagnostic submission, every VS record, every scenario account, and every DSC entry is attached to an association channel, not to the operator's channel.
|
|
|
|
This is not a technical convenience. It is the architectural expression of the project's civic purpose. The diagnostic record belongs to the association — to its geography, its homeowners, and its governance history — not to the platform operator. The channel model makes that ownership explicit and portable.
|
|
|
|
---
|
|
|
|
## Why Hubzilla Channels
|
|
|
|
A Hubzilla channel is a portable, federated identity. It carries:
|
|
|
|
- Its own cryptographic keys
|
|
- Its own connections and connection permissions
|
|
- Its own privacy groups and group memberships
|
|
- Its own content and activity history
|
|
- Its own ACL settings
|
|
|
|
Verified from the host: the Hubzilla channel export includes `group`, `group_member`, and `channel_default_group`. Privacy groups and their memberships are exported with the channel and survive node migration intact.
|
|
|
|
This means an association channel created on `diagnostics.kane-il.us` can be cloned to a RON-operated node — Cook County, Orange County CA, or any aligned Civic Infrastructure host — and the channel identity, its privacy groups, and its member assignments all travel with it. The diagnostic record is not tied to any single node.
|
|
|
|
---
|
|
|
|
## Horizontal Scaling
|
|
|
|
There are thousands of HOA associations in Illinois. Each association channel is independent. Demand is distributed naturally across nodes — a Kane County association lives on the Kane County node, a Cook County association lives on the Cook County node, a California association lives on whatever RON node serves that geography.
|
|
|
|
No central node needs to hold all associations. No migration required when demand grows — the Peer Operator slot in the institutional directory is the operational expression of this design. The Peer Operator is the node that receives cloned channels when capacity or geography requires it.
|
|
|
|
---
|
|
|
|
## Channel Structure Per Association
|
|
|
|
Each association channel is created by the operator. The operator is the channel owner. The channel carries:
|
|
|
|
**Identity fields:**
|
|
- Channel name: the association's legal name
|
|
- Channel address: `[association-slug]@diagnostics.kane-il.us`
|
|
- Location: street address of the association (Kane County address for Kane associations)
|
|
- Description: association type (VS-01 classification), unit count (VS-02), and primary statute
|
|
|
|
**Privacy Groups — three tiers:**
|
|
|
|
| Group name | Role | Access |
|
|
|---|---|---|
|
|
| `public` | Anyone | Read-only — VS content, DSC categories, scenario carousel |
|
|
| `sase_participant` | SASE-verified homeowner | Submit VS records, submit scenario accounts |
|
|
| `corpus_builder` | Promoted participant or operator | Review TMP, promote to corpus, submit professional records |
|
|
|
|
The group names are conventions, not hardcoded values. The addon reads group IDs from `config.json` per association — not a single global value.
|
|
|
|
**Default group:** `public` — unauthenticated visitors see the association's public record without joining.
|
|
|
|
---
|
|
|
|
## config.json Per Association
|
|
|
|
The current `config.json.template` carries a single `corpus_builder_group_id`. This is replaced by a per-association configuration structure.
|
|
|
|
The revised `config.json.template`:
|
|
|
|
```json
|
|
{
|
|
"_note": "Copy to config.json. Do not commit config.json.",
|
|
"receiver_url": "REPLACE — orchestrator receiver endpoint",
|
|
"node_token": "REPLACE — shared secret for node-to-orchestrator auth",
|
|
"listings_file": "REPLACE — absolute path to listings.json on the host",
|
|
"directory_default_tab": "core",
|
|
"associations": {
|
|
"REPLACE-CHANNEL-ID": {
|
|
"name": "REPLACE — association legal name",
|
|
"address": "REPLACE — association street address",
|
|
"channel_id": "REPLACE — Hubzilla channel ID (integer)",
|
|
"channel_address": "REPLACE — channel@node address",
|
|
"groups": {
|
|
"public": 0,
|
|
"sase_participant": 0,
|
|
"corpus_builder": 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Multiple associations can be registered on a single node — each as a keyed entry in the `associations` object. The addon identifies the current association from the URL path: `/vs01/[association-slug]`.
|
|
|
|
---
|
|
|
|
## Access State Function — Revised
|
|
|
|
The current `vs01_access_state()` function reads a single global group ID. The revised function accepts an association identifier and reads the per-association group configuration.
|
|
|
|
```php
|
|
function vs01_access_state($association_key) {
|
|
$config = vs01_load_config();
|
|
$assoc = $config['associations'][$association_key] ?? null;
|
|
|
|
if (!$assoc) return 'public';
|
|
|
|
$channel_id = local_channel();
|
|
if (!$channel_id) return 'public';
|
|
|
|
// Operator check — channel owner of the association channel
|
|
if ($channel_id === intval($assoc['channel_id'])) return 'operator';
|
|
|
|
$observer = get_observer_hash();
|
|
$groups = $assoc['groups'] ?? [];
|
|
|
|
// Corpus Builder — highest participant tier
|
|
$cb_gid = intval($groups['corpus_builder'] ?? 0);
|
|
if ($cb_gid && in_array($observer, group_get_members_xchan($cb_gid))) {
|
|
return 'participant'; // corpus_builder maps to participant access state
|
|
}
|
|
|
|
// SASE Participant
|
|
$sase_gid = intval($groups['sase_participant'] ?? 0);
|
|
if ($sase_gid && in_array($observer, group_get_members_xchan($sase_gid))) {
|
|
return 'participant';
|
|
}
|
|
|
|
return 'public';
|
|
}
|
|
```
|
|
|
|
The `professional` access state is determined separately — by the presence of an active listing entry in `listings.json` matching the current observer's xchan hash. This is not a Hubzilla group — it is an operator-managed listing record.
|
|
|
|
```php
|
|
function vs01_is_professional($observer_hash) {
|
|
$listings = vs01_load_listings();
|
|
foreach (['tier1', 'tier2'] as $tier) {
|
|
foreach ($listings[$tier] ?? [] as $entry) {
|
|
if (($entry['xchan'] ?? '') === $observer_hash
|
|
&& ($entry['status'] ?? '') === 'active') {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
```
|
|
|
|
The `listings.json` schema gains an `xchan` field per Tier-I and Tier-II entry. The operator records the professional's xchan hash when approving the listing. This is the connection between the institutional directory and the access control system.
|
|
|
|
---
|
|
|
|
## URL Routing
|
|
|
|
The current skeleton routes on `/vs01` with no association context. The revised routing adds the association slug as the first path segment:
|
|
|
|
```
|
|
/vs01 — index: list of registered associations
|
|
/vs01/[association-slug] — association landing page, VS navigation
|
|
/vs01/[association-slug]/VS-01 — VS-01 form for this association
|
|
/vs01/[association-slug]/VS-02 — VS-02 form for this association
|
|
...
|
|
/vs01/[association-slug]/manage — operator management interface
|
|
```
|
|
|
|
The addon reads `argv(1)` for the association slug and `argv(2)` for the VS code. Both are validated against the config before any content is rendered.
|
|
|
|
```php
|
|
function vs01_content() {
|
|
$association_slug = argv(1) ?? '';
|
|
$vs_code = strtoupper(argv(2) ?? '');
|
|
|
|
if (!$association_slug) {
|
|
return vs01_render_association_index();
|
|
}
|
|
|
|
$config = vs01_load_config();
|
|
if (!isset($config['associations'][$association_slug])) {
|
|
return vs01_render_not_found();
|
|
}
|
|
|
|
$access = vs01_access_state($association_slug);
|
|
|
|
if ($vs_code && preg_match('/^VS-\d{2}$/', $vs_code)) {
|
|
return vs01_render_vs_form($association_slug, $vs_code, $access);
|
|
}
|
|
|
|
return vs01_render_association_landing($association_slug, $access);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Operator Workflow — Creating an Association Channel
|
|
|
|
When a new association is registered in the diagnostic system, the operator:
|
|
|
|
1. Creates a new Hubzilla channel for the association on the node
|
|
2. Creates three privacy groups on that channel: `sase_participant`, `corpus_builder`, and notes the `public` group ID (group 0 or the channel's default)
|
|
3. Notes the channel ID (integer) and channel address
|
|
4. Adds the association entry to `config.json` on the host with the channel ID, address, and group IDs
|
|
5. Pulls from Gitea and copies the updated config to the addon directory
|
|
|
|
No code change is required to add a new association. The JSON controls the registration. This is the JSON-over-PHP discipline applied to the association model.
|
|
|
|
---
|
|
|
|
## Migration Workflow — Cloning to a RON Node
|
|
|
|
When an association channel is migrated to a Peer Operator node:
|
|
|
|
1. The operator exports the association channel from the current node (Hubzilla channel export)
|
|
2. The Peer Operator imports the channel to their node
|
|
3. The export includes: channel identity, connections, privacy groups, group memberships, and content
|
|
4. The Peer Operator adds the association entry to their node's `config.json` with the new channel ID assigned on import
|
|
5. The spool receiver URL in `config.json` points to the Peer Operator's orchestrator
|
|
|
|
The diagnostic record — the VS submissions, the scenario accounts, the DSC entries — lives in the orchestrator and IPFS, not in the Hubzilla channel. The channel migration moves the identity and access control. The record itself is independently preserved.
|
|
|
|
---
|
|
|
|
## What Changes in the Skeleton
|
|
|
|
The current skeleton `vs01.php` has:
|
|
- A single `corpus_builder_group_id` in config
|
|
- A single `vs01_access_state()` with no association context
|
|
- A single route at `/vs01`
|
|
|
|
The revised `vs01.php` needs:
|
|
- Per-association config structure
|
|
- `vs01_access_state($association_slug)` with group lookup per association
|
|
- `vs01_is_professional($observer_hash)` with listings lookup
|
|
- URL routing on `argv(1)` and `argv(2)`
|
|
- Association index render function
|
|
- Association landing render function
|
|
- VS form render function — loads schema, renders form per VS-RENDERER-SPEC.md
|
|
|
|
The `config.json.template` is revised to the per-association structure defined above.
|
|
|
|
The `listings.json` schema gains an `xchan` field per Tier-I and Tier-II entry — to be added in the next revision of `ADDON-SKELETON-SPEC.md`.
|
|
|
|
---
|
|
|
|
## What Does Not Change
|
|
|
|
- The PDL — `aside`, `content`, `right_aside` structure is unchanged
|
|
- The Widget — four-tab institutional directory is unchanged
|
|
- The right PDL — `notifications` and `newmember` are unchanged
|
|
- The spool envelope — gains `association_channel_id` in place of `association_address`
|
|
- The orchestrator pattern — the addon POSTs to the orchestrator, nothing stored on the Hubzilla node
|
|
- The coding constraints — 500-line limit, no inline styles or scripts, no hardcoded values
|