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

11 KiB

Open AP Tab — Mark VII "PineAP Settings" Card 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: Restyle the PineAP Open AP tab (#/pineap/open) into the Mark VII "PineAP" settings card — titled card with subtitle, iOS-style slide switches grouped into sections, and a batched Mk7-style Save button — replacing the current flat checkbox list.

Architecture: Frontend-only. Rewrite views.pineap_open in www/js/views.js to build a .pineap-card-settings card: a .pineap-card-title-flex title row ("PineAP" + subtitle + Save button), three grouped sections of .switch slide toggles, and a muted footer. Toggles stage values locally (dirty flags); Save applies only the dirty fields batched per backend route (set_config for logging/capture, enable, mimic, ssidpool/advertise). Two new CSS classes in app.css. No backend changes.

Tech Stack: Vanilla JS (h(), btn(), PagerAPI, App.toast, pineapShell), the existing .switch slider CSS, existing PagerAPI.post routes.

Global Constraints

  • No backend changes. No changes to server.py or tests/.
  • Toggles must NOT call the API on change — they stage locally and mark dirty.
  • Save applies ONLY dirty fields; untouched fields keep their daemon state (preserves the unreadable live Karma state).
  • Batching per route: set_config fields (loghandshake, logpartialhandshake, logpcap, logwigle, logrecon, autossidpool) go in ONE POST /api/pineap/set_config; pineap_disabledPOST /api/pineap/enable {enable}; karmaPOST /api/pineap/mimic {enable}; advertisePOST /api/pineap/ssidpool/advertise {enable}.
  • load() runs once on entry and after a successful Save; it must not clobber staged-but-unsaved values.
  • 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: from repo root, powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1 -Password "<PAGER_PASSWORD>", then /etc/init.d/pagerwebui restart over sshpass.
  • Commit messages follow repo style (feat:, fix:, docs:).

Task 1: Open AP tab Mk7 settings card (CSS + rewrite)

Files:

  • Modify: payload/user/general/pager-webui/www/css/app.css (append three classes)
  • Modify: payload/user/general/pager-webui/www/js/views.js (replace the whole views.pineap_open function, currently lines ~327-375)

Interfaces:

  • Consumes: h(tag, attrs, ...children), btn(label, onclk, cls), PagerAPI.get/post, App.toast, pineapShell(root, hash), tabBar; existing .switch/.track CSS and .pineap-* layout classes from prior work.

  • Produces: views.pineap_open rendering the Mk7 settings card; staged/dirty maps; save() batched apply; load() populating switches without clobbering staged values.

  • Step 1: Append the three CSS classes to app.css

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

/* ---- Open AP: Mark VII PineAP settings card ---- */
.pineap-card-subtitle { color: var(--muted); font-size: 13px; margin: -8px 0 10px; }
.pineap-settings-section { font-size: 13px; font-weight: 500; color: var(--muted); margin: 14px 0 4px; }
.pineap-card-settings .switch { margin: 6px 0; }
  • Step 2: Replace the whole views.pineap_open function in views.js

Replace everything from views.pineap_open = (root) => { through the closing }; of that function (current lines ~327-375, i.e. just before const EVIL_ENC) with:

views.pineap_open = (root) => {
  const box = pineapShell(root, '#/pineap/open');
  const wrap = h('div', { class: 'pineap-title-card pineap-card-settings' });
  wrap.appendChild(h('div', { class: 'pineap-card-title-flex' },
    h('span', { text: 'PineAP' }),
    h('span', { class: 'toolbar-spacer' }),
    btn('Save', save, '')));
  wrap.appendChild(h('div', { class: 'pineap-card-subtitle', text: 'Quickly set the general behavior of PineAP' }));

  const staged = {};
  const dirty = {};
  const toggles = {};
  const groups = [
    ['Karma', [
      ['pineap_disabled', 'Enable PineAP'],
      ['karma', 'Karma']
    ]],
    ['SSID Pool', [
      ['autossidpool', 'Capture SSIDs to Pool'],
      ['advertise', 'Advertise AP Impersonation Pool']
    ]],
    ['Logging', [
      ['loghandshake', 'Log Handshakes'],
      ['logpartialhandshake', 'Log Partial Handshakes'],
      ['logpcap', 'Log PCAP'],
      ['logwigle', 'Log WiGLE'],
      ['logrecon', 'Log Recon']
    ]]
  ];
  groups.forEach(([section, items]) => {
    wrap.appendChild(h('div', { class: 'pineap-settings-section', text: section }));
    items.forEach(([k, label]) => {
      const cb = h('input', { type: 'checkbox', id: 'oap-' + k });
      toggles[k] = cb;
      cb.addEventListener('change', () => { staged[k] = cb.checked; dirty[k] = true; });
      wrap.appendChild(h('label', { class: 'switch' },
        cb, h('span', { class: 'track' }), ' ' + label));
    });
  });
  const info = h('div', { class: 'muted', style: 'margin-top:10px' });
  wrap.appendChild(info);
  box.appendChild(wrap);

  function save() {
    const keys = Object.keys(dirty);
    if (!keys.length) { App.toast('No changes'); return; }
    const reqs = [];
    const setCfg = {};
    keys.forEach((k) => {
      if (k === 'pineap_disabled') reqs.push(PagerAPI.post('/api/pineap/enable', { enable: staged[k] }));
      else if (k === 'karma') reqs.push(PagerAPI.post('/api/pineap/mimic', { enable: staged[k] }));
      else if (k === 'advertise') reqs.push(PagerAPI.post('/api/pineap/ssidpool/advertise', { enable: staged[k] }));
      else setCfg[k] = staged[k];
    });
    if (Object.keys(setCfg).length) reqs.push(PagerAPI.post('/api/pineap/set_config', setCfg));
    Promise.allSettled(reqs).then((results) => {
      const ok = results.every((r) => r.status === 'fulfilled');
      App.toast(ok ? 'Settings saved' : 'Some settings failed', ok ? '' : 'error');
      Object.keys(dirty).forEach((k) => delete dirty[k]);
      load();
    });
  }

  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: {} }))
    ]).then(([cfg, host, ap]) => {
      const c = cfg.data || {}, hh = host.data || {}, a = ap.data || {};
      if (!dirty.pineap_disabled) toggles.pineap_disabled.checked = !hh.pineap_disabled;
      if (!dirty.karma) toggles.karma.checked = !!c.mimic;
      ['loghandshake', 'logpartialhandshake', 'logpcap', 'logwigle', 'logrecon', 'autossidpool']
        .forEach((k) => { if (!dirty[k]) toggles[k].checked = !!c[k]; });
      const pool = a.pool || {};
      if (!dirty.advertise) toggles.advertise.checked = pool.disabled === false;
      const o = a.open || {};
      info.textContent = 'PineAP MAC: ' + (o.bssid || '—') + '   Target MAC: ' + (o.target || '—');
    });
  }
  load();
  return { destroy: () => {} };
};
  • Step 3: Run the JS delimiter balance check

