Files
kane-diagnostics/ASSOC-PROFILE-SPEC.md
2026-06-06 04:30:57 -04:00

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:

  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