fix(openwrt): fix TollGate provisioning pipeline, add reconfigure UI
Several compounding bugs were blocking end-to-end TollGate provisioning
on OpenWrt 25.x (apk-native) routers:
- install_ipk's non-ar fallback assumed a flat tarball, but some .ipks are
a gzip tar of the three classic ipk members one level deep; it was
dumping debian-binary/data.tar.gz/control.tar.gz straight into / instead
of unpacking the real payload.
- Manually-extracted packages never ran their pending /etc/uci-defaults/*
scripts (that only happens through opkg/apk's own postinst bookkeeping),
so nothing ever created /etc/config/tollgate.
- uci_apply() never ensured the target config file existed first — `uci
set` fails outright on a config namespace nothing has created yet, which
is true for a package-defined one like "tollgate" (unlike wireless/
network/dhcp, which ship by default).
- The installed-check and restart_services looked for a binary/init script
named after the opkg package ("tollgate-module-basic-go"/"tollgate"),
but the real on-disk names are tollgate-wrt — so status always reported
"not installed" and service restarts silently no-op'd.
- provision_ssid used `uci add`, creating a new wifi-iface section (and
therefore a new duplicate broadcast SSID) on every provision call instead
of updating one in place.
Also adds a TollGateConfig.enabled field so the enable/disable state is
actually applied to the running service and the SSID's own broadcast
(stop + disable at boot, or start + enable), not just written to UCI.
On the frontend, the OpenWrt Gateway page's TollGate panel was read-only
once installed — add an edit form (price, step size, min steps, mint URL,
enabled toggle) that reuses the same idempotent provision-tollgate call.
This commit is contained in:
@@ -26,6 +26,7 @@ interface TollGateStatus {
|
||||
metric?: string
|
||||
step_size_ms?: number
|
||||
price_per_step?: number
|
||||
min_steps?: number
|
||||
currency?: string
|
||||
mint_url?: string
|
||||
}
|
||||
@@ -80,6 +81,16 @@ const provisioning = ref(false)
|
||||
const provisionError = ref('')
|
||||
const provisionSuccess = ref(false)
|
||||
|
||||
// TollGate reconfigure form (shown once installed)
|
||||
const editingTollgate = ref(false)
|
||||
const updatingTollgate = ref(false)
|
||||
const updateTollgateError = ref('')
|
||||
const editPriceSats = ref(10)
|
||||
const editStepSizeMin = ref(1)
|
||||
const editMinSteps = ref(1)
|
||||
const editMintUrl = ref('')
|
||||
const editEnabled = ref(true)
|
||||
|
||||
// WAN setup flow
|
||||
type WanStep = 'idle' | 'scan' | 'scanning' | 'list' | 'password' | 'dhcp' | 'connecting' | 'done'
|
||||
const wanStep = ref<WanStep>('idle')
|
||||
@@ -147,6 +158,41 @@ async function provisionTollgate() {
|
||||
}
|
||||
}
|
||||
|
||||
function startEditTollgate() {
|
||||
const tg = status.value?.tollgate
|
||||
editPriceSats.value = tg?.price_per_step ?? 10
|
||||
editStepSizeMin.value = Math.max(1, Math.round((tg?.step_size_ms ?? 60000) / 60000))
|
||||
editMinSteps.value = tg?.min_steps ?? 1
|
||||
editMintUrl.value = tg?.mint_url ?? ''
|
||||
editEnabled.value = tg?.enabled ?? true
|
||||
updateTollgateError.value = ''
|
||||
editingTollgate.value = true
|
||||
}
|
||||
|
||||
async function saveTollgateConfig() {
|
||||
updatingTollgate.value = true
|
||||
updateTollgateError.value = ''
|
||||
try {
|
||||
const params: Record<string, unknown> = {
|
||||
host: connectedParams.value?.host ?? status.value?.host,
|
||||
ssh_user: connectedParams.value?.ssh_user ?? sshUser.value,
|
||||
ssh_password: connectedParams.value?.ssh_password ?? sshPassword.value,
|
||||
price_sats: editPriceSats.value,
|
||||
step_size_ms: editStepSizeMin.value * 60_000,
|
||||
min_steps: editMinSteps.value,
|
||||
mint_url: editMintUrl.value,
|
||||
enabled: editEnabled.value,
|
||||
}
|
||||
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
|
||||
editingTollgate.value = false
|
||||
await load(connectedParams.value ?? undefined)
|
||||
} catch (e) {
|
||||
updateTollgateError.value = e instanceof Error ? e.message : String(e)
|
||||
} finally {
|
||||
updatingTollgate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startWanSetup() {
|
||||
wanStep.value = 'scan'
|
||||
wanError.value = ''
|
||||
@@ -623,13 +669,16 @@ onMounted(() => load())
|
||||
<p v-if="provisionSuccess && !provisioning" class="mt-3 text-xs text-green-400">TollGate provisioned successfully.</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="w-2 h-2 rounded-full inline-block"
|
||||
:class="status.tollgate.enabled ? 'bg-green-400' : 'bg-yellow-400'"></span>
|
||||
<span class="text-sm" :class="status.tollgate.enabled ? 'text-green-300' : 'text-yellow-300'">
|
||||
{{ status.tollgate.enabled ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
<template v-else-if="!editingTollgate">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full inline-block"
|
||||
:class="status.tollgate.enabled ? 'bg-green-400' : 'bg-yellow-400'"></span>
|
||||
<span class="text-sm" :class="status.tollgate.enabled ? 'text-green-300' : 'text-yellow-300'">
|
||||
{{ status.tollgate.enabled ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="text-xs text-white/40 hover:text-white" @click="startEditTollgate">Edit</button>
|
||||
</div>
|
||||
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
||||
<div>
|
||||
@@ -649,6 +698,76 @@ onMounted(() => load())
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
<!-- Reconfigure form -->
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between mb-4 py-3 border-b border-white/10">
|
||||
<div>
|
||||
<div class="text-sm text-white">Enable TollGate</div>
|
||||
<div class="text-xs text-white/40">Stops the service and the SSID broadcast when off</div>
|
||||
</div>
|
||||
<button
|
||||
class="relative w-11 h-6 rounded-full transition-colors flex-shrink-0"
|
||||
:class="editEnabled ? 'bg-green-500/60' : 'bg-white/15'"
|
||||
@click="editEnabled = !editEnabled"
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 w-5 h-5 rounded-full bg-white transition-transform"
|
||||
:class="editEnabled ? 'translate-x-5' : 'translate-x-0.5'"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs text-white/40 mb-2">Price per step (sats)</label>
|
||||
<input
|
||||
v-model.number="editPriceSats"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs text-white/40 mb-2">Step size (minutes)</label>
|
||||
<input
|
||||
v-model.number="editStepSizeMin"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs text-white/40 mb-2">Minimum steps per purchase</label>
|
||||
<input
|
||||
v-model.number="editMinSteps"
|
||||
type="number"
|
||||
min="1"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs text-white/40 mb-2">Mint URL</label>
|
||||
<input
|
||||
v-model="editMintUrl"
|
||||
type="text"
|
||||
placeholder="http://..."
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
:disabled="updatingTollgate"
|
||||
class="glass-button glass-button-success flex-1 text-sm font-medium"
|
||||
:class="updatingTollgate ? 'opacity-40 cursor-not-allowed' : ''"
|
||||
@click="saveTollgateConfig"
|
||||
>
|
||||
{{ updatingTollgate ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
<button class="text-xs text-white/40 hover:text-white px-3 py-2" :disabled="updatingTollgate" @click="editingTollgate = false">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="updateTollgateError" class="mt-3 text-xs text-red-400">{{ updateTollgateError }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- WiFi interfaces -->
|
||||
|
||||
Reference in New Issue
Block a user