12 KiB
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.
{
"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.
{
"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
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
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
{
"_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
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:
- Install the
assoc_profileaddon - Copy
assoc_registry.jsonfrom the source node toaddon/assoc_profile/on the target node - Set ownership:
chown www-data:www-data /var/www/hubzilla/addon/assoc_profile/assoc_registry.json - Create and populate
config.jsonwith theregistry_filepath - 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.jsonand a corresponding entry toconfig.json