Run: & "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe" "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/css/app.css payload/user/general/pager-webui/www/js/views.js
git commit -m "feat: Mark VII PineAP settings card for Open AP tab with batched save"

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 (over sshpass SSH):

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: Confirm the deployed files contain the new code
sshpass -p "<PAGER_PASSWORD>" ssh root@172.16.52.1 "grep -c 'pineap-settings-section' /root/payloads/user/general/pager-webui/www/css/app.css; grep -c 'Quickly set the general behavior of PineAP' /root/payloads/user/general/pager-webui/www/js/views.js"

Expected: both counts greater than zero.

  • Step 3: On-device save-path check (read-only + one harmless write/restore)

Over sshpass SSH, base64 a script and run it via echo ... | base64 -d | sh:

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 before:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/get_config | grep -o '"logrecon":[a-z]*'
curl -s -b /tmp/pwj -X POST http://127.0.0.1:8080/api/pineap/set_config -d '{"logrecon":true}' > /dev/null
echo after-set-true:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/get_config | grep -o '"logrecon":[a-z]*'
curl -s -b /tmp/pwj -X POST http://127.0.0.1:8080/api/pineap/set_config -d '{"logrecon":false}' > /dev/null
echo restored:; curl -s -b /tmp/pwj http://127.0.0.1:8080/api/pineap/get_config | grep -o '"logrecon":[a-z]*'

Expected: before shows the current logrecon value, after-set-true shows true, restored shows the original value (verifies the batched set_config merge path the Save button uses).

  • Step 4: Report for user UI walk

Tell the user the Open AP tab is now the Mk7 "PineAP" settings card: title + subtitle "Quickly set the general behavior of PineAP", grouped iOS-style slide switches (Karma / SSID Pool / Logging), and a Save button that batches only the changed toggles (unchanged Karma is left untouched). Ask them to refresh http://172.16.52.1:8080/#/pineap/open, flip a logging toggle, Save, and confirm the toast + that get_config reflects the change.