From 7abd17d53b198c56cf80672d6e9189f6ac9d8a3e Mon Sep 17 00:00:00 2001 From: TheRON Date: Sat, 6 Jun 2026 04:30:57 -0400 Subject: [PATCH] Updated --- ASSOC-PROFILE-SPEC.md | 312 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 ASSOC-PROFILE-SPEC.md diff --git a/ASSOC-PROFILE-SPEC.md b/ASSOC-PROFILE-SPEC.md new file mode 100644 index 0000000..00d34f4 --- /dev/null +++ b/ASSOC-PROFILE-SPEC.md @@ -0,0 +1,312 @@ +# Association Profile Specification + +**Project:** kane-diagnostics +**Version:** 1.0 +**Addon:** `assoc_profile` +**Status:** Settled — the decisions in this document govern the assoc_profile addon and the assoc_registry.json format + +--- + +## Purpose + +This document specifies the `assoc_profile` addon and the `assoc_registry.json` file format. Together they provide structured, portable, operator-managed diagnostic identity fields for every association channel in the system. + +The profile table in Hubzilla core has a fixed schema that cannot be extended without modifying core. This addon does not modify core. It stores association profile data in a JSON registry file and renders it on the channel profile page via Hubzilla hooks. + +--- + +## Design Decisions + +**JSON registry, not MariaDB.** +Association profile data is written rarely and read frequently by simple key lookup. 5,000 associations at full field population is under 500KB. A flat JSON file is the correct storage for this use case — no schema, no migrations, no queries, no join complexity. + +**Native PHP JSON functions only.** +Hubzilla's vendor stack contains no JSON schema or manipulation library. PHP's native `json_decode` and `json_encode` are correct and sufficient. No vendor dependency is introduced. + +**Atomic writes via `LOCK_EX`.** +`file_put_contents($path, $json, LOCK_EX)` is the write mechanism. It is safe for single-server deployment and does not require any locking library. + +**Two files, one slug key.** +`config.json` holds node-specific operational data — channel ID, group IDs — that differs per node. +`assoc_registry.json` holds portable diagnostic identity data — legal name, type, units, management — that is identical on every node. +The association slug is the key in both files. The addon merges by slug when it needs the full picture. + +**Migration is one file copy.** +A Peer Operator installs the addon, copies `assoc_registry.json` to the addon directory, sets `www-data` ownership. No import script, no schema, no database migration. + +--- + +## File Location + +``` +/var/www/hubzilla/addon/assoc_profile/assoc_registry.json +``` + +Operator-managed. Never committed to the repo. Owned by `www-data`. Path is configurable via `config.json` using the `registry_file` field — see config template below. + +--- + +## Registry Format + +The registry is a JSON object. Each key is the association slug — lowercase, hyphens only, matching `argv(1)` in the addon routes and the key in `config.json` associations. Each value is the association's profile record. + +```json +{ + "arbors-bgca": { + "assoc_legal_name": "Arbors of Buffalo Grove Condominium Association", + "assoc_type": "UNK", + "assoc_statute": "", + "assoc_street": "6 Oak Creek Drive", + "assoc_city": "Buffalo Grove", + "assoc_county": "Cook County", + "assoc_state": "IL", + "assoc_zip": "60089", + "assoc_placekey": "247-222@5sb-8bs-y35", + "assoc_website": "http://arborsofbuffalogrove.com", + "assoc_buildings": "6", + "assoc_units": "330", + "assoc_year_built": "1974", + "assoc_stories": "3", + "assoc_mgmt_company": "Foster Premier Inc", + "assoc_mgmt_contact": "Janet Santilli — jsantilli@fosterpremier.com", + "assoc_mgmt_certified": "UNK", + "assoc_sos_id": "", + "assoc_sos_standing": "UNK", + "assoc_declaration_recorded": "UNK", + "assoc_declaration_instrument": "", + "assoc_registered": "2026-06-15", + "assoc_updated": "2026-06-15" + } +} +``` + +--- + +## Field Definitions + +### Group 1 — Identity + +| Nickname | Label | Type | Values / Notes | +|---|---|---|---| +| `assoc_legal_name` | Legal Name | text | Full legal name as recorded with SOS | +| `assoc_type` | Association Type | select | `Condominium` / `Master` / `CIC` / `Unincorporated` / `UNK` / `Disputed` — VS-01 | +| `assoc_statute` | Governing Statute | text | e.g. `765 ILCS 605` — populated after VS-01 verification | +| `assoc_street` | Street Address | text | Physical address of the property | +| `assoc_city` | City | text | e.g. `Buffalo Grove` | +| `assoc_county` | County | text | e.g. `Cook County` | +| `assoc_state` | State | text | e.g. `IL` | +| `assoc_zip` | ZIP Code | text | e.g. `60089` | +| `assoc_placekey` | Placekey | text | Permanent geographic anchor — e.g. `247-222@5sb-8bs-y35` | +| `assoc_website` | Association Website | text | URL of association-managed site if any | + +### Group 2 — Physical Structure + +| Nickname | Label | Type | Values / Notes | +|---|---|---|---| +| `assoc_buildings` | Number of Buildings | text | Integer or `UNK` — VS-02 | +| `assoc_units` | Number of Units | text | Integer or `UNK` — VS-02 | +| `assoc_year_built` | Year Built | text | e.g. `1974` | +| `assoc_stories` | Stories Per Building | text | e.g. `3` | + +### Group 3 — Governance and Management + +| Nickname | Label | Type | Values / Notes | +|---|---|---|---| +| `assoc_mgmt_company` | Management Company | text | e.g. `Foster Premier Inc` | +| `assoc_mgmt_contact` | Management Contact | text | Name and email of primary contact | +| `assoc_mgmt_certified` | Manager IDFPR Certified | select | `Yes` / `No` / `UNK` — VS-03 | +| `assoc_sos_id` | SOS Entity File Number | text | Illinois Secretary of State entity file number — VS-08 | +| `assoc_sos_standing` | Corporate Standing | select | `Active` / `Dissolved` / `UNK` — VS-08 | +| `assoc_declaration_recorded` | Declaration Recorded | select | `Yes` / `No` / `UNK` — VS-10 | +| `assoc_declaration_instrument` | Declaration Instrument No. | text | County Recorder document number — VS-10 | + +### Group 4 — Record Metadata + +| Nickname | Label | Type | Values / Notes | +|---|---|---|---| +| `assoc_registered` | Registered Date | text | ISO date when this association was added to the registry | +| `assoc_updated` | Last Updated | text | ISO date of most recent field update | + +--- + +## Select Field Allowed Values + +The addon validates select fields against these allowed value sets on write. Any value not in the set is rejected and the field retains its previous value. + +```json +{ + "assoc_type": ["Condominium", "Master", "CIC", "Unincorporated", "UNK", "Disputed"], + "assoc_mgmt_certified": ["Yes", "No", "UNK"], + "assoc_sos_standing": ["Active", "Dissolved", "UNK"], + "assoc_declaration_recorded": ["Yes", "No", "UNK"] +} +``` + +`UNK` is the correct default for all select fields at registration. It is never null. An empty string is not a valid value for select fields. + +--- + +## Addon File Structure + +``` +hubzilla/addon/assoc_profile/ + assoc_profile.php — entry point: hooks, load/unload, content router + assoc_profile.apd — app descriptor + mod_assoc_profile.pdl — PDL layout + config.json.template — registry_file path and other config + assoc_registry.json.template — empty registry with one example entry + view/ + css/ + assoc_profile.css — profile display styles + js/ + assoc_profile.js — minimal behavior +``` + +No `Widget/` directory — this addon does not render a sidebar Widget. The institutional directory Widget belongs to `vs01`, `dsc01`, and `scn01`. This addon renders profile fields only. + +No `vital-signs/` directory — this addon does not render VS forms. + +No `contracts/` directory — this addon does not POST to the orchestrator. + +--- + +## PHP Function Structure + +### Registry I/O + +```php +function assoc_load_registry() { + $config = assoc_load_config(); + $path = $config['registry_file'] + ?? 'addon/assoc_profile/assoc_registry.json'; + $raw = @file_get_contents($path); + if ($raw === false) { + logger('assoc_profile: registry file not found: ' . $path); + return []; + } + $data = json_decode($raw, true); + if (json_last_error() !== JSON_ERROR_NONE) { + logger('assoc_profile: registry file malformed: ' . $path); + return []; + } + return $data; +} + +function assoc_write_registry($data) { + $config = assoc_load_config(); + $path = $config['registry_file'] + ?? 'addon/assoc_profile/assoc_registry.json'; + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + if ($json === false) { + logger('assoc_profile: failed to encode registry'); + return false; + } + $result = file_put_contents($path, $json, LOCK_EX); + if ($result === false) { + logger('assoc_profile: failed to write registry: ' . $path); + return false; + } + return true; +} + +function assoc_get($slug) { + $registry = assoc_load_registry(); + return $registry[$slug] ?? null; +} +``` + +### Validation + +```php +function assoc_validate_select($field, $value) { + $allowed = [ + 'assoc_type' => ['Condominium','Master','CIC','Unincorporated','UNK','Disputed'], + 'assoc_mgmt_certified' => ['Yes','No','UNK'], + 'assoc_sos_standing' => ['Active','Dissolved','UNK'], + 'assoc_declaration_recorded' => ['Yes','No','UNK'], + ]; + if (!isset($allowed[$field])) return true; // text fields always valid + return in_array($value, $allowed[$field], true); +} +``` + +### Hooks + +The addon hooks into two Hubzilla hooks: + +**`profile_edit`** — renders the assoc_profile fields in the profile edit form when the channel being edited has a registry entry. Operator-only — the form only renders if `local_channel()` is the channel owner. + +**`profile_view`** — renders the assoc_profile fields in the profile view when the channel being viewed has a registry entry. Public — any visitor sees the fields. + +The hook callbacks read the registry by the channel's `channel_address` slug, extract the slug from the address, and render accordingly. + +--- + +## config.json.template + +```json +{ + "_note": "Copy to config.json. Do not commit config.json.", + "registry_file": "REPLACE — absolute path to assoc_registry.json on the host, e.g. /var/www/hubzilla/addon/assoc_profile/assoc_registry.json" +} +``` + +--- + +## Hubzilla Hook Registration + +```php +function assoc_profile_load() { + register_hook('profile_edit', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_edit_hook'); + register_hook('profile_view', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_view_hook'); +} + +function assoc_profile_unload() { + unregister_hook('profile_edit', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_edit_hook'); + unregister_hook('profile_view', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_view_hook'); +} +``` + +--- + +## Profile Display Format + +When rendered on the channel profile page, fields are grouped under a section heading **"Association Diagnostic Profile"** and displayed in the three groups defined above. Select fields show their human-readable value. Empty text fields show `—`. `UNK` select values display as `Unknown — not yet verified`. + +The `pdesc` field on the Hubzilla profile (short description) is set by the operator manually to the structured summary format: + +``` +{assoc_type} | {assoc_buildings} bldgs | {assoc_units} units | {assoc_county} +``` + +Example: +``` +UNK | 6 bldgs | 330 units | Cook County +``` + +This is the one field that uses the native Hubzilla profile table and is not stored in the registry. It is the at-a-glance identity visible in directory listings. + +--- + +## Migration Procedure + +**On the receiving Peer Operator node, before importing the channel:** + +1. Install the `assoc_profile` addon +2. Copy `assoc_registry.json` from the source node to `addon/assoc_profile/` on the target node +3. Set ownership: `chown www-data:www-data /var/www/hubzilla/addon/assoc_profile/assoc_registry.json` +4. Create and populate `config.json` with the `registry_file` path +5. Import the channel via Hubzilla's standard channel import + +The registry data is now present on the target node before the channel arrives. The channel profile page will render the association fields immediately after import. + +--- + +## What This Addon Does Not Do + +- It does not create a database table +- It does not POST to the orchestrator +- It does not render a sidebar Widget +- It does not manage privacy groups +- It does not handle VS form submissions +- It does not register associations — registration is adding a slug entry to `assoc_registry.json` and a corresponding entry to `config.json`