Files
Mark-VIII/docs/superpowers/plans/2026-08-11-pineap-overview-markvii.md
T
2026-08-11 20:24:24 -07:00

13 KiB

PineAP Overview — Mark VII Stats + Mode Toggle Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Bring the Pager WebUI PineAP overview (#/pineap) to Mark VII parity — a clickable 3-card stats row (Total SSIDs in Pool / Clients Connected / Handshakes Captured) and a Passive/Active/Advanced quick mode toggle — using only functionality the Pager supports.

Architecture: Frontend-only change to views.pineap in www/js/views.js plus a small .seg segmented-control style in www/css/app.css. All data comes from existing, on-device-verified webui endpoints; the backend and test modules are untouched. Karma (mimic) state is unreadable from the daemon, so it is tracked in a view-local variable.

Tech Stack: Vanilla JS (hyperscript h() helper), existing PagerAPI client, existing .cards/.card/.badge/.toggle CSS, plain CSS additions.

Global Constraints

  • No backend changes. No changes to server.py or tests/.
  • Karma state is NOT readable from the daemon (only mimic/enable|disable); track it client-side as karmaOn (null = unknown, defaults to null on page load).
  • Mode presets only apply Pager-supported features: POST /api/pineap/enable and POST /api/pineap/mimic. Do NOT touch ssidpool/* (broadcast cannot start natively — it stays a manual Quick Settings toggle).
  • JS verification uses the Python delimiter-balance checker at C:\Users\root\AppData\Local\Temp\opencode\js_balance.py (no node available).
  • Python for the unittest loop: $env:LOCALAPPDATA\Programs\Python\Python311\python.exe.
  • Deploy: powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 -Password "<PAGER_PASSWORD>", then /etc/init.d/pagerwebui restart over sshpass SSH.
  • Commit messages follow repo style (feat:, fix:, docs:, test:).
  • Mode badge highlight refinement vs. the approved spec (intent-preserving): when karmaOn is tracked, prefer it over the unreadable daemon state so the toggle does not visually jump after a user applies a preset.

Task 1: Rebuild the PineAP overview (stats row + mode toggle + karma tracking)

Files:

  • Modify: payload/user/general/pager-webui/www/css/app.css (append .seg styles)
  • Modify: payload/user/general/pager-webui/www/js/views.js (replace the body of views.pineap, lines ~187-255)

Interfaces:

  • Consumes: pineapShell(root, hash) (appends h1 + tab bar, returns content box), pineapSetCard(card, label, value) (sets .card-label/.card-value text), h(), btn(label, onclick, variant), PagerAPI.get/post, App.go(hash), App.toast.

  • Produces: views.pineap with (1) a .seg segmented control with three buttons, (2) a 3-card stats row keyed stats.ssids/stats.clients/stats.handshakes, (3) a view-local karmaOn variable consumed by load() for the Karma card and the computed mode.

  • Step 1: Append the segmented-control CSS to app.css

Append to the end of payload/user/general/pager-webui/www/css/app.css:

.seg { display: inline-flex; margin-top: 8px; border: 1px solid var(--ink, #999); border-radius: 4px; overflow: hidden; }
.seg-btn { background: transparent; border: none; padding: 5px 14px; font-size: 12px; cursor: pointer; color: var(--muted, #666); }
.seg-btn + .seg-btn { border-left: 1px solid var(--ink, #999); }
.seg-btn.active { background: var(--primary, #1976d2); color: #fff; }
.seg-btn.busy { opacity: .5; pointer-events: none; }
  • Step 2: Replace the body of views.pineap in views.js

Replace everything from views.pineap = (root) => { through the closing }; of that function (current lines ~187-255) with:

views.pineap = (root) => {
  const box = pineapShell(root, '#/pineap');
  const mode = h('span', { class: 'badge', text: '-' });
  const intro = h('p', { class: 'muted' });
  const head = h('div', { class: 'section' },
    h('h2', {}, 'PineAP'),
    h('div', { class: 'row' }, h('div', {}, mode)),
    intro);
  box.appendChild(head);

  const segBtns = {};
  const modeBar = h('div', { class: 'seg' });
  ['passive', 'active', 'advanced'].forEach((m) => {
    const b = h('button', { class: 'seg-btn', text: m[0].toUpperCase() + m.slice(1) });
    b.addEventListener('click', () => applyMode(m));
    modeBar.appendChild(b);
    segBtns[m] = b;
  });
  const modeInfo = h('div', { class: 'muted', style: 'margin-top:8px;font-size:12px' });
  head.appendChild(modeBar);
  head.appendChild(modeInfo);

  const quick = {
    collect: h('input', { type: 'checkbox', id: 'po-collect' }),
    advertise: h('input', { type: 'checkbox', id: 'po-advertise' })
  };
  const quickBox = h('div', { class: 'section' }, h('h2', {}, 'Quick Settings'));
  quickBox.appendChild(h('label', { class: 'toggle' }, quick.collect, ' Capture SSIDs to Pool'));
  quickBox.appendChild(h('label', { class: 'toggle' }, quick.advertise, ' Advertise AP Impersonation Pool'));
  quickBox.appendChild(h('div', { class: 'muted', style: 'margin-top:8px' },
    'Client connect/disconnect notifications are handled by the Pager alert payload system.'));
  box.appendChild(quickBox);

  const stats = {};
  const statWrap = h('div', { class: 'cards' });
  const statDefs = [
    ['ssids', 'Total SSIDs in Pool', '#/pineap/impersonation'],
    ['clients', 'Clients Connected', '#/pineap/clients'],
    ['handshakes', 'Handshakes Captured', '#/pineap/evilwpa']
  ];
  statDefs.forEach(([k, label, hash]) => {
    const card = h('div', { class: 'card' },
      h('div', { class: 'card-label' }),
      h('div', { class: 'card-value' }),
      h('div', { class: 'row' }, btn('View', () => App.go(hash), 'ghost')));
    statWrap.appendChild(card);
    stats[k] = { label: card.querySelector('.card-label'), value: card.querySelector('.card-value') };
    stats[k].label.textContent = label;
  });
  box.appendChild(statWrap);

  const cards = { karma: {}, open: {}, wpa: {}, ent: {} };
  const cardWrap = h('div', { class: 'cards' });
  Object.keys(cards).forEach((k) => {
    const card = h('div', { class: 'card' },
      h('div', { class: 'card-label' }),
      h('div', { class: 'card-value' }),
      h('div', { class: 'row' }, btn('Configure', () => App.go({
        karma: '#/pineap/open', open: '#/pineap/open',
        wpa: '#/pineap/evilwpa', ent: '#/pineap/enterprise'
      }[k]), 'ghost')));
    cardWrap.appendChild(card);
    cards[k].label = card.querySelector('.card-label');
    cards[k].value = card.querySelector('.card-value');
  });
  box.appendChild(cardWrap);

  let karmaOn = null;

  function bind(cb, on) {
    cb.addEventListener('change', () => on(cb.checked).then(load).catch(() => { cb.checked = !cb.checked; App.toast('Failed', 'error'); }));
  }
  bind(quick.collect, (v) => PagerAPI.post('/api/pineap/ssidpool/collect', { enable: v }));
  bind(quick.advertise, (v) => PagerAPI.post('/api/pineap/ssidpool/advertise', { enable: v }));

  function setMode(m) {
    Object.keys(segBtns).forEach((k) => segBtns[k].classList.toggle('active', k === m));
    modeInfo.textContent = {
      passive: 'PineAP is on; network impersonation (Karma) is off.',
      active: 'PineAP and Karma are on; the open network is impersonated.',
      advanced: 'All PineAP features are enabled and customizable.'
    }[m] || '';
  }

  function applyMode(m) {
    const btn = segBtns[m];
    if (!btn || btn.classList.contains('active')) return;
    const on = m !== 'passive';
    btn.classList.add('busy');
    Promise.all([
      PagerAPI.post('/api/pineap/enable', { enable: true }),
      PagerAPI.post('/api/pineap/mimic', { enable: on })
    ]).then(() => {
      karmaOn = on;
      setMode(m);
      App.toast('Mode: ' + m[0].toUpperCase() + m.slice(1));
      load();
    }).catch(() => App.toast('Failed', 'error')).finally(() => btn.classList.remove('busy'));
  }

  function load() {
    Promise.all([
      PagerAPI.get('/api/pineap/get_config').catch(() => ({ data: {} })),
      PagerAPI.get('/api/pineap/hostapd').catch(() => ({ data: {} })),
      PagerAPI.post('/api/pineap/wifi/get_ap').catch(() => ({ data: {} })),
      PagerAPI.get('/api/pineap/ssids').catch(() => ({ data: {} })),
      PagerAPI.get('/api/pineap/clients').catch(() => ({ data: {} })),
      PagerAPI.get('/api/pineap/handshakes').catch(() => ({ data: {} }))
    ]).then(([cfg, host, ap, ss, cl, hs]) => {
      const c = cfg.data || {}, hh = host.data || {}, a = ap.data || {};
      const disabled = !!hh.pineap_disabled;
      const wpa = a.wpa || {}, ent = a.enterprise || {};
      const advanced = !disabled && (wpa.enabled || ent.enabled);
      const computed = disabled ? 'passive' : (karmaOn === false ? 'passive' : (advanced ? 'advanced' : 'active'));
      mode.textContent = computed[0].toUpperCase() + computed.slice(1);
      mode.className = 'badge ' + (disabled ? 'off' : 'on');
      intro.textContent = disabled
        ? 'PineAP is disabled. Enable it from the Open AP tab to begin impersonating networks.'
        : 'The WiFi Pineapple will respond to probe requests and impersonate the Open, Evil WPA, and Evil Enterprise access points.';
      setMode(computed);
      quick.collect.checked = !!c.autossidpool;
      const pool = a.pool || {};
      quick.advertise.checked = pool.disabled === false;
      stats.ssids.value.textContent = (ss.data && Array.isArray(ss.data.ssids)) ? ss.data.ssids.length : '—';
      stats.clients.value.textContent = (cl.data && typeof cl.data.count === 'number') ? cl.data.count : '—';
      stats.handshakes.value.textContent = (hs.data && Array.isArray(hs.data.files)) ? hs.data.files.length : '—';
      pineapSetCard(cards.karma, 'Karma', karmaOn == null ? null : (karmaOn ? 'On' : 'Off'));
      const open = a.open || {};
      pineapSetCard(cards.open, 'Open Network', open.enabled == null ? '—' : (open.enabled ? 'On' : 'Off'));
      pineapSetCard(cards.wpa, 'Evil WPA', wpa.enabled ? 'On' : 'Off');
      pineapSetCard(cards.ent, 'Evil Enterprise', ent.enabled ? 'On' : 'Off');
    });
  }
  load();
  const iv = setInterval(load, 5000);
  return { destroy: () => clearInterval(iv) };
};
  • Step 3: Run the JS delimiter balance check

Run: python "C:\Users\root\AppData\Local\Temp\opencode\js_balance.py" "C:\Users\root\Documents\Pineapple\pager-webui\payload\user\general\pager-webui\www\js\views.js" Expected: ...views.js: delimiter balance OK

  • Step 4: Run the 13-module unittest loop (backend must stay green)

Run from C:\Users\root\Documents\Pineapple\pager-webui:

$py = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe"; Get-ChildItem tests\test_*.py | ForEach-Object { $mod = "tests." + [IO.Path]::GetFileNameWithoutExtension($_.Name); $out = & $py -m unittest $mod 2>&1; ($out | Select-Object -Last 1) }

Expected: every module reports OK (recon may show skipped=1).

  • Step 5: Commit
git add payload/user/general/pager-webui/www/js/views.js payload/user/general/pager-webui/www/css/app.css
git commit -m "feat: PineAP overview stats cards + passive/active/advanced mode toggle"

Task 2: Deploy and verify on device

Files: none (verification only; no commit).

Interfaces:

  • Consumes: Task 1 output (deployed via scripts/deploy.ps1).

  • Step 1: Deploy the payload and restart the webui

Run from C:\Users\root\Documents\Pineapple\pager-webui:

powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 -Password "<PAGER_PASSWORD>"

Then restart and confirm the port is up:

sshpass -p "<PAGER_PASSWORD>" ssh root@172.16.52.1 "/etc/init.d/pagerwebui restart; sleep 4; curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/api_ping"

Expected: 401 (auth required = running).

  • Step 2: Verify the three stat endpoints return live data on device

Run (base64 the script then echo ... | base64 -d | sh over sshpass):

curl -s -c /tmp/pwj -X POST http://127.0.0.1:8080/api/login -H "Content-Type: application/json" -d '{"username":"root","password":"<PAGER_PASSWORD>"}' > /dev/null
echo ssids:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/ssids
echo clients:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/clients
echo handshakes:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/handshakes

Expected: ssids returns {"ssids":[...]}, clients returns {"clients":[],"count":0} (or a number), handshakes returns {"files":[...],"handshakes":[...]}.

  • Step 3: Confirm the deployed files contain the new code
grep -c "seg-btn\|Total SSIDs in Pool\|Handshakes Captured" /root/payloads/user/general/pager-webui/www/js/views.js
grep -c "\.seg" /root/payloads/user/general/pager-webui/www/css/app.css

Expected: counts greater than zero.

  • Step 4: Report for user UI walk

Tell the user the overview now has: the three stat cards (numbers populate in the 5s poll; each View button navigates to its tab), the Passive/Active/Advanced segmented toggle (applies PineAP master + Karma; karma tracked client-side and shown on the Karma card; badge/description update; failures toast + revert), and unchanged Quick Settings. Ask them to refresh http://172.16.52.1:8080/#/pineap and confirm.