# 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`