# Web of Trust — Admin Web UI Plan

## Overview

Add a **Web of Trust** section to the existing Authorization page in the admin web interface. The section lets the admin see whether their kind 3 contact list is on the relay, select a WoT level, trigger a sync, and view the current whitelist count.

## Architecture

### Data Flow

```mermaid
flowchart LR
    A[Web UI] -->|config_set wot_enabled N| B[Kind 23456 Event]
    A -->|system_command wot_sync| B
    A -->|system_command wot_status| B
    B --> C[Relay Backend]
    C -->|Kind 23457 Response| D[Web UI updates display]
```

### How It Works

1. **On page load** — the Authorization page already calls `loadAuthRules`. We add a parallel call to `loadWotStatus` which sends a `system_command` / `wot_status` event.
2. **Backend responds** with a kind 23457 JSON containing: `wot_enabled` level, `wot_whitelist_count`, and `admin_kind3_exists` boolean.
3. **UI renders** a card showing:
   - Whether the admin's kind 3 event exists on the relay
   - Current WoT level as a 3-option radio/button group
   - Count of whitelisted pubkeys
   - A SYNC button to force resync
4. **Changing level** sends `config_set` / `wot_enabled` / `0|1|2` via the existing admin API, then triggers `wot_sync` if level > 0.
5. **SYNC button** sends `system_command` / `wot_sync`.

## Detailed Changes

### 1. Backend: New system commands in `src/config.c`

Add two new branches to `handle_system_command_unified`:

#### `wot_status` command

Returns JSON response:
```json
{
  "command": "wot_status",
  "status": "success",
  "wot_enabled": 2,
  "admin_kind3_exists": true,
  "wot_whitelist_count": 347,
  "timestamp": 1234567890
}
```

Implementation:
- Read `wot_enabled` from config table
- Query `SELECT COUNT(*) FROM events WHERE kind = 3 AND pubkey = ?` with admin_pubkey to check kind 3 existence
- Query `SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1` for whitelist count
- Return as kind 23457 signed response

#### `wot_sync` command

Calls `wot_sync_from_admin_kind3` and returns result:
```json
{
  "command": "wot_sync",
  "status": "success",
  "wot_whitelist_count": 347,
  "timestamp": 1234567890
}
```

### 2. Backend: Hook `config_set` for `wot_enabled`

In `handle_config_set_unified`, after the config value is updated, add a check:
```c
// After successful update, trigger WoT sync if wot_enabled changed
if (strcmp(config_key, "wot_enabled") == 0) {
    int new_level = atoi(config_value);
    if (new_level > 0) {
        wot_sync_from_admin_kind3();
    } else {
        // Level 0: clear wot_whitelist rules and reset auth flags
        sqlite3_exec(g_db, "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'", NULL, NULL, NULL);
        update_config_in_table("nip42_auth_required_subscriptions", "false");
    }
}
```

### 3. Frontend: HTML — WoT section in `api/index.html`

Add a new div **inside** the `authRulesSection`, before the auth rules table. This keeps WoT visually grouped with authorization:

```html
<!-- Web of Trust Section -->
<div id="wotSection" class="input-group">
    <div class="section-header" style="font-size: 14px; margin-bottom: 10px;">
        WEB OF TRUST
    </div>
    
    <!-- Kind 3 Status Indicator -->
    <div id="wotKind3Status" class="wot-status-row">
        <span>Admin Contact List (kind 3):</span>
        <span id="wotKind3Indicator" class="wot-indicator wot-unknown">Checking...</span>
    </div>
    
    <!-- WoT Level Selector -->
    <div class="wot-level-selector">
        <label>WoT Level:</label>
        <div class="inline-buttons">
            <button type="button" id="wotLevel0Btn" class="wot-level-btn" onclick="setWotLevel(0)">
                OFF
            </button>
            <button type="button" id="wotLevel1Btn" class="wot-level-btn" onclick="setWotLevel(1)">
                WRITE ONLY
            </button>
            <button type="button" id="wotLevel2Btn" class="wot-level-btn" onclick="setWotLevel(2)">
                FULL
            </button>
        </div>
        <div class="wot-level-description" id="wotLevelDescription">
            Level 0: Open relay — anyone can read and write
        </div>
    </div>
    
    <!-- WoT Stats -->
    <div class="wot-stats-row">
        <span>Whitelisted Pubkeys:</span>
        <span id="wotWhitelistCount">—</span>
    </div>
    
    <!-- Sync Button -->
    <div class="inline-buttons">
        <button type="button" id="wotSyncBtn" onclick="syncWot()">SYNC FROM KIND 3</button>
        <button type="button" id="wotRefreshBtn" onclick="loadWotStatus()">REFRESH STATUS</button>
    </div>
</div>

<hr style="margin: 15px 0; border-color: var(--border-color);">
```

