Reference
GUI Reference
eIOU GUI Reference
Complete documentation for the eIOU Docker node web-based GUI.
Table of Contents
- Architecture Overview
- Directory Structure
- Controllers
- Layout Components
- Session Management
- Security Features
- Helpers
- Known Limitations
- Development Setup
- See Also
Architecture Overview
The eIOU GUI is a server-rendered PHP web application optimized for Tor Browser compatibility. It follows an MVC-inspired pattern with controllers handling POST requests and HTML templates rendering the view.
Request Flow
Browser Request
|
v
index.html (Entry Point)
|
+-- Authentication check (Session.php)
| |
| +-- Not authenticated --> authenticationForm.html
| |
| +-- Authenticated --> Continue
|
+-- Functions.php (Central Router)
| |
| +-- POST Request? --> Route to Controller
| | |
| | +-- ContactController
| | +-- TransactionController
| | +-- SettingsController
| |
| +-- Initialize View Data
| |
| +-- Balances, Transactions
| +-- Contacts (all states)
| +-- Address Types
| +-- Notification Tracking
|
+-- wallet.html (Main Layout)
|
+-- Include walletSubParts/*.html
|
v
Rendered HTML Response
Key Characteristics
| Characteristic | Description |
|---|---|
| Rendering | Server-side PHP with embedded HTML |
| JavaScript | Minimal client-side JS for UX enhancements |
| Compatibility | Optimized for Tor Browser (no WebSockets, limited JS) |
| State Management | Session-based with CSRF protection |
| Styling | Inline CSS via page.css include |
| Forms | Traditional POST form submissions with redirect |
| AJAX | Limited use for specific features (ping, tx drop, debug report) |
| Amount precision | Every currency amount label is clickable: it opens a detail popup with the full 8-decimal (1e-8) value, independent of the displayDecimals setting. Dashboard totals carry exact whole/frac; modal-rendered amounts (contact balance/credit, transaction amounts) carry the major-units float, formatted to 8 decimals on open |
Directory Structure
files/src/gui/
├── controllers/ # Request handlers
│ ├── ContactController.php # Contact CRUD operations
│ ├── TransactionController.php # Transaction processing
│ ├── SettingsController.php # Settings and debug operations
│ ├── PaybackMethodsController.php # Payback-methods CRUD + reveal gate
│ ├── DlqController.php # Dead letter queue retry/abandon (AJAX)
│ └── PluginController.php # Plugin list/toggle/public-routes/upload/upgrade (AJAX)
│
├── functions/
│ └── Functions.php # Central router and view data initializer
│
├── helpers/
│ ├── MessageHelper.php # Message formatting and parsing
│ ├── ViewHelper.php # HTML rendering utilities
│ └── ContactDataBuilder.php # Contact data structure builder
│
├── includes/
│ └── Session.php # Session management and security
│
├── layout/
│ ├── authenticationForm.html # Login page
│ ├── wallet.html # Main wallet layout (includes subparts)
│ └── walletSubParts/ # Modular UI components
│ ├── banner.html # Dynamic image banner carousel
│ ├── header.html # Page header with Download icon + logout
│ ├── downloadModal.html # Global export modal (entity + filters + format + view)
│ ├── notifications.html # Toast and banner notifications
│ ├── quickActions.html # Quick action cards
│ ├── walletInformation.html # Balance and address display
│ ├── eiouForm.html # Send transaction form
│ ├── contactForm.html # Add contact form
│ ├── contactSection.html # Contact list and modal
│ ├── transactionHistory.html # Transaction list and modal
│ ├── dlqSection.html # Dead letter queue management
│ ├── pluginsSection.html # Plugins tab (list, enable/disable, public-routes toggle, upload/upgrade)
│ ├── pluginsTab.html # Plugins tab nav entry
│ ├── paybackMethodsSection.html # Payback methods dashboard section (list + "How payback methods work" intro)
│ ├── paybackMethodForm.html # Two-step Add / Edit / View modal (type picker → per-type fields)
│ ├── settingsSection.html # Settings form and debug panel
│ ├── floatingButtons.html # Back-to-top and refresh buttons
│ └── analyticsConsentModal.html # One-time analytics opt-in modal
│ (wallet.html also contains inline modal definitions for Transaction Details and What's New)
│
└── assets/
├── css/
│ ├── page.css # Main wallet styles
│ └── authentication-form.css # Login page styles
├── js/
│ └── script.js # Client-side JavaScript
└── fontawesome/ # Icon library
Controllers
Controllers handle POST requests routed through Functions.php. All controllers follow a consistent pattern:
- Verify CSRF token
- Validate and sanitize input
- Execute operation via service layer
- Redirect with message
ContactController
Handles all contact-related operations.
| Method | Action Value | Description | Parameters |
|---|---|---|---|
handleAddContact() |
addContact |
Add new contact | address, name, fee, credit, currency, description (optional) |
handleAcceptContact() |
acceptContact |
Accept pending request | contact_address, contact_name, contact_fee, contact_credit, contact_currency |
handleDeleteContact() |
deleteContact |
Delete contact | contact_address |
handleBlockContact() |
blockContact |
Block contact | contact_address |
handleUnblockContact() |
unblockContact |
Unblock contact | contact_address |
handleEditContact() |
editContact |
Update contact settings | contact_address, contact_name, contact_fee, contact_credit, contact_currency |
handlePingContact() |
pingContact |
Check contact status (AJAX) | contact_address |
handleProposeChainDrop() |
proposeChainDrop |
Propose dropping missing tx (AJAX) | contact_pubkey_hash |
handleAcceptChainDrop() |
acceptChainDrop |
Accept tx drop proposal (AJAX) | proposal_id |
handleRejectChainDrop() |
rejectChainDrop |
Reject tx drop proposal (AJAX) | proposal_id |
handleAcceptCurrency() |
acceptCurrency |
Accept pending incoming currency | pubkey_hash, currency, fee, credit |
handleAddCurrency() |
addCurrency |
Add a new currency to an existing contact (AJAX) | pubkey, currency, fee, credit |
handleAcceptAllCurrencies() |
acceptAllCurrencies |
Accept all pending currencies for a contact (AJAX) | pubkey_hash, currencies (JSON array), is_new_contact, contact_address, contact_name |
handleApplyContactDecisions() |
applyContactDecisions |
Batched per-currency decisions for a pending contact request — accept some, decline others, leave the rest deferred. Used by the Contact Request modal in place of the per-row Accept/Decline + Accept-All buttons. Thin POST adapter — partition / declines-first / first-accept-via-add logic lives in the shared ContactDecisionService::apply() so the GUI, the eiou contact apply / accept CLI, and POST /api/v1/contacts/:hash/decisions all share one implementation |
pubkey_hash, decisions (JSON array of {currency, action: "accept"|"decline", fee?, credit?} — defer rows omitted by client), is_new_contact, contact_address, contact_name |
handleDeclineContact() |
declineContact |
Decline every pending currency on an incoming contact request in one shot. Mirrors eiou contact decline <pubkey-hash> and POST /api/v1/contacts/:hash/decline so all three surfaces share the same effect — useful for “reject this whole request” without enumerating its currencies |
pubkey_hash |
handleDeclineCurrency() |
declineCurrency |
Decline a single pending currency on a contact (or contact request), leaving the others untouched, rather than declining the whole request | pubkey_hash, currency |
AJAX Response Format (pingContact):
{
"success": true,
"contact_name": "Bob",
"online_status": "online",
"chain_valid": true,
"message": "Ping complete"
}
Internally, the ping/pong protocol exchanges per-currency data: prevTxidsByCurrency (chain heads per currency), chainStatusByCurrency (per-currency chain validity), availableCreditByCurrency (per-currency available credit), and creditLimitByCurrency (the raw credit the contact extends to us). The AJAX response aggregates chain validity into a single chain_valid boolean. Per-currency available credit is stored in the contact_credit table and displayed in the contact modal.
If you required a minimum credit limit when adding a contact, each pong’s creditLimitByCurrency is checked against that minimum. When a contact grants you less than you required, the contact row shows a warning icon beside its Accepted status and the contact modal’s Status tab shows a “Credit Requirement” notice; clicking the row-header icon jumps to that notice. The flag clears on its own once they grant at least the minimum. This only surfaces the mismatch, since a contact’s extended credit is their own setting and cannot be forced.
When the remote node responds with status: rejected, ContactStatusService::describePingRejection() translates the reason code into a sentence the user can act on rather than echoing the raw token. unknown_contact (the remote responded but doesn’t recognize this user — almost always because the local-side acceptance hasn’t been delivered yet) reads “Contact is online but doesn’t recognize you yet — your acceptance hasn’t been delivered to them. Check the Failed Messages panel under Activity; delivery will keep retrying in the background.” blocked and disabled get reason-specific phrasings; any unrecognized reason falls back to the existing brief form so the message stays accurate as new reasons are added upstream.
The Apply button on the Contact Request modal goes through the page-wide loader pattern (showLoader + startOperationTimeout, mirroring payment-request approve) so a degraded transport — slow Tor circuit or self-signed TLS hop — can’t lock up the UI while the user-initiated outbound notification is in flight. Local state flips immediately; the notification is sent fire-and-forget by ContactSyncService (one synchronous attempt, then DLQ retries on failure) and the GUI auto-reloads after 15s with a post-reload toast pointing at the Failed Messages panel.
TransactionController
Handles transaction operations.
| Method | Action Value | Description | Parameters |
|---|---|---|---|
handleSendEIOU() |
sendEIOU |
Send eIOU transaction | recipient or manual_recipient, address_type, amount, currency, description |
handleCheckUpdates() |
GET check_updates=1 |
Poll for updates | last_check (timestamp) |
handleApproveP2p() |
approveP2pTransaction |
Approve a pending P2P transaction (AJAX) | hash, optional candidate_id for best-fee mode |
handleRejectP2p() |
rejectP2pTransaction |
Reject a pending P2P transaction (AJAX) | hash |
handleGetP2pCandidates() |
getP2pCandidates |
Fetch P2P route candidates for best-fee approval (AJAX) | hash |
handleGetTransactionByTxid() |
getTransactionByTxid |
Fetch a single transaction’s detail (AJAX) | txid |
handleRefund() |
refundTransaction |
Return a received transaction to its original sender, in full or in part (“Return to sender”, AJAX) | txid, optional amount (omit for the full remaining) |
handleGetRefundsForTxid() |
getRefundsForTxid |
List the refunds already issued against a received transaction (AJAX) | txid |
Recipient Resolution:
- If
manual_recipientis provided, use as-is (P2P routing) - If
recipient+address_type, look up specific address - Fallback to
recipientname (backend resolution)
SettingsController
Handles settings, encrypted backup management, and debug operations.
| Method | Action Value | Description | Parameters |
|---|---|---|---|
handleUpdateSettings() |
updateSettings |
Save wallet settings | Multiple settings fields |
handleListGuiBackups() |
listGuiBackups |
List scheduled and archive-triggered full snapshots (CSRF-gated JSON) | None |
handleCreateGuiBackup() |
createGuiBackup |
Create an encrypted full-database backup (sensitive-access JSON) | None |
handleVerifyGuiBackup() |
verifyGuiBackup |
Verify that a currently listed backup decrypts and contains valid SQL (sensitive-access JSON) | filename |
handleResetToDefaults() |
resetToDefaults |
Wipe every saved setting back to build defaults via UserContext::resetToDefaults() — empties defaultconfig.json so every setting getter falls back to its Constants::-backed default; clears the name key from userconfig.json. Identity fields, contacts, transactions, backups, and API keys are untouched. CSRF-gated. GUI confirmation modal requires typing reset before the submit button enables |
csrf_token |
handleClearDebugLogs() |
clearDebugLogs |
Clear debug entries | None |
handleSendDebugReport() |
sendDebugReport |
Generate debug file | description |
handleGetDebugReportJson() |
getDebugReportJson |
Download debug JSON (AJAX) | description, report_mode |
handleSubmitDebugReport() |
submitDebugReport |
Submit debug report to support via Tor (AJAX, non-blocking) | description, report_mode |
handleAnalyticsConsent() |
analyticsConsent |
Save one-time analytics consent choice (AJAX) | consent (0 or 1) |
What’s New actions (handled directly in Functions.php, not via a controller):
| Action Value | Description | Parameters |
|---|---|---|
whatsNewDismiss |
Mark current version’s “What’s New” as seen (AJAX) | None |
whatsNewNotes |
Fetch release notes from GitHub for a version (AJAX, cached) | version |
Available Settings:
| Setting | Type | Description |
|---|---|---|
name |
string | Display name shared via QR codes (saved to userconfig.json) |
sessionTimeoutMinutes |
int | Session inactivity timeout (5, 10, 15, 30, or 60 minutes) |
defaultCurrency |
string | Default currency code |
defaultFee |
float | Default fee percentage |
minFee |
float | Minimum fee amount |
maxFee |
float | Maximum fee percentage |
defaultCreditLimit |
float | Default credit limit |
maxP2pLevel |
int | Maximum P2P routing hops |
p2pExpiration |
int | P2P routing request timeout (seconds); P2P transactions get an extra 120s delivery window after this expires |
directTxExpiration |
int | Direct (non-P2P) transaction delivery timeout in seconds; 0 = no expiry (default); recommended: 120s (two Tor round-trips) |
maxOutput |
int | Max display lines |
defaultTransportMode |
string | Preferred transport (http/https/tor) |
autoRefreshEnabled |
bool | Auto-refresh when transactions pending |
contactAvatarStyle |
string | Contact avatar rendering style (gradient, pixel, tile) |
amountColorScheme |
string | Color scheme for transaction amounts (neutral, western, eastern) |
statusColorScheme |
string | Color scheme for status badges (neutral, western, eastern) |
autoBackupEnabled |
bool | Enable automatic daily database backups |
updateCheckEnabled |
bool | Check Docker Hub daily for newer versions (read-only API call) |
autoAcceptTransaction |
bool | Auto-accept P2P transactions when route found (when OFF, transactions pause at awaiting_approval for user review in both fast and best-fee modes) |
syncChunkSize |
int | Transactions per sync chunk (10-500) |
syncMaxChunks |
int | Max sync chunks per cycle (10-1000) |
heldTxSyncTimeoutSeconds |
int | Held tx sync timeout in seconds (30-299) |
Advanced Settings are organized into categories via a dropdown selector:
| Category | Settings |
|---|---|
| Feature Toggles → Contacts | contactStatusEnabled, contactStatusSyncOnPing, autoAcceptRestoredContact, autoRejectUnknownCurrency |
| Feature Toggles → Transactions | autoAcceptTransaction, hopBudgetRandomized, autoChainDropPropose, autoChainDropAccept, autoChainDropAcceptGuard |
| Feature Toggles → GUI | autoRefreshEnabled, hideEmptyGuiSections |
| Feature Toggles → System | apiEnabled, autoBackupEnabled, updateCheckEnabled, analyticsEnabled |
| Backup & Logging → Backup | backupCronTime, backupRetentionCount |
| Backup & Logging → Logging | logMaxEntries, logLevel |
| Data Retention → Cleanup | cleanupDeliveryRetentionDays, cleanupDlqRetentionDays, cleanupHeldTxRetentionDays, cleanupRp2pRetentionDays, cleanupMetricsRetentionDays |
| Data Retention → Archive | paymentRequestsArchiveRetentionDays, paymentRequestsArchiveBatchSize, transactionsArchiveRetentionDays, transactionsArchiveBatchSize |
| Rate Limiting → Throughput | p2pRateLimitPerMinute |
| Rate Limiting → Attempt Blocking | rateLimitMaxAttempts, rateLimitWindowSeconds, rateLimitBlockSeconds |
| Sync | syncChunkSize, syncMaxChunks, heldTxSyncTimeoutSeconds |
| Network → Transport Timeouts | httpTransportTimeoutSeconds, torTransportTimeoutSeconds |
| Network → Tor Resilience | torCircuitMaxFailures, torCircuitCooldownSeconds, torFailureTransportFallback, torFallbackRequireEncrypted |
| Network → Routing & Delivery | maxP2pLevel, p2pExpiration, directTxExpiration |
| Network → API | apiCorsAllowedOrigins |
| Currency | allowedCurrencies |
| Display | displayDecimals, displayDateFormat, displayRecentTransactionsLimit, maxOutput, sessionTimeoutMinutes, contactAvatarStyle, amountColorScheme, statusColorScheme |
DlqController
Handles dead letter queue management actions (AJAX only — all responses are JSON).
| Method | Action Value | Description | Parameters |
|---|---|---|---|
handleRetry() |
dlqRetry |
Re-send a failed message to its original recipient | dlq_id, csrf_token |
handleAbandon() |
dlqAbandon |
Mark a DLQ item as abandoned | dlq_id, csrf_token |
handleRetryAll() |
dlqRetryAll |
Retry all retryable DLQ items in bulk (transaction + contact types only) | csrf_token |
handleAbandonAll() |
dlqAbandonAll |
Abandon all pending/retrying DLQ items | csrf_token |
Retry constraints:
- Only
transactionandcontactmessage types can be retried p2pandrp2pitems are rejected with an explanatory error — they are time-sensitive relay messages that expire in ≤300s and are stale by the time they reach the DLQ
Retry mechanism:
The controller re-sends the original signed payload (stored verbatim in the DLQ) directly to the recipient address using TransportUtilityService::send(). If the recipient returns a success status, the item is marked resolved. On failure it returns to pending.
ApiKeysController
Handles all API-key management done from the wallet GUI’s Settings tab (AJAX only — all responses are JSON). CSRF is validated on every call via Session::validateCSRFToken(..., false) (non-rotating, since the page fires many AJAX calls from a single load). Every destructive call is additionally gated by a short-lived “sensitive access” grant (see below).
| Method | Action Value | Description | Parameters |
|---|---|---|---|
status() (inline) |
apiKeysStatus |
Report whether the session currently holds a sensitive-access grant and its seconds remaining. Safe to call from any page render. | csrf_token |
verify() |
apiKeysVerify |
Verify the user’s auth code and open a Session::SENSITIVE_ACCESS_TTL_SECONDS grant (default 5 min) for this session. |
authcode, csrf_token |
clearAccess() (inline) |
apiKeysClearAccess |
Drop the sensitive-access grant immediately (user clicked “lock again”). | csrf_token |
listKeys() |
apiKeysList |
List every key on this node — name, key_id, permissions, rate_limit_per_minute, enabled, timestamps, expires_at. No sensitive-access required — key_id values are public identifiers and listing them is non-destructive. |
csrf_token |
createKey() |
apiKeysCreate |
Mint a new key. Returns the key_id + secret once; the secret is never retrievable again. Reuses ApiKeyService::validatePermissions() and validateRateLimit() so CLI and GUI agree on what’s accepted. |
name, permissions[], rate_limit_per_minute (optional, default 100), expires_in_days (optional, 0 or absent = never), csrf_token |
toggleKey() |
apiKeysToggle |
Enable or disable a single key. Idempotent — toggling a key that’s already in the target state returns success. | key_id, enable ("1" or "0"), csrf_token |
updateKey() |
apiKeysUpdate |
Edit a key’s label, rate limit, and/or expiry. Permissions are intentionally not editable (revoke + reissue to change scope). Expiry may only be shortened — a proposed timestamp later than the current expires_at returns expiration_extension_not_allowed. Each of name / rate_limit_per_minute / expires_in_days is independently optional; omitted fields stay untouched. |
key_id, any of name, rate_limit_per_minute, expires_in_days, csrf_token |
deleteKey() |
apiKeysDelete |
Permanently delete a single key. GUI requires the user to type the key’s label to confirm. | key_id, csrf_token |
| (inline) | apiKeysDisableAll |
Disable every currently-enabled key in one statement. Returns the affected count. |
csrf_token |
| (inline) | apiKeysDeleteAll |
Permanently delete every key on the node. GUI requires the user to type delete all verbatim before the destructive button enables. |
csrf_token |
Sensitive-access gate:
Every mutating action (apiKeysCreate, apiKeysToggle, apiKeysUpdate, apiKeysDelete, apiKeysDisableAll, apiKeysDeleteAll) calls $this->requireSensitive() before handling the request. Without an active grant the controller returns 401 sensitive_access_required with a descriptive message; the client responds by opening the verify modal, collecting the auth code, and retrying the original request automatically on a successful verify. The grant is bound to the session’s auth_time, so logout or a session-timeout rotation invalidates it immediately. Listing (apiKeysList, apiKeysStatus) is deliberately outside the gate because it’s read-only and leaks nothing the operator doesn’t already see.
Bulk-action safety:
- “Disable all” is only offered when ≥1 enabled key exists; the confirmation modal states the exact active count.
- “Delete all” is only offered when ≥1 key exists (enabled or disabled); the user must type
delete allinto a confirmation input before the destructive button enables. No bulk Enable — re-activation is intentionally one key at a time so a recently disabled key can’t be reactivated in a single careless click. - Both return a
countof affected rows so the client can toast the exact number processed.
Audit logging:
All GUI-driven creations, toggles, updates, deletions, and bulk operations log through the unified Logger facade — SecureLogger masks any eiou_* key-id patterns and any sk_* secret patterns, so the audit trail never contains a shown-once secret or correlates a key_id to an action in readable form in app.log.
Routing:
Dispatched from Functions.php via an allowlist of action names. A new action must be added to both the in_array($action, [...]) check in Functions.php and the switch ($action) in ApiKeysController::routeAction() — missing it from the allowlist causes Functions.php to fall through and render wallet.html (107 kB of HTML) instead of routing to the controller, which manifests client-side as a silent res.json() rejection.
AltCodeController
Handles status / set / rotate / clear of the user-chosen alternate auth code that complements the seed-derived primary auth code. AJAX only; JSON responses. CSRF validated on every call via Session::validateCSRFToken(..., false) (non-rotating). Set and clear are additionally rate-limited per-IP (5 attempts / 5 min / 15-min block) via the gui_altcode_modify bucket, separate from gui_login so attempts on the two surfaces can’t drain each other.
| Method | Action Value | Description | Parameters |
|---|---|---|---|
status() |
altCodeStatus |
Cheap presence probe — does an alt code exist on this node, was the current session authenticated via that alt code, and what is the minimum length the validator enforces. Used by the settings panel to decide whether to render the rotate / clear buttons and whether to lock them. | csrf_token |
setAlt() |
altCodeSet |
Set or rotate the alt code. Requires the primary auth code re-entered in band — not via the sensitive-access grant, and never accepted via the alt code itself. Validates strength through AltCodeValidator (≥12 chars; ≥1 each of upper/lower/digit/symbol; no triple-repeats; no monotonic runs ≥4; not a common-password substring). Refuses outright for alt-authenticated sessions with alt_session_forbidden. |
primary_authcode, new_alt_code, csrf_token |
clearAlt() |
altCodeClear |
Remove the alt code. Same primary-required, alt-session-forbidden gating as altCodeSet. |
primary_authcode, csrf_token |
Why primary-only for set/rotate/clear: The alt code may not rotate or clear itself. Otherwise an attacker who learned the alt code (shoulder-surfing, leaked password manager, etc.) could overwrite it with one only they know and lock the legitimate operator out. The seed-derived primary always retains control; recovery from forgetting the alt code is “log in with the primary, rotate the alt.” Recovery from forgetting the primary is the seed phrase, exactly as before this feature.
Constant-time verification (defense in depth):
The verifier the controller delegates to (AltCodeVerifier::verify()) always runs an Argon2id check, against a per-process placeholder hash when no real hash is configured. Combined with the equivalent change in Session::authenticate(), this prevents a network observer from inferring alt-code presence by timing failed-login responses (~50 ms with vs ~µs without otherwise).
Rate limit semantics:
The alt_session_forbidden refusal fires before the rate-limit gate, so an alt-code-only session can never exhaust the bucket and lock the legitimate operator out of rotating. Bucket is shared between altCodeSet and altCodeClear so attempts can’t be laundered across endpoints.
Self-rotation lockout UX (when authenticated via alt):
The settings panel renders an alert-warning callout above the action row explaining the restriction. The Rotate / Remove buttons render with aria-disabled="true" and a lock icon (rather than HTML disabled, which suppresses clicks entirely), so clicks on them route to an explainAltCodeLocked handler that scrolls the warning into view and flashes it — silent dead clicks would be a worse UX than a visible “here’s why”.
Routing:
Same dispatcher allowlist as every other AJAX controller — registered with GuiActionRegistry at TIER_AUTH. Reachable only on an authenticated session; index.html short-circuits unauthenticated requests to the login form before the dispatcher runs.
PaybackMethodsController
Handles every AJAX call the dashboard’s Payback Methods section and the contact modal’s Payback tab fire — CRUD for your own methods plus the synchronous E2E fetch used to pull a contact’s shareable methods over the wire. JSON-only; CSRF is validated on every call.
| Method | Action Value | Description | Parameters |
|---|---|---|---|
list() |
paybackMethodsList |
List this node’s methods (public shape, sensitive fields masked). Response also carries sensitive_access + seconds_remaining mirroring apiKeysList, so the section header can show “🔓 Unlocked for N min” without an extra round-trip. |
csrf_token |
create() |
paybackMethodsCreate |
Create a new method. Validates via PaybackMethodTypeValidator (which delegates to PaybackMethodTypeContract::validate() for plugin-registered types). Returns method_id. |
type, label, currency, fields[...], optional share_policy, optional priority, csrf_token |
update() |
paybackMethodsUpdate |
Patch an existing row. Accepts any of label, share_policy, priority, enabled, fields — sending fields re-encrypts the entire blob atomically. |
method_id, any of label, share_policy, priority, enabled, fields[...], csrf_token |
reveal() |
paybackMethodsReveal |
Return the method with all fields decrypted to plaintext. Used by Edit (to pre-populate sensitive inputs) and the per-field Copy buttons on the detail modal. | method_id, csrf_token |
remove() |
paybackMethodsRemove |
Permanently delete a method. | method_id, csrf_token |
setSharePolicy() |
paybackMethodsSetSharePolicy |
Atomic share-policy update without touching other fields. | method_id, share_policy, csrf_token |
fetchFromContact() |
paybackMethodsFetchFromContact |
Fire a synchronous payback-methods-request.v1 E2E round-trip at a contact and return their shareable methods inline. Nothing is cached — closing the tab drops the in-memory copy. Returns {status, methods, ttl_seconds}; status is one of ok / denied / rate_limited. Stale responses are dropped if the user switched contacts mid-flight. |
address, optional currency, csrf_token |
Sensitive-access gate:
All mutations (Create, Update, Remove, SetSharePolicy) and Reveal require an active sensitive-access grant — the GUI shares the same apiKeysVerify / apiKeysStatus / apiKeysClearAccess mechanism so a single unlock covers both API-key and payback-method edits for the grant’s lifetime. The client routes all seven calls through withSensitiveAccess(requestFn, onResponse, label) which opens apiKeysVerifyModal on a 401 sensitive_access_required response and retries on successful unlock. Listing (paybackMethodsList) and contact-fetch (paybackMethodsFetchFromContact) are outside the gate — list rows already show masked values, and the contact-fetch response is composed by the other node so there’s nothing sensitive for this node to re-gate.
E2E fetch mechanics (fetchFromContact):
The controller extracts the inner response body from the envelope that MessageService echoes back from MessageDeliveryService::sendSyncMessage(), normalizes the status strings, and returns them to the GUI. Underlying flow: request → receiver’s ReceivedPaybackMethodService::handleIncomingRequest() → receiver’s PaybackMethodService::listShareable() → response envelope back through the same E2E channel. Nothing is written to payback_methods_received on this flow — that table is reserved for a future cache layer.
Routing:
Same allowlist rule as every other AJAX controller — new actions must be added both to Functions.php’s action check and the controller’s switch dispatch.
PluginController
Handles every AJAX call the Plugins tab fires — listing installed plugins, toggling them on/off, the per-plugin public-routes toggle, uploads/upgrades, and uninstall. JSON-only; CSRF is validated on every call via the shared routeAction() dispatch.
| Method | Action Value | Description | Parameters |
|---|---|---|---|
listPlugins() |
pluginsList |
List installed plugins with metadata, enabled flag, status, and whether a restart is pending. | csrf_token |
togglePlugin() |
pluginsToggle |
Persist a plugin’s enabled flag (no restart). The response carries the re-classified public_routes_state, which the client adopts before re-rendering the row: a disabled plugin classifies as none, so without it a plugin the operator just enabled would show no Public routes toggle until a reload. When the plugin declares public routes but they won’t be served (this plugin’s toggle off under the default allow ceiling, or the node ceiling explicitly off), the response also carries public_routes_gated: true and public_routes_message, so the GUI raises a warning toast. |
name, enabled, csrf_token |
setPublicRoutes() |
pluginsSetPublicRoutes |
The per-plugin Public routes toggle (applies under EIOU_PUBLIC_PLUGIN_ROUTES=allow, the default ceiling). Persists the preference and re-renders nginx immediately. Response carries public_routes_enabled and public_routes_state (live / plugin_off / node_off / none), plus a public_routes_message when the state is node_off. Under the node-wide force-all ceiling (on) the row shows a read-only “on (node-forced)” badge instead of an interactive toggle. |
name, enabled, csrf_token |
requestRestart() |
pluginsRequestRestart |
Request a node restart to apply pending plugin state changes. | csrf_token |
showChangelog() |
pluginChangelog |
Return a plugin’s bundled changelog for display in the GUI. | name, csrf_token |
uninstallPlugin() |
pluginsUninstall |
Run the full uninstall sequence (must be disabled first). | name, csrf_token |
uploadPlugin() |
pluginsUpload |
Upload a new plugin archive; staged as disabled. | file upload, csrf_token |
uploadAsUpgrade() |
pluginsUploadAsUpgrade |
Upload an archive as an in-place upgrade of an installed plugin (old version preserved for rollback). | file upload, name, csrf_token |
upgradeBundled() |
pluginsUpgrade |
Upgrade an installed plugin to a newer image-bundled version. | name, csrf_token |
reportUploadLimits() |
pluginsUploadLimits |
Report the effective upload size limits so the client can pre-validate. | csrf_token |
The owned action names are kept in a single OWNED_ACTIONS constant so the live registration path and the loader-not-ready stub path cannot drift.
Layout Components
authenticationForm.html
Login page displayed when user is not authenticated.
| Element | Purpose |
|---|---|
| Password input | Auth code entry |
| Error message | Displayed on failed login |
| Loading overlay | Shown during submission |
wallet.html
Main layout container that includes all subpart components.
Tab Structure:
| Tab | Components |
|---|---|
| Dashboard | walletInformation.html |
| Send | eiouForm.html, paymentRequestsSection.html |
| Contacts | contactSection.html |
| Activity | transactionHistory.html, dlqSection.html |
| Plugins | pluginsSection.html |
| Settings | settingsSection.html, debugSection.html |
Include Order:
- banner.html
- header.html
- notifications.html
- walletInformation.html (Dashboard tab)
- eiouForm.html (Send tab)
- paymentRequestsSection.html (Send tab)
- contactSection.html (Contacts tab)
- transactionHistory.html (Activity tab)
- dlqSection.html (Activity tab)
- pluginsSection.html (Plugins tab)
- settingsSection.html (Settings tab)
- debugSection.html (Settings tab — appended below settings form)
- floatingButtons.html
- downloadModal.html (global export modal, hidden until opened)
- analyticsConsentModal.html
walletSubParts Components
banner.html
Loads and displays banner images from /gui/assets/banners/. Any image placed in that directory (jpg, jpeg, png, gif, svg, webp) is shown at the top of the wallet page. Files are sorted alphabetically. Used for promotional or informational banners.
| Element | Purpose |
|---|---|
| Banner carousel | Displays images from the banners directory |
header.html
| Element | Purpose |
|---|---|
| Wallet title | Branding with icon |
Download icon (data-action="openDownloadModal") |
Opens the global Download modal — single entry point for exporting transactions / payment requests / contacts / balances. See downloadModal.html. |
| Logout link | Ends session |
downloadModal.html
Global export entry point — triggered by the Download icon in the wallet header (and also from a smaller Download icon inside the contact modal’s Transactions tab, which pre-fills the modal with entity=transactions and the open contact’s pubkey hash).
| Element | Purpose |
|---|---|
| Entity radio | Pick what to export: Transactions / Payment requests / Contacts / Balances. Filter blocks below show/hide based on selection (syncDownloadModalFilters()). |
| Contact picker | Server-side rendered <select> populated from ContactRepository::getAcceptedContacts() at modal-template render time. Shared by transactions / payment_requests / balances. Pubkey-hash values; no fetch round-trip (Tor Browser compatible). |
| Per-entity status select | Different enums per entity (transactions = pending / sending / sent / accepted / rejected / cancelled / completed / failed; payment_requests = pending / sending / approved / failed / declined / cancelled / expired; contacts = accepted / pending / blocked). On submit the visible block’s value writes back to a shared status query param. |
| Transaction direction / payment-request direction | Direction filters; per-entity to match the entity-specific column (txns use type, payment_requests use direction). |
| Currency text input | Balances only. Free-text 3-10 char currency code. |
| Date range | UTC from / to date inputs. Only shown for transactions + payment_requests. to is autofilled with today (UTC) on every modal open; from stays empty so the default behaviour is “all history up to today”. |
| Format radio | CSV (Excel/Sheets, UTF-8 BOM, RFC 4180 quoting) or NDJSON (one JSON object per line). |
| View radio | Slim (accountant-friendly column set) or Full (audit-grade, includes addresses, signatures, internal fields). |
Download button (data-action="triggerDownload") |
Walks the visible filter blocks, builds a /gui/export.html?entity=…&… URL, and navigates the browser there. The response’s Content-Disposition: attachment header turns the navigation into a file download — Tor Browser compatible (no fetch / Blob / objectURL path). |
| Cancel button | Closes the modal. |
Reset on open: resetDownloadModalForm() runs at the top of every openDownloadModalWithPrefill() so the modal starts from a clean baseline regardless of any previous selections (entity → transactions, every select to its first “Any …” option, every text/date input cleared, format → CSV, view → slim, to → today UTC). The contact-modal entry then applies its entity + contactPkh prefill on top.
Stacking when opened from another modal: the contact-modal Download icon (data-action="openDownloadModalForContact") opens this modal with .modal-stack-top + bringModalToTop() so it renders above the contact modal; the class drops in closeDownloadModal.
Backend entry point: /app/eiou/www/gui/export.html — session-authenticated PHP entry point (CSRF not required, read-only). Accepts the modal’s query params (and contact=NAME as a CLI-parity alternative to contact_pubkey_hash=), validates, opens php://output, calls ExportService::export(). See docs//docs/reference/cli-reference (Export Commands) and docs//docs/reference/api-reference (Export Endpoints) for the same pipeline’s CLI and REST faces.
notifications.html
| Element | Purpose |
|---|---|
| Operation result toasts | Success/error messages from redirects |
| Tor connectivity status | Warning when Tor is unreachable, success toast when restored |
| Update available banner | Shows current vs available version with Docker pull command |
| What’s New banner | After upgrade, shows “See what’s new in vX.X.X” link that opens a release notes modal; dismissible per-version |
| In-progress banner | Shows pending transaction count |
| Pending contacts banner | Shows pending contact request count |
| Pending currency requests | Shows incoming currency requests from existing contacts |
| Tx drop proposal banner | Incoming proposals requiring action (red alert) |
| Completed transaction toasts | Notifications for finished transactions |
| Received transaction toasts | Notifications for incoming payments |
| DLQ notifications | Dead letter queue failure alerts |
quickActions.html
Note: This component is no longer included in the dashboard layout. Navigation is now fully tab-based. The file is retained but unused.
walletInformation.html
| Element | Purpose |
|---|---|
| Last updated timestamp + Refresh link | Shows data freshness with manual refresh |
| Total Balance | Aggregated wallet balance per currency (blue card) |
| Total Fee Earnings | P2P relay fee earnings per currency (amber/gold card) |
| Total Available Credit | Sum of available credit per currency (blue-purple card), from ping/pong, ~5 min refresh |
| User Addresses | HTTP/HTTPS/Tor with Copy and QR code buttons |
| Scan Contact QR | Opens camera scanner — on scan, switches to Contacts tab and opens Add Contact modal with pre-filled address and name |
| Public Key | Wallet public key with copy button |
| Status | Always “Active” |
All three dashboard cards display per-currency rows. When a card has no data for a given category, it shows “0.00” with the currency derived from other data sources for consistency.
The ⓘ icons next to “Total Fee Earnings” and “Total Available Credit” open a small info modal on click (tap-friendly on mobile).
QR Code Format: QR codes use a typed JSON envelope for forward compatibility:
| Type | Format | Description |
|---|---|---|
contact |
{"type":"contact","address":"...","name":"..."} |
Add contact (name optional) |
payment |
{"type":"payment","address":"...","amount":...,"currency":"...","description":"..."} |
Payment request (future) |
Legacy plain-text QR codes (just an address string) are parsed as type contact for backward compatibility.
eiouForm.html
The New eIOU card leads with two sub-tabs under its header (Send eIOU and Request Payment), backed by a single shared recipient picker. Send is the universal send (a saved contact or a raw P2P address); Request Payment is contact-only and hides the send-only fields.
| Field | Type | Modes | Description |
|---|---|---|---|
| recipient | hidden | both | Selected contact name (set by the recipient search) |
| address_type | select | both | Address type for the selected contact (options sorted Tor > HTTPS > HTTP, most-secure auto-selected) |
| manual_recipient | text | send only | Direct address entry for P2P routing to a non-contact |
| amount | number | both | Transaction amount |
| currency | select | both | Currency code (user’s allowed currencies; filtered to the contact’s accepted currencies once a contact is selected) |
| description | text | both | Optional memo |
| best_fee | checkbox | send only | [Experimental] Best-fee routing: collects all route responses and selects the lowest fee |
Sub-tab behaviour (setSendMode):
- Send mode points the form at
sendEIOU, shows the manual-address entry (when no contact is picked) and the Best Fee Route toggle, and shows the Send eIOU submit. - Request mode points the form at
createPaymentRequest, hides the manual-address and Best Fee Route fields, swaps in request-specific hint text that makes no reference to P2P routing or relay nodes (a request is a direct, contact-only message), and shows the Request Payment submit. That submit is disabled until a contact is selected, and runs the browser’s native field validation before submitting (parity with the Send submit). - Elements that differ by mode carry
.send-mode-only/.request-mode-only;setSendModetoggles their inlinedisplay(inline rather than a.d-noneclass because.form-hint’sdisplayrule would otherwise win the cascade).
Features:
- Dynamic address type selector (options sorted by security preference: Tor > HTTPS > HTTP, most-secure auto-selected)
- Dynamic currency dropdown: all allowed currencies when no contact selected, filtered to the contact’s accepted currencies once one is
- Opened prefilled from a contact via the contact modal’s Send eIOU / Request Payment buttons (
sendToContact/requestFromContact→openSendFormForContact), which select the matching sub-tab and focus Amount
paymentRequestsSection.html
Rendered below the Send form in the Send tab. Shows incoming and outgoing payment requests.
Pending Requests callout: When there are any pending requests (incoming + outgoing combined), they render inside a .pending-callout .pending-callout-has-body yellow card with a header showing the count and direction breakdown. The shared .pending-callout-* classes are reused by dlqSection.html for the same visual treatment on Failed Messages — both sections present “you have N pending items” with identical chrome (yellow gradient, count chip, embedded white-row table).
Incoming Requests (pending):
| Element | Purpose |
|---|---|
| Requester name/address | Who sent the request |
| Amount + currency | Requested amount |
| Description | Optional note from requester |
| Pay button | Opens a confirmation modal pre-loaded with the requester’s description (read-only) and an optional payer-note textarea. The on-chain description becomes "payment: <requester desc> | <your note>" if the note is filled. A live counter under the textarea shows remaining chars and decrements as the user types — the cap is computed dynamically against this specific request’s description (max_note = 255 − len(desc) − 3) so the joined string always fits the on-chain ceiling. If the requester’s description already fills the budget, the textarea is disabled and the user can still hit Pay (with no note). Same modal is used whether the user clicks Pay on the inline pending-row form or the Pay button inside the row-click “Payment Request Details” drill-down modal — the submit handler is event-delegated so dynamically-rendered Pay forms get caught too. |
| Decline button | Rejects the request, notifies requester |
Outgoing Requests (pending):
| Element | Purpose |
|---|---|
| Recipient name/address | Who you sent the request to |
| Amount + currency | Requested amount |
| Description | Optional note |
| Status badge | Pending / Approved / Declined |
| Cancel button | Cancels the outgoing request |
Resolved requests (approved/declined) appear in a collapsed history section. Approved requests show a clickable truncated txid that opens the transaction detail modal.
History Paginator (resolved requests only):
- Same shared
PaginatorIIFE as the Recent Transactions and Contacts tables. Page buttons + size selector (25 / 50 / 100 / All), persisted aseiou_paginator_size_payment-requests. - “Load older” button — fetches next server-side page via
loadMorePaymentRequestsGUI AJAX action. Backed byPaymentRequestRepository::getResolvedHistoryPage($limit, $offset, ?$cursor). When the client posts acursor(default for second-and-later pages), the query uses a keyset predicate on(COALESCE(responded_at, created_at), id)and runs in constant time regardless of depth; without a cursor it falls back toLIMIT ? OFFSET ?on the same ORDER BY. Sort key matches the initial template’susortso pages append cleanly. Rows rendered via a shared_paymentRequestRow.htmlpartial. - “Showing the last N requests” counter is dynamic via
#pr-meta-loaded-count; updates after each Load-older click.
Search database (server-side):
- Same pattern as Recent Transactions — button next to the search input, or Enter in the search box. Fires
searchPaymentRequestsGUI AJAX action →PaymentRequestRepository::searchResolvedHistory(LIKE acrosscontact_name,description,requester_pubkey_hash,recipient_pubkey_hash,requester_address). - Respects the direction / status filter selects. Hard-capped at 500 rows; response includes
{html, total, capped, cap}. - Result rows replace the tbody; banner shows
N matches for "term" · Clear search; Clear reloads the page.
contactForm.html
Note: This file contains the Add Contact form fields. It is included as part of the Add Contact modal inside
contactSection.html, not as a standalone include inwallet.html.
| Field | Type | Description |
|---|---|---|
| address | text | Contact node address (placeholder: “Enter Tor (.onion) or HTTP(S) address”, with QR scan button) |
| name | text | Display name |
| credit | number | Credit limit (default from settings) |
| fee | number | Fee percentage (default from settings) |
| currency | select | Currency code (dynamically populated from user’s allowed currencies) |
| description | text | Optional message sent with the contact request (max 255 chars). For non-Tor contacts, sent as a separate E2E encrypted follow-up after key exchange. For Tor contacts, included directly (protected by Tor transport encryption) |
contactSection.html
The Contacts tab. The contact list is shown first. The “Add Contact” form is accessed via the “+ New Contact” button which opens a modal dialog (the form fields from contactForm.html are embedded in this modal, not rendered inline).
Contact Grid:
- Accepted contacts with balance
- Pending user contacts (outgoing)
- Blocked contacts
- Search/filter functionality
- Show more toggle (>16 contacts)
- Shared paginator — page-size selector (25/50/100/All, persisted per-table via
safeStorageSetundereiou_paginator_size_contacts) + prev/next buttons + range indicator, installed byPaginator.create('contacts', …)inscript.js. Orthogonal.filter-hidden/.paginator-hiddenclasses on rows so the local search + status/chain/online filters keep working in combination with pagination - “Load more” button (accepted contacts only — pending + blocked are always rendered up-front because they’re bounded and operationally important). Fetches the next server-side page via the
loadMoreContactsGUI AJAX action, rendering rows through a shared_contactRow.htmlpartial so appended rows are byte-identical to the initial render - Tx drop proposal badges on contact cards:
- Red “Action Required” — incoming proposal needs acceptance
- Blue “Awaiting Response” — outgoing proposal pending
- Orange “Blocked” — rejected proposal, transactions blocked
- Yellow triangle — chain invalid, no proposal yet
Contact Modal (Tabbed):
| Tab | Contents |
|---|---|
| Info | Per-currency balance, credit limit, fee, your/their available credit (via horizontal currency slider pills), addresses (with Copy and QR code buttons; QR regenerates when switching address types), public key (single-line display with Copy button), contact ID. A Send eIOU / Request Payment button pair sits directly under the balance (accepted contacts only); each opens the New eIOU form on the matching sub-tab with this contact prefilled and Amount focused. |
| Transactions | Recent transactions with this contact. A small Download icon (data-action="openDownloadModalForContact") sits next to the Refresh button — opens the global Download modal with entity=transactions and this contact’s pubkey hash pre-selected. |
| Payback | Live E2E fetch of the contact’s shareable payback methods (bank wire, PayPal, BTC, etc.). Opens a synchronous payback-methods-request.v1 round-trip through the node; response is rendered inline and never persisted to disk or localStorage. Currency dropdown defaults to the currency of the largest debt owed, filters the fetch when changed (auto-refetches). Rows are clickable → per-method detail modal with per-field Copy buttons. Renders dedicated copy for denied / rate_limited states. |
| Status | Online status, chain status (proposal-aware, clickable — switches to this tab), Check Status button, tx drop resolution section (propose/accept/reject) |
| Settings | Edit form, block/unblock/delete buttons |
Every tab has a collapsible About
Tx Drop Resolution Section (in Info tab):
- Shown when chain has a gap or active proposal
- Sub-sections: propose, awaiting acceptance, incoming (accept/reject), rejected (repropose)
- Funds warning: dropping a transaction removes its funds from both balances
- Chain status badge clicks scroll to this section
Pending Contact Requests Section:
- Lists incoming requests with direction-aware currency display
- Outgoing currencies shown as read-only badges (“Awaiting their acceptance”)
- Incoming currencies shown in a batched-decision modal (“They requested”):
- Each currency renders as an accordion with a 3-state segmented control (Accept / Decline / Defer) and fee/credit inputs visible only when Accept
- Smart default: Accept for currencies in the user’s allowed list, Defer for “new” currencies (not in
Allowed Currencies) - State badge in each accordion summary (✓ Accept / ✗ Decline / ⏳ Defer) — visible without expanding
- Accordion auto-collapses on Decline/Defer (nothing to configure), re-opens on Accept
- Apply button at the bottom commits everything in a single
applyContactDecisionsPOST. Label updates live: “Accept request” / “Accept all N currencies” / “Decline N” / “Apply: X accept, Y decline”; disabled when every row is Defer - Defaults guard: one confirmation modal if any Accept rows are at both default fee + default credit
- Inline help disclosure (“How to handle this request”) collapsed by default at the top of the modal
- “About this contact” reference section below the action area (addresses, public key)
- Delete Request silently drops the whole request (other side not notified). Block Contact prevents future requests
- Legacy fallback form preserved for contacts without
contact_currenciesdata
transactionHistory.html
In-Progress Section:
- Phase indicators (pending, route_search, route_found, sending, syncing)
- Direct badge shown for non-P2P transactions (P2P type is implied by route/fee info)
- Held transaction notices
- P2P approval gate: when
autoAcceptTransactionis OFF, transactions pause atawaiting_approval(blue badge) with route selection UI. Send amount shown inline before candidates- Fast mode: shows 1 route with fee breakdown, Accept/Reject buttons
- Best-fee mode: lists all returned routes ordered by fee (lowest first), user picks one. On mobile (≤576px) candidate rows stack vertically
- Route count updates dynamically as late-arriving candidates are received
- Node fee hidden during awaiting_approval (route fee shown per-candidate instead)
Transaction Table Columns:
| Column | Description |
|---|---|
| Status icon | Leading column, no header. Single check (sent/accepted), double check (completed), hourglass (pending), cross (rejected), ban (cancelled). Muted grey for normal states; amber for pending, red for rejected |
| Counterparty | Avatar + contact name |
| Amount | Sortable. ±value carries sent/received direction |
| Description | CSS-truncated; full text on hover |
| Type | Direct / P2P / Contact badge |
| Origin / Dest. | P2P only: final destination (sent) or original sender (received) |
| Date | Sortable, formatted timestamp |
- Type badges (Contact, P2P, Direct)
- Click any row to open detail modal
Paginator:
- Shared
PaginatorIIFE inscript.js— same infrastructure Contacts and Payment Requests history use. Page-size selector (25 / 50 / 100 / All, persisted per-table), prev/next buttons, range indicator (“101–125 of 200”). - “Load older” button — fetches the next server-side page via the
loadMoreTransactionsGUI AJAX action, rendering rows through the_transactionHistoryRow.htmlpartial that both the initial foreach and the AJAX handler consume. Client concatenates the returneddata.rowsonto the in-memorytransactionData[]soopenTransactionModal(index)keeps resolving for appended rows. Button auto-hides when the server reportsexhausted: true. - “Showing the last N transactions” counter updates dynamically as rows are loaded or replaced. Uses a
#tx-meta-loaded-countspan rewritten byrefreshMetaLoadedCount().
Search database (server-side):
- Button next to the search input, also triggered by pressing Enter in the search box.
- Fires
searchTransactionsGUI AJAX action — backend runs a case-insensitiveLIKE %term%across counterparty name (direct + P2P end-recipient + P2P initial-sender), transaction description, all four address fields (sender/receiver/end-recipient/initial-sender), and the txid. Pasting any full or partial hash surfaces the matching row, whether it’s currently loaded in the paginator window or not. - Respects the current direction / type / status filter select values.
- Hard-capped at 500 rows; response carries
{html, rows[], total, capped, cap}so the banner can disclose when a broad query was clipped. - Result rows replace (not append to) the table body. Paginator moves to page 0, load-older is suspended (the result set is bounded by the server cap). Banner shows
N matches for "term" · Clear searchwhich reloads to the default view. - The local search input’s live-keystroke filter also searches the P2P endpoint name/address via new
data-tx-endpoint-name/data-tx-endpoint-addressrow attributes and the txid (viadata-txid, which was already present on the row for the detail-modal cross-link) — so typing “carol” or pasting a txid locally matches the same rows the database search would return for already-loaded transactions, and the “X transactions found” counter stays consistent after a database search.
Transaction Modal:
- Full transaction details
- Copy buttons for addresses/txid
Auto-Refresh:
- Polls every 15 seconds when transactions pending
- Controlled by
autoRefreshEnabledsetting
dlqSection.html
The Dead Letter Queue section displays messages that could not be delivered after all automatic retry attempts.
Pending Messages callout: When the queue has any pending or retrying items, the table renders inside a .pending-callout .pending-callout-has-body yellow card with a Pending Messages [count] (X retryable, Y abandon-only) header. Shared chrome with paymentRequestsSection.html’s “Pending Requests” callout — same .pending-callout-* classes, same yellow-gradient card with white table rows. When no pending items exist, the table renders normally with no callout. (Previously the pending count was a free-floating badge in the section header; that badge is gone, though the .dlq-pending-badge CSS class survives because the dashboard quick-actions tile still uses it.)
Status Filter: Dropdown matching contacts/transactions pattern — Any status (default), Pending & Retrying, Pending Only, Resolved, Abandoned. The search bar and filter are both gated on !empty($dlqItems) — an empty queue hides them so users aren’t shown controls with nothing to act on (matches the Contacts and Payment Requests sections).
Stats Bar: Per-status counts (Pending / Retrying / Resolved / Abandoned).
Table Columns: Status icon (leading, slim), Type, Recipient, Failure Reason (truncated), Added, Actions. Uses contacts-table chrome with 60vh scrollable wrapper. Clicking any row opens a detail modal with all fields + Retry/Abandon buttons. The leading status-icon column carries pending / retrying / resolved / abandoned state via fa-hourglass-half / fa-sync-alt fa-spin / fa-check-double / fa-ban — resolved and abandoned colours follow the user-selected status colour scheme. The legacy dedicated “Status” column was removed: the Actions cell carries “Delivered” / “Abandoned” text for terminal rows and the icon + row-click modal cover the rest.
Mobile (≤600px): Shows status icon + Type + Recipient (the generic .contacts-table rule hiding cols 3+ is overridden here so the recipient stays visible alongside the icon column). Tap row for detail modal with full info and actions. Action buttons collapse to icon-only at ≤900px.
Actions per row:
| Action | Available For | Description |
|---|---|---|
| Retry | transaction, contact types only, pending/retrying status |
Re-sends the original signed payload directly to the recipient |
| Abandon | Any pending/retrying item | Marks as abandoned — no further retries |
| Retry All | Bulk action — all retryable items | Re-sends all eligible items (transaction + contact types only) |
| Abandon All | Bulk action — all pending/retrying items | Marks all actionable items as abandoned |
Important — retry eligibility by message type:
Type Retryable Reason transaction✅ User-initiated payment; payload remains valid contact✅ User-initiated contact request; payload remains valid p2p❌ P2P routing request; expires in ≤300s — stale by retry time rp2p❌ Relay message forwarded on behalf of another node; underlying P2P has expired or resolved elsewhere
p2pandrp2pitems show an “Expired” label instead of a Retry button. Use Abandon to clear them.
Tab Badge: When pending DLQ items exist, the Activity tab in the navigation bar displays a count badge.
DLQ Indicator in Transaction History:
Transactions that have a pending or retrying DLQ entry display a DLQ icon next to the status icon in the Recent Transactions table and a DLQ badge in the In-Progress Transactions list. Clicking the icon/badge navigates to #dlq to retry or abandon the delivery. When a transaction’s delivery is exhausted and it moves to the DLQ, its status is immediately set to cancelled so it is removed from the In-Progress panel and stops triggering auto-refresh. Retrying from the DLQ resets the status to sending and re-delivers the original signed payload.
Mobile Layout (≤600px): Collapses to three columns: Status icon | Counterparty | Amount. The +/− on Amount carries direction; the status icon confirms completion state.
Notifications: A warning toast appears when new items are added to the DLQ (tracked per session — each item fires once per browser session).
paybackMethodsSection.html
Dashboard section rendering the user’s own payback methods — the settlement rails (bank wire, PayPal, BTC, custom free-text, etc.) they offer contacts for squaring debts. Uses the same form-container fade-in-up chrome as Payment Requests.
Header:
+ Addbutton — opens the two-step Add/Edit modal (paybackMethodForm.html)🔓 Unlocked for N min— full-width muted status row rendered above the section body when a sensitive-access grant is active (hidden when locked). Server-side initial state is rendered straight fromSession::sensitiveAccessSecondsRemaining(), and a top-levelupdateSensitiveAccessBadges()helper inscript.jskeeps every visible row across the page in sync from a single source — unlocking from any verify modal refreshes all badges, not just the section that triggered the prompt
Header callout — “How payback methods work”: per-row encryption at rest (AES-256-GCM, keyed to the wallet), how each rail’s masking differs (typed rails show last-4; custom shows the first 80 chars as a preview since it’s user-authored free text), share policy behaviour.
Filter bar: client-side Type and Currency dropdowns reusing the shared .contacts-filter-select chrome from Recent Transactions. Only surfaces when >1 option exists on an axis — a single-method / single-currency wallet sees no dead widgets.
Table columns: Type (plain uppercase, no badge), Currency, Label, Details (the masked_display), Share policy. Ordered by priority ASC, created_at DESC — same sort the repository uses. Row click opens the view modal.
Add / View / Edit modal (paybackMethodForm.html) is a two-step flow:
- Step 1 — Type picker. Tiles injected from the catalog JSON at
#payback-methods-catalog(the PHP side echoes the fullPaybackMethodTypeValidator::getCatalog()result, including plugin-registered types). Tiles group bybank/crypto/p2p/other— plugin-declared groups are slotted in automatically between the pre-declared groups, withotheralways last. - Step 2 — Per-type fields. Form body is built from the catalog entry’s
fieldsspec: label input, currency select (filtered totype.currencieswhen the type declares a whitelist), type-specific inputs (IBAN, routing number, email, address…), share-policy select, priority number input. SWIFT sub-rail gets a segmented[IBAN] [Account number]toggle so only the chosen identifier submits.
View mode renders the same DOM with inputs set to readonly and a “View only. Click Edit below to make changes” banner; footer swaps to Close · Edit. Edit mode flips inputs to editable without refetch (title changes, banner hides, footer swaps to Cancel · Delete · Save Method).
Sensitive-access gate: opening the modal for edit/view — and every mutation (add, update, remove, share-policy, reveal) — returns 401 sensitive_access_required unless a short-lived grant is active. The client routes through the same withSensitiveAccess(requestFn, onResponse, label) helper that apiKeysSection.html uses, so a denied request opens apiKeysVerifyModal on top of the form and retries on successful unlock. Unlock persists for a few minutes; the section’s “Unlocked for N min” badge appears as a full-width muted row above the body content while the grant is active.
Per-rail “About info HTML string. Starts collapsed so returning users who don’t need the refresher see the compact form. Plugins opt-in by returning an info key from getCatalogEntry() — a BTC plugin can call out accepted address formats, a PayPal plugin can remind operators to link an active account, etc.
Client-side module (script.js, IIFE exported as window.paybackMethods) talks to PaybackMethodsController via paybackMethodsList / paybackMethodsCreate / paybackMethodsUpdate / paybackMethodsRemove / paybackMethodsReveal / paybackMethodsSetSharePolicy AJAX actions. Session-expired responses (302 / 401 / HTML login page) are caught in the JSON-parse branch and surfaced as a readable “Session expired — please sign in again” message instead of a cryptic bad_json toast.
Related surfaces:
- Contact modal Payback tab (
contactSection.html) — live E2E fetch of the other contact’s shareable methods; nothing persists. See the Contact Modal table above. PaybackMethodsController— see Controllers section below.
settingsSection.html
Header callout — .section-intro explaining what Save does, what Reset reverts (unsaved changes only), and pointing at Advanced Settings → Reset to Defaults for a full wipe.
Settings Form:
- Basic wallet settings organized into four
<h5 class="settings-group-heading">subsections — Appearance (contact avatar style, amount and status color schemes), Identity (display name), Payments (default currency, fees, credit limit), Network (default transport mode) - Unified Settings category dropdown. Categories: Appearance, Identity, Payments, Network, Notifications, Feature Toggles, Currency, Display, Backup & Logging, Data Retention, Sync, Network (advanced), Rate Limiting, GUI Security, Reset to Defaults
- Advanced categories with >3 fields are further subdivided into
<h5 class="settings-group-heading">groups: Feature Toggles (Contacts, Transactions, GUI, System), Backup & Logging (Backup, Logging), Data Retention (Cleanup, Archive), Rate Limiting (Throughput, Attempt Blocking), Network (Transport Timeouts, Tor Resilience, Routing & Delivery, API). Each subsection is its own.settings-gridso the 3-columnauto-filllayout applies per group - GUI subsection hosts
autoRefreshEnabledandhideEmptyGuiSections.hideEmptyGuiSections(default OFF) hides the Failed Messages / Payment Requests / Pending Contact Requests sections when their lists are empty — when OFF (the default) these sections render an empty-state panel so users know the feature exists - GUI Security category hosts Session Timeout (moved here from the main grid), Remember Me Duration, Max Remembered Devices, and the Active Remembered Sessions list. The sessions list heading uses
settings-group-headingfor cross-category consistency, and the empty state uses the shared.empty-panel(dashed border + centered text) that also backs the API Keys empty state - Backup & Logging includes an encrypted-backup table with filename, scheduled/archive-triggered origin, size, creation time, and Create / Download / Verify actions. List is CSRF-gated; the other actions require the short-lived auth-code unlock. Downloads stream the encrypted artifact as an attachment; database restore is not exposed in the GUI.
- Data Retention category has two subsections with distinct semantics: a Cleanup block (
cleanupDeliveryRetentionDays,cleanupDlqRetentionDays,cleanupHeldTxRetentionDays,cleanupRp2pRetentionDays,cleanupMetricsRetentionDays) where rows past retention are deleted, and a separate Archive block (paymentRequestsArchiveRetentionDays,paymentRequestsArchiveBatchSize,transactionsArchiveRetentionDays,transactionsArchiveBatchSize) where resolved payment requests and eligible gap-free transactions move into their archive tables and stay queryable. The archive block carries its own inline warning (“nothing is deleted”) so users don’t confuse it with the cleanup retentions above it - Reset to Defaults category is a dedicated destructive-action surface — danger button opens
settingsResetToDefaultsModalwhich requires typingresetinto a confirmation input before the submit button enables. Submits toSettingsController::handleResetToDefaults()via a separate form (outside the main settings<form>, since a nested form isn’t legal HTML) - Save / Reset buttons at the bottom — Save posts
updateSettingsvia XHR (the form’s native submit is intercepted bysubmitSettingsForm()inscript.js); Reset is a plain<button type="reset">that rolls back unsaved form state 🔓 Unlocked for N min— full-width muted status row above the form, in sync with the same row in API Keys and Payback Methods (every section that gates on a sensitive-access grant). Hidden when no grant is active
updateSettings is AJAX, not a full-page POST. The action registers at TIER_SENSITIVE so the registry’s pre-dispatch handles both the CSRF check and the sensitive-access gate as a JSON 403 sensitive_access_required response. submitSettingsForm() catches that envelope, opens apiKeysVerifyModal for re-auth, and re-submits the same form once the user verifies — the user never loses their unsaved field edits to a session-grant lapse. Validation errors return 400 with {success: false, errors: […]} and surface as a toast; save success returns {success: true} and the page reloads to re-render any computed labels and rotate the page-load CSRF token. Pre-AJAX, the form was a full-page POST that on a sensitive-access lapse just redirected with a “Please re-enter your auth code” toast and no way for the user to actually re-enter it without abandoning the page.
The Settings tab additionally hosts altCodeSection.html (alternate auth code management, see below), apiKeysSection.html (API-key lifecycle, see below), and debugSection.html (debug logs, system info, debug report).
altCodeSection.html
Rendered on the Settings tab, between the main settings form and the API Keys section. Exposes status / set / rotate / clear for the user-chosen alternate auth code. All form submissions go through the controller surface documented under AltCodeController; this entry covers the user-facing components.
Status row (always shown):
An alternate auth code is currently configured.(green check) — whenUserContext::hasAltCode()is trueNo alternate auth code is set.(muted dot) — otherwise
Action row:
- Set alt code (key icon, success-coloured) — opens
altCodeSetModal(rendered as Rotate alt code when one already exists) - Remove alt code (trash icon, danger-coloured) — opens
altCodeClearModal; only shown when an alt code is currently configured
Self-rotation lockout (when session was authenticated via the alt code itself):
- A full-width
.alert-warningcallout renders above the action row explaining the lockout in three parts: a bold “Locked this session” headline, the reason (preventing alt-code-holders from rotating to lock out the legitimate operator), and the recovery path (“log out, log in with the primary auth code”) - The action buttons render with
aria-disabled="true"(not the HTMLdisabledattribute) and alockicon replacing the usual key/trash. Visual cues:opacity: 0.55,cursor: not-allowed, nativetitletooltip - Clicking a locked button does not silently fail — it routes through an
explainAltCodeLockeddata-action that scrolls the warning callout into view (scrollIntoView({ behavior: 'smooth', block: 'center' }), with a hard-jump fallback for older Tor Browser builds) and pulses a yellow box-shadow on it for 0.9 s. Repeated clicks always re-trigger - The server-side gate in
AltCodeControllerenforces this independently — the UI lockout is UX-only; even a hand-crafted POST gets refused withalt_session_forbidden
Modals:
altCodeSetModal— three fields: Primary auth code, New alt code, Confirm new alt code. Inline strength-meter beneath the new-code input mirrors the server-side rules inAltCodeValidator(length floor, character classes, triple-repeat / sequence detection). The meter is advisory only — the server is authoritative; this is so the user knows what’s missing before submittingaltCodeClearModal— single field: Primary auth code. Both modals are minimal — no live status polling, no per-field reveal toggles, no remember-me on the primary input (this is a high-stakes one-shot, not a routine login)
Client-side glue:
Self-contained inline <script nonce> block at the bottom of the section file rather than an addition to script.js — keeps the initial cut localised; if the feature grows it’ll be promoted out. Uses only vanilla DOM APIs (no async/await, no fetch shorthand) for Tor Browser “Safer” / “Safest” compatibility. CSRF token is read from the page-level hidden input on every submit; status code 200 with {success: true} reloads the page, anything else surfaces r.data.error (and r.data.errors[] for the validation-failure shape) into the modal’s inline error slot.
.section-intro — shared top-of-section callout
Any section can start with a <details class="section-intro text-muted"> callout:
<details class="section-intro text-muted">
<summary><i class="fas fa-info-circle"></i> <span>Short title</span></summary>
<div class="section-intro-body">Longer explanation…</div>
</details>
Currently applied to: Wallet Settings, API Keys, Failed Messages, Debug Information, New eIOU, Your Contacts, Recent Transactions, Payback Methods, Plugins, and every tab inside the contact modal (Info, Transactions, Payback, Status, Settings) — per-tab “About
Always ships closed (no open attribute) — user clicks the summary to expand via native <details> behaviour. Same UX on desktop and mobile and for JS-disabled users (Tor “Safest” mode). The summary shows an info icon on the left and a chevron on the right that rotates when open. CSS lives at .section-intro / details.section-intro > summary / .section-intro-body in page.css.
apiKeysSection.html
Rendered on the Settings tab, between the settings form and the debug section. Lists every API key on the node and exposes the full lifecycle (create / enable / disable / edit / delete / bulk-disable / bulk-delete) without leaving the page.
Toolbar:
- Create API Key — opens the create modal
- Refresh — re-fetches the list without a full page reload
- Disable all — visible only when ≥1 key is enabled
- Delete all — visible only when ≥1 key exists (enabled or disabled)
🔓 Unlocked for N min— full-width muted status row above the body content while a sensitive-access grant is active (hidden when locked). Same row treatment as Payback Methods and Wallet Settings; kept in sync across all three sections by the page-wideupdateSensitiveAccessBadges()helper
List rows (one per key):
- Label (bold) + Active/Disabled badge
key_id(monospace)- Meta line: permission summary (comma-joined), rate limit, created timestamp, last-used timestamp, expiration (if any)
- Actions: Enable/Disable (toggles single key), Edit (label / rate limit / expiry), Delete (typed-confirmation)
Modals (all position: fixed, stacked on .modal z-index 10000 except re-auth at 10010):
apiKeysCreateModal— label, permission checkboxes grouped by scope (Wallet / Contacts / System / Backup / Admin), Read-only / Full access / Clear presets, rate limit (1–ApiKeyService::MAX_RATE_LIMIT, default 100), expiry (Never / 30d / 90d / 1 year)apiKeysRevealModal— one-time secret display with per-field Copy buttons and a mandatory “I’ve saved…” acknowledgement before Done. Secret is scrubbed from the DOM the moment this modal closesapiKeysEditModal— read-only permission chips + editable label / rate-limit / expiry-shortening. Live warning when rate limit is raised above the stored valueapiKeysDeleteModal— typed confirmation (must match the key’s label)apiKeysDisableAllModal— one-click confirmation with the exact active-key countapiKeysDeleteAllModal— typed confirmation (must typedelete allverbatim)apiKeysVerifyModal— sensitive-action re-auth prompt (auth code input + Unlock). Opens on top of whichever modal triggered the gate
Client-side module (script.js, IIFE exported as window.apiKeys) handles dispatch through a shared withSensitiveAccess(requestFn, onResponse, label) helper: on a 401 sensitive_access_required response the module opens the verify modal, and on successful verify retries the original request with the same onResponse handler so the caller doesn’t have to know about the re-auth dance.
debugSection.html
Rendered at the bottom of the Settings tab (below the settings form).
Debug Logs (Tabbed):
| Tab | Contents |
|---|---|
| App Logs | Debug entries from database |
| eIOU Log | /var/log/eiou/app.log |
| PHP Logs | PHP error log |
| nginx Logs | nginx error log |
| Processes | PID-to-processor lookup table — friendly name + current PID + status (Running / Stale lockfile / Stopped) for the four long-running processors (Transaction, P2P, Cleanup, Contact Status). Each row is a <details> so the role description collapses on mobile; a top-level “About running processes” <details> collapses the cross-reference help. Status comes from posix_kill($pid, 0) against the PID in each processor’s /tmp/*.pid lockfile. Short-lived P2pWorker forks are intentionally not listed (they exit in seconds). |
| System Info | PHP version, extensions, config files, constants |
Debug Report:
- Limited report: Same data as GUI display
- Full report: Complete log history
floatingButtons.html
| Button | Purpose |
|---|---|
| Back to top | Scroll to page top |
| Manual refresh | Reload wallet data |
analyticsConsentModal.html
One-time modal shown after first login to ask the user whether to enable anonymous analytics. The choice is saved via the analyticsConsent AJAX action and the modal never reappears (analyticsConsentAsked flag in config).
| Element | Purpose |
|---|---|
| Consent modal | Opt-in/opt-out choice for anonymous analytics |
| Enable button | Sets analyticsEnabled=true |
| Decline button | Sets analyticsEnabled=false |
firstLoginSeedModal.html
Shown once, right after a wallet is first generated, so the operator can back up
their 24-word seed phrase. The CLI hands the plaintext mnemonic to the GUI through
a randomly-named, owner-read-only file on tmpfs (/dev/shm, never written to disk,
never decrypted from the stored config); the GUI reads it only behind an
authenticated session and renders the words in this modal. The modal keeps
appearing until the operator confirms via the firstLoginSeedAck action (a
CSRF-gated POST that works with JavaScript disabled), at which point the handoff
file is deleted and the phrase can never be displayed here again. A 24-hour backstop
deletes the handoff for operators who only ever use the CLI. Restores skip the
handoff (the operator already holds the seed). The page that embeds the words is
no-store like the rest of the authenticated GUI.
| Element | Purpose |
|---|---|
| Word list | The 24 mnemonic words, numbered |
| Confirm checkbox | Native required checkbox; gates the Done button without JS |
| Done button | Posts firstLoginSeedAck, which deletes the handoff and dismisses the modal |
Session Management
The Session class provides secure session handling.
Session Configuration
| Parameter | Value | Description |
|---|---|---|
lifetime |
0 | Session cookie (expires on browser close) |
httponly |
true | Prevent JavaScript access |
samesite |
Strict | CSRF protection via cookie |
secure |
auto | HTTPS-only when available |
name |
EIOU_WALLET_SESSION_<nodeHash> |
Custom session name. Suffixed with substr(sha256(public_key_pem), 0, 16) so two nodes sharing a hostname (only differ by port — typical localhost dev with :443 and :8443) write distinct cookies. Cookies ignore port per RFC 6265, so without the suffix the second login overwrites the first’s session and both tabs end up logged out. Falls back to EIOU_WALLET_SESSION_default while userconfig.json is missing (pre-init window). |
Key Methods
| Method | Description |
|---|---|
isAuthenticated() |
Check if user is logged in |
authenticate($authCode, $userAuthCode) |
Validate auth code |
checkSessionTimeout() |
Enforce configurable inactivity limit (default 30 min, reads from sessionTimeoutMinutes in config) |
logout() |
Clear session and destroy cookie |
requireAuth() |
Redirect to login if not authenticated |
generateCSRFToken() |
Create secure token |
validateCSRFToken($token) |
Verify token with constant-time comparison |
verifyCSRFToken() |
Auto-verify POST requests |
getCSRFField() |
Generate hidden input HTML |
setMessage($message, $type) |
Set flash message |
getMessage() |
Get and clear flash message |
Session Security Features
| Feature | Implementation |
|---|---|
| Session regeneration | Every 5 minutes |
| Auth code comparison | hash_equals() constant-time |
| Session timeout | Configurable (5/10/15/30/60 min, default 30) |
| ID regeneration on login | Prevents session fixation |
| CSRF token expiration | 1 hour max age |
Security Features
CSRF Protection
All POST forms include a CSRF token:
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
Token validation occurs at the start of every controller action via $this->session->verifyCSRFToken().
XSS Prevention
| Location | Method |
|---|---|
| View output | htmlspecialchars($value, ENT_QUOTES, 'UTF-8') |
| URL parameters | URL encoding + HTML escaping |
| JSON in HTML | json_encode() + htmlspecialchars() |
| User input | Security::sanitizeInput() |
Input Validation
All user input is validated through InputValidator:
| Validator | Purpose |
|---|---|
validateAddress($address) |
HTTP/HTTPS/Tor address format |
validateContactName($name) |
Alphanumeric with spaces |
validateFeePercent($fee) |
0-100 range |
validateCreditLimit($credit) |
Positive number |
validateCurrency($currency) |
Supported currency code |
validateAmount($amount, $currency) |
Positive transaction amount |
validateNotSelfSend($address, $userContext) |
Prevent sending to self |
Session Security
| Protection | Description |
|---|---|
| Constant-time comparison | Auth code and CSRF validation |
| Session regeneration | Periodic and on authentication |
| HTTP-only cookies | Prevent XSS token theft |
| SameSite Strict | Prevent CSRF via cookies |
| Inactivity timeout | 30-minute auto-logout |
Content Security Policy & Response Headers
Security::setGuiHeaders() runs at the top of the GUI entry point and sets, per
request, a strict nonce-based Content-Security-Policy (both script-src and
style-src are 'self' 'nonce-...', no 'unsafe-inline'), a Permissions-Policy
(camera=(self) for the QR scanner, geolocation and microphone denied), and
Cache-Control: no-store on the whole response. nginx supplies the static set
(X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS). Full
rationale in SECURITY.md → GUI Response Headers.
Because the policy forbids 'unsafe-inline', GUI markup (templates and
JS-built HTML) must follow these rules, or the browser blocks it:
| Don’t | Do instead |
|---|---|
Inline style="..." attributes |
A CSS class in page.css (reuse utilities like text-muted, d-none, u-fs1, w-100, or add one) |
Inline onclick= / on*= handlers |
The delegated data-action / data-confirm dispatch, or addEventListener in script.js |
Non-nonced inline <script> / <style> |
Add nonce="<?php echo cspNonce(); ?>" to the tag |
Toggling visibility by writing the initial state into an inline style and resetting it with el.style.display = '' |
An initial d-none class plus el.classList.toggle('d-none', cond) |
Styling set at runtime through the CSSOM (element.style.x = y) is not
governed by style-src, so JS show/hide and dynamic positioning keep working.
A plugin’s Plugins-tab panel renders into this same page and is subject to the
same rules (see /docs/reference/plugins → The Plugins tab).
Helpers
MessageHelper
Utility class for message handling between controllers and views.
Key Methods
| Method | Description |
|---|---|
parseContactOutput($output) |
Parse CLI output, determine message type |
parseCliJsonOutput($output) |
Parse JSON CLI response |
formatMessage($message, $type) |
Generate HTML message div |
getMessageClass($type) |
Get CSS class for message type |
getMessageIcon($type) |
Get icon character for message type |
redirectMessage($message, $type, $url) |
Redirect with URL-encoded message |
getMessageFromUrl() |
Extract message from GET params |
displayFlashMessage($session) |
Render flash or URL message |
getGuiFriendlyMessage($errorCode, $detail) |
Map error codes to user messages |
Message Types
| Type | CSS Class | Icon |
|---|---|---|
| success | message-success | checkmark |
| error | message-error | X |
| warning | message-warning | triangle |
| info | message-info | i |
| contact-accepted | message-success | checkmark |
ViewHelper
Utility class for view rendering.
| Method | Description |
|---|---|
sanitize($text) |
HTML escape with UTF-8 |
formatTimestamp($timestamp, $format) |
Format date/time |
getTransactionClass($type) |
CSS class for transaction type |
getStatusBadgeClass($status) |
CSS class for contact status |
generatePagination($page, $total, $url) |
Render pagination links |
renderSelectOptions($options, $selected) |
Generate option tags |
generateBreadcrumbs($items) |
Render breadcrumb nav |
ContactDataBuilder
Builds standardized contact data structures for the GUI.
| Method | Description |
|---|---|
buildContactData($contact, $status) |
Create normalized contact array |
buildEncodedContactData($contact, $status) |
JSON-encoded, HTML-safe for onclick |
Contact Data Fields: name, address, fee, credit_limit, currency, status, pubkey, pubkey_hash, balance, balances_by_currency, contact_id, transactions, online_status, valid_chain, my_available_credit, their_available_credit, chain_drop_proposal, chain_gap_details, currencies, pending_currencies, outgoing_currencies, plus all dynamic address types.
Address Priority: Tor > HTTPS > HTTP (security preference)
Known Limitations
| Limitation | Description | Workaround |
|---|---|---|
| No WebSockets | Tor Browser blocks WebSockets | Polling-based updates |
| Limited JavaScript | Tor Browser security settings | Server-side rendering |
| Session-based auth | No persistent login | Re-authenticate on browser close |
| No real-time updates | Page refresh required | Auto-refresh when enabled |
| Large contact lists | Performance with >100 contacts | Pagination/virtualization planned |
| Debug log size | Full report slow over Tor | Limited report option available |
Development Setup
Volume Mount
During development, mount the source directory:
volumes:
- ./files/src:/app/eiou/src:ro
File Paths
| Context | Path Prefix |
|---|---|
| Inside container | /app/eiou/src/gui/ |
| Outside container | ./files/src/gui/ |
| Browser assets | /gui/assets/ |
Testing Changes
- Edit files in
./files/src/gui/ - Refresh browser (container uses mounted files)
- Check browser console for JS errors
- Check PHP logs for server errors
CSS Development
Styles are in assets/css/page.css, included inline via PHP require. Changes require browser refresh.
JavaScript Development
Scripts are in assets/js/script.js, included inline. Use browser dev tools for debugging.
See Also
- API Reference - REST API documentation
- CLI Reference - Command-line interface documentation
- Error Codes - Complete error code reference