Files
kane-diagnostics/hubzilla/addon/cry01/cry01_renderer.php
2026-06-12 15:33:51 -04:00

352 lines
16 KiB
PHP

<?php
/**
* cry01_renderer.php — All HTML rendering for cry01.
* Knows nothing about network calls or POST handling.
* Reads config and cache via cry01_chain.php functions.
*/
// ----------------------------------------------------------------------------
// ASSOCIATION LANDING
// ----------------------------------------------------------------------------
function cry01_render_landing($association_slug, $access, $lookup = []) {
// Renders the main cry01 page: balance display + signal board + public lookup.
// $lookup may contain: lookup_error, lookup_address, lookup_balance,
// lookup_account_info — populated when this page is rendered as the
// result of a lookup POST.
$raw = @file_get_contents('addon/vs01/config.json');
$cfg = $raw ? json_decode($raw, true) : [];
$assoc = $cfg['associations'][$association_slug] ?? [];
$name = cry01_h($assoc['name'] ?? $association_slug);
$out = '<div class="cry01-content">';
$out .= '<div class="cry01-header mb-3">';
$out .= '<h2>' . $name . '</h2>';
$out .= '<p class="text-muted">Value Layer — Ğ1 balance and capacity signals.</p>';
$out .= '</div>';
// Optional participation notice.
$out .= '<p class="text-muted small cry01-optional-notice">';
$out .= 'The Value Layer and the Ğ1 Wallet are <strong>optional</strong>. ';
$out .= 'Ğ1 is a libre currency independent of fiat, used for valuing and bartering surplus capacity among private individuals. ';
$out .= 'The Civic Infrastructure implements it for participants who choose to engage in that economy. ';
$out .= 'It is not required for participation in the diagnostic record system.';
$out .= '</p>';
$out .= cry01_render_balance_display();
$out .= cry01_render_lookup_form($association_slug, $lookup);
$out .= cry01_render_signal_board($association_slug, $access);
if ($access === 'participant') {
$signal_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/signal';
$out .= '<div class="mt-3">';
$out .= '<a href="' . $signal_url . '" class="btn btn-outline-primary btn-sm">Register a Signal</a>';
$out .= '</div>';
}
if ($access === 'operator') {
$manage_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/manage';
$g1_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/g1';
$out .= '<div class="mt-3 d-flex gap-2">';
$out .= '<a href="' . $manage_url . '" class="btn btn-sm btn-outline-secondary">Manage</a>';
$out .= '<a href="' . $g1_url . '" class="btn btn-sm btn-outline-secondary">Ğ1 Candidates</a>';
$out .= '</div>';
}
$out .= '</div>';
return $out;
}
// ----------------------------------------------------------------------------
// BALANCE DISPLAY (operator/cached)
// ----------------------------------------------------------------------------
function cry01_render_balance_display() {
// Renders the Ğ1 balance from cache. The cached key belongs to whoever last refreshed.
// Shows staleness if cache is old.
$cache = cry01_read_cache();
$balance = $cache['balance'] ?? null;
$pubkey = $cache['g1_pubkey'] ?? '';
$refreshed = $cache['refreshed_at'] ?? null;
$out = '<div class="cry01-balance-display mb-4">';
$out .= '<h5 class="cry01-section-label">Ğ1 Balance</h5>';
if ($balance === null) {
$out .= '<p class="text-muted fst-italic">Balance not yet loaded. Unlock your Ğ1 wallet and use Manage to refresh.</p>';
} else {
$out .= '<p class="cry01-balance-amount">' . cry01_h($balance) . '</p>';
if ($pubkey) {
$out .= '<p class="text-muted small">' . cry01_h(substr($pubkey, 0, 16) . '...') . '</p>';
}
if ($refreshed) {
$stale = cry01_cache_is_stale() ? ' <span class="badge bg-warning text-dark">stale</span>' : '';
$out .= '<p class="text-muted small">Last refreshed: ' . cry01_h($refreshed) . $stale . '</p>';
}
}
$out .= '</div>';
return $out;
}
// ----------------------------------------------------------------------------
// PUBLIC BALANCE LOOKUP
// ----------------------------------------------------------------------------
function cry01_render_lookup_form($association_slug, $lookup = []) {
// Renders the public Ğ1 address lookup form.
// Anyone can use this — no SASE, no wallet session, nothing stored.
// Paste an address, see its current balance and account details, read
// directly from this node's own Duniter mirror.
$lookup_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/lookup';
$error = $lookup['lookup_error'] ?? '';
$entered_address = $lookup['lookup_address'] ?? '';
$balance = $lookup['lookup_balance'] ?? null;
$account_info = $lookup['lookup_account_info'] ?? null;
$out = '<div class="cry01-lookup mb-4">';
$out .= '<h5 class="cry01-section-label">Ğ1 Address Lookup</h5>';
$out .= '<p class="text-muted small">Paste any Ğ1 wallet address to check its current balance and account details — read directly from this node\'s Duniter mirror. Nothing is stored.</p>';
$out .= '<form method="post" action="' . $lookup_url . '" class="cry01-lookup-form">';
$out .= cry01_csrf_token();
$out .= '<div class="input-group">';
$out .= '<input type="text" class="form-control font-monospace" name="g1_lookup_address"
placeholder="g1..." maxlength="64"
value="' . cry01_h($entered_address) . '">';
$out .= '<button type="submit" class="btn btn-outline-primary">Check Balance</button>';
$out .= '</div>';
$out .= '</form>';
if ($error) {
$out .= '<div class="alert alert-danger mt-2">' . cry01_h($error) . '</div>';
} elseif ($balance !== null) {
$out .= '<div class="alert alert-success mt-2">';
$out .= '<span class="font-monospace small">' . cry01_h(substr($entered_address, 0, 16) . '...') . '</span><br>';
$out .= '<strong>' . cry01_h($balance) . '</strong>';
$out .= '</div>';
if ($account_info) {
$out .= cry01_render_account_info_table($account_info);
}
}
$out .= '</div>';
return $out;
}
function cry01_render_account_info_table($info) {
// Renders the full decoded AccountInfo struct in plain language.
// $info is the array returned by cry01_decode_account_info() /
// cry01_get_account_info(): nonce, consumers, providers, sufficients,
// free, reserved, frozen, flags — all base-10 decimal strings of raw
// on-chain units (free/reserved/frozen/flags are in centimes; nonce/
// consumers/providers/sufficients are plain counts).
//
// 'free' is already shown separately as the headline balance — this
// table covers the remaining fields. 'flags' is an internal bitfield
// (account feature flags) and is not meaningful to a layperson; it is
// omitted from display but available in $info for anyone who needs it.
$reserved = cry01_format_g1_amount($info['reserved'] ?? '0');
$frozen = cry01_format_g1_amount($info['frozen'] ?? '0');
$nonce = $info['nonce'] ?? '0';
$out = '<div class="cry01-account-info mt-2">';
$out .= '<table class="table table-sm cry01-account-info-table">';
$out .= '<tbody>';
$out .= '<tr><td class="text-muted">Transactions sent</td><td>' . cry01_h($nonce) . '</td></tr>';
if ($info['reserved'] !== '0') {
$out .= '<tr><td class="text-muted">Reserved</td><td>' . cry01_h($reserved) . '</td></tr>';
}
if ($info['frozen'] !== '0') {
$out .= '<tr><td class="text-muted">Frozen</td><td>' . cry01_h($frozen) . '</td></tr>';
}
$out .= '</tbody>';
$out .= '</table>';
$out .= '</div>';
return $out;
}
// ----------------------------------------------------------------------------
// SIGNAL BOARD
// ----------------------------------------------------------------------------
function cry01_render_signal_board($association_slug, $access) {
// Renders the signal board. Visible to participants and operator only.
if ($access === 'public') {
return '<div class="alert alert-info">Signal board is visible to verified participants only.</div>';
}
// TODO: load signals from orchestrator spool for this association.
$out = '<div class="cry01-signal-board mb-4">';
$out .= '<h5 class="cry01-section-label">Capacity Signals</h5>';
$out .= '<p class="text-muted fst-italic">No signals posted yet.</p>';
$out .= '</div>';
return $out;
}
// ----------------------------------------------------------------------------
// SIGNAL FORM
// ----------------------------------------------------------------------------
function cry01_render_signal_form($association_slug, $access) {
// Renders the capacity signal registration form.
// The g1_pubkey field is populated by the g1wallet session event when available.
$config = cry01_load_config();
$categories = $config['signal_categories'] ?? [];
$form_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/signal';
$out = '<div class="cry01-content">';
$out .= '<h3>Register a Capacity Signal</h3>';
$out .= '<p class="text-muted">Describe what you are offering or seeking, denominated in Ğ1.</p>';
// Wallet session note — replaced by g1wallet integration once built.
$out .= '<div class="alert alert-info small">Your Ğ1 public key will be populated automatically once the Ğ1 Wallet addon is installed. Until then, enter it manually below.</div>';
$out .= '<form method="post" action="' . $form_url . '" class="cry01-form" novalidate>';
$out .= cry01_csrf_token();
// g1_pubkey — will be read-only and auto-populated by g1wallet session once available.
$out .= '<div class="mb-3">';
$out .= '<label class="form-label" for="g1_pubkey">Your Ğ1 Public Key <span class="text-danger">*</span></label>';
$out .= '<input type="text" class="form-control" id="g1_pubkey" name="g1_pubkey" required
placeholder="G1..." maxlength="64"
data-g1wallet-target="pubkey">';
$out .= '<div class="form-text">Your Ğ1 address. This is your payment address for this signal.</div>';
$out .= '</div>';
// Direction
$out .= '<div class="mb-3">';
$out .= '<fieldset><legend class="form-label">Direction <span class="text-danger">*</span></legend>';
$out .= '<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="dir_offer" name="direction" value="offer" required>
<label class="form-check-label" for="dir_offer">Offering capacity</label>
</div>';
$out .= '<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="dir_seek" name="direction" value="seek">
<label class="form-check-label" for="dir_seek">Seeking capacity</label>
</div>';
$out .= '</fieldset></div>';
// Category
if ($categories) {
$out .= '<div class="mb-3">';
$out .= '<label class="form-label" for="category_id">Category <span class="text-danger">*</span></label>';
$out .= '<select class="form-select" id="category_id" name="category_id" required>';
$out .= '<option value="">— select —</option>';
foreach ($categories as $cat) {
$v = cry01_h($cat['id'] ?? '');
$l = cry01_h($cat['label'] ?? $v);
$out .= '<option value="' . $v . '">' . $l . '</option>';
}
$out .= '</select></div>';
}
// Description
$out .= '<div class="mb-3">';
$out .= '<label class="form-label" for="capacity_description">Description <span class="text-danger">*</span></label>';
$out .= '<textarea class="form-control" id="capacity_description" name="capacity_description"
rows="4" required
placeholder="Describe what you are offering or seeking in plain language."></textarea>';
$out .= '</div>';
// Denomination
$out .= '<div class="mb-3">';
$out .= '<label class="form-label" for="g1_denomination">Ğ1 Amount <span class="text-danger">*</span></label>';
$out .= '<input type="number" class="form-control" id="g1_denomination" name="g1_denomination"
required min="1" placeholder="e.g. 100">';
$out .= '<div class="form-text">The amount in Ğ1 you consider fair exchange.</div>';
$out .= '</div>';
// Duration
$out .= '<div class="mb-3">';
$out .= '<label class="form-label" for="duration_days">Duration (days) <span class="text-danger">*</span></label>';
$out .= '<input type="number" class="form-control" id="duration_days" name="duration_days"
required min="1" max="365" placeholder="e.g. 14">';
$out .= '</div>';
$out .= '<div class="mt-3">';
$out .= '<button type="submit" class="btn btn-primary">Register Signal</button>';
$out .= '</div>';
$out .= '</form>';
$out .= '</div>';
return $out;
}
// ----------------------------------------------------------------------------
// Ğ1 CERTIFICATION CANDIDATE LIST
// ----------------------------------------------------------------------------
function cry01_render_g1_candidates($association_slug) {
// Renders the operator-only list of SASE participants with registered Ğ1 keys.
// TODO: load candidate list from orchestrator spool.
$out = '<div class="cry01-content">';
$out .= '<h3>Ğ1 Certification Candidates</h3>';
$out .= '<p class="text-muted">SASE participants who have registered a Ğ1 public key and are eligible for web of trust certification.</p>';
$out .= '<p class="text-muted fst-italic">No candidates registered yet.</p>';
$out .= '</div>';
return $out;
}
// ----------------------------------------------------------------------------
// MANAGE
// ----------------------------------------------------------------------------
function cry01_render_manage($association_slug) {
// Renders the operator manage page: cache refresh and config status.
// The Ğ1 public key comes from the wallet session — not from config.
$cache = cry01_read_cache();
$refreshed = $cache['refreshed_at'] ?? null;
$config = cry01_load_config();
$has_rpc = !empty($config['g1_rpc_endpoint']);
$manage_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/manage';
$out = '<div class="cry01-content">';
$out .= '<h3>Value Layer — Manage</h3>';
// Config status
$out .= '<div class="mb-3">';
$out .= '<h5>Configuration</h5>';
$out .= '<p>' . ($has_rpc ? '✓' : '✗') . ' Duniter RPC endpoint: ';
$out .= $has_rpc
? '<span class="text-success">configured</span>'
: '<span class="text-danger">missing</span>';
$out .= '</p>';
$out .= '<p class="text-muted small">Your Ğ1 public key comes from your Ğ1 Wallet session — not from config. ';
$out .= 'Unlock your wallet at <a href="' . z_root() . '/g1wallet">Ğ1 Wallet</a>, then return here to refresh.</p>';
$out .= '</div>';
// Cache status
$out .= '<div class="mb-3">';
$out .= '<h5>Balance Cache</h5>';
if ($refreshed) {
$cached_key = $cache['g1_pubkey'] ?? '';
$stale = cry01_cache_is_stale() ? ' (stale)' : ' (current)';
$out .= '<p>Last refreshed: ' . cry01_h($refreshed) . $stale . '</p>';
if ($cached_key) {
$out .= '<p class="text-muted small">Cached key: ' . cry01_h(substr($cached_key, 0, 16) . '...') . '</p>';
}
} else {
$out .= '<p class="text-muted">Cache has not been populated yet.</p>';
}
$out .= '</div>';
// Refresh form — g1_pubkey comes from wallet session via hidden field.
// g1wallet.js populates data-g1wallet-target="pubkey" fields on unlock.
$out .= '<form method="post" action="' . $manage_url . '">';
$out .= cry01_csrf_token();
$out .= '<input type="hidden" name="action" value="refresh_cache">';
$out .= '<input type="hidden" name="g1_pubkey" id="manage_g1_pubkey" data-g1wallet-target="pubkey">';
$out .= '<button type="submit" class="btn btn-sm btn-outline-primary">Refresh Balance Cache</button>';
$out .= '<span class="text-muted small ms-2">Requires active Ğ1 Wallet session.</span>';
$out .= '</form>';
$out .= '</div>';
return $out;
}