### 4. Frontend: CSS additions in `api/index.css`

```css
/* Web of Trust styles */
.wot-status-row, .wot-stats-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    font-size: 14px;
}

.wot-indicator {
    padding: 2px 10px;
    border-radius: 4px;
    font-weight: bold;
    font-size: 12px;
}

.wot-indicator.wot-found { background: #28a745; color: white; }
.wot-indicator.wot-missing { background: #dc3545; color: white; }
.wot-indicator.wot-unknown { background: #6c757d; color: white; }

.wot-level-selector { padding: 10px 0; }
.wot-level-selector label { display: block; margin-bottom: 5px; font-weight: bold; }

.wot-level-btn { min-width: 100px; }
.wot-level-btn.active {
    background: var(--accent-color, #007bff);
    color: white;
    border-color: var(--accent-color, #007bff);
}

.wot-level-description {
    font-size: 12px;
    color: var(--text-secondary);
    margin-top: 5px;
    font-style: italic;
}
```

### 5. Frontend: JavaScript functions in `api/index.js`

#### `loadWotStatus` — query current WoT state

```javascript
async function loadWotStatus() {
    try {
        if (!isLoggedIn || !userPubkey || !relayPool) return;
        
        const command_array = ["system_command", "wot_status"];
        const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
        if (!encrypted_content) return;
        
        const event = {
            kind: 23456,
            pubkey: userPubkey,
            created_at: Math.floor(Date.now() / 1000),
            tags: [["p", getRelayPubkey()]],
            content: encrypted_content
        };
        
        const signedEvent = await window.nostr.signEvent(event);
        const url = relayConnectionUrl.value.trim();
        await relayPool.publish([url], signedEvent);
        
        log('WoT status query sent', 'INFO');
    } catch (error) {
        log('Failed to load WoT status: ' + error.message, 'ERROR');
    }
}
```

#### `setWotLevel` — change WoT level via config_set

```javascript
async function setWotLevel(level) {
    try {
        if (!isLoggedIn || !userPubkey || !relayPool) return;
        
        // Send config_set for wot_enabled
        const command_array = ["config_set", "wot_enabled", String(level)];
        const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
        if (!encrypted_content) return;
        
        const event = {
            kind: 23456,
            pubkey: userPubkey,
            created_at: Math.floor(Date.now() / 1000),
            tags: [["p", getRelayPubkey()]],
            content: encrypted_content
        };
        
        const signedEvent = await window.nostr.signEvent(event);
        const url = relayConnectionUrl.value.trim();
        await relayPool.publish([url], signedEvent);
        
        log('WoT level set to ' + level, 'INFO');
        
        // Refresh status after a short delay
        setTimeout(() => loadWotStatus(), 1500);
    } catch (error) {
        log('Failed to set WoT level: ' + error.message, 'ERROR');
    }
}
```

#### `syncWot` — force WoT sync

```javascript
async function syncWot() {
    try {
        if (!isLoggedIn || !userPubkey || !relayPool) return;
        
        const command_array = ["system_command", "wot_sync"];
        const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
        if (!encrypted_content) return;
        
        const event = {
            kind: 23456,
            pubkey: userPubkey,
            created_at: Math.floor(Date.now() / 1000),
            tags: [["p", getRelayPubkey()]],
            content: encrypted_content
        };
        
        const signedEvent = await window.nostr.signEvent(event);
        const url = relayConnectionUrl.value.trim();
        await relayPool.publish([url], signedEvent);
        
        log('WoT sync command sent', 'INFO');
        
        // Refresh status after sync completes
        setTimeout(() => loadWotStatus(), 2000);
    } catch (error) {
        log('Failed to sync WoT: ' + error.message, 'ERROR');
    }
}
```

#### `handleWotStatusResponse` — process backend response

```javascript
function handleWotStatusResponse(responseData) {
    const kind3Indicator = document.getElementById('wotKind3Indicator');
    const whitelistCount = document.getElementById('wotWhitelistCount');
    const levelDesc = document.getElementById('wotLevelDescription');
    
    // Update kind 3 indicator
    if (kind3Indicator) {
        if (responseData.admin_kind3_exists) {
            kind3Indicator.textContent = 'Found ✓';
            kind3Indicator.className = 'wot-indicator wot-found';
        } else {
            kind3Indicator.textContent = 'Not Found ✗';
            kind3Indicator.className = 'wot-indicator wot-missing';
        }
    }
    
    // Update whitelist count
    if (whitelistCount) {
        whitelistCount.textContent = responseData.wot_whitelist_count || 0;
    }
    
    // Update level buttons
    const level = responseData.wot_enabled || 0;
    ['wotLevel0Btn', 'wotLevel1Btn', 'wotLevel2Btn'].forEach((id, i) => {
        const btn = document.getElementById(id);
        if (btn) {
            btn.classList.toggle('active', i === level);
        }
    });
    
    // Update description
    const descriptions = [
        'Level 0: Open relay — anyone can read and write',
        'Level 1: Write-only — only followed pubkeys can publish events',
        'Level 2: Full — only followed pubkeys can publish AND subscribe (NIP-42 required)'
    ];
    if (levelDesc) {
        levelDesc.textContent = descriptions[level] || descriptions[0];
    }
    
    // Enable/disable sync button based on kind 3 existence
    const syncBtn = document.getElementById('wotSyncBtn');
    if (syncBtn) {
        syncBtn.disabled = !responseData.admin_kind3_exists;
    }
}
```

#### Wire into existing response handler

In the existing `handleSystemCommandResponse` function, add:
```javascript
if (responseData.command === 'wot_status' || responseData.command === 'wot_sync') {
    handleWotStatusResponse(responseData);
}
```

#### Wire into page navigation

In the `authorization` case of the page switch handler, add:
```javascript
case 'authorization':
    loadAuthRules().catch(...);
    loadWotStatus().catch(error => {
        console.log('Auto-load WoT status failed: ' + error.message);
    });
    break;
```

## Files to Modify

| File | Changes |
|------|---------|
| `src/config.c` | Add `wot_status` and `wot_sync` branches to `handle_system_command_unified`; add WoT sync hook to `handle_config_set_unified` |
| `api/index.html` | Add WoT section HTML inside `authRulesSection` |
| `api/index.css` | Add WoT-specific CSS styles |
| `api/index.js` | Add `loadWotStatus`, `setWotLevel`, `syncWot`, `handleWotStatusResponse` functions; wire into page nav and response handler |

## Edge Cases

1. **Admin not logged in** — WoT section shows but buttons are disabled; status shows "Checking..."
2. **No kind 3 event** — Indicator shows red "Not Found ✗"; SYNC button is disabled; level selector still works but sync will whitelist only admin + relay
3. **Level change from 2 to 0** — Backend clears all wot_whitelist rules and resets nip42_auth_required_subscriptions
4. **Concurrent config_set and wot_sync** — The backend handles these sequentially via SQLite transactions; no race condition

## UI Mockup

```
┌─────────────────────────────────────────────┐
│ WEB OF TRUST                                │
│                                             │
│ Admin Contact List (kind 3):  [Found ✓]     │
│                                             │
│ WoT Level:                                  │
│ [  OFF  ] [ WRITE ONLY ] [  FULL  ]         │
│ Level 2: Full — only followed pubkeys can   │
│ publish AND subscribe (NIP-42 required)     │
│                                             │
│ Whitelisted Pubkeys: 347                    │
│                                             │
│ [ SYNC FROM KIND 3 ] [ REFRESH STATUS ]     │
├─────────────────────────────────────────────┤
│ ─────────────────────────────────────────── │
│ AUTH RULES MANAGEMENT                       │
│ ... existing auth rules table ...           │
└─────────────────────────────────────────────┘
```
