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:
parent
1866c40edf
commit
d6c1feca97
@ -92,10 +92,13 @@ impl RpcHandler {
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// TollGate — check via opkg (≤24.x) or binary presence (25.x apk-native)
|
||||
// TollGate — check via opkg (≤24.x) or binary presence (25.x apk-native).
|
||||
// The service binary is /usr/bin/tollgate-wrt (per its init.d script),
|
||||
// not /usr/bin/tollgate-module-basic-go — that's only the opkg/apk
|
||||
// *package* name, never an on-disk filename.
|
||||
let tollgate_installed = router
|
||||
.run("/usr/bin/opkg list-installed 2>/dev/null | grep -q '^tollgate-module-basic-go ' || \
|
||||
test -f /usr/bin/tollgate-module-basic-go 2>/dev/null")
|
||||
test -f /usr/bin/tollgate-wrt 2>/dev/null")
|
||||
.map(|(_, code)| code == 0)
|
||||
.unwrap_or(false);
|
||||
|
||||
@ -106,6 +109,7 @@ impl RpcHandler {
|
||||
"metric": router.uci_get("tollgate.main.metric").unwrap_or_default(),
|
||||
"step_size_ms": router.uci_get("tollgate.main.step_size").ok().and_then(|v| v.parse::<u64>().ok()).unwrap_or(0),
|
||||
"price_per_step":router.uci_get("tollgate.main.price_per_step").ok().and_then(|v| v.parse::<u64>().ok()).unwrap_or(0),
|
||||
"min_steps": router.uci_get("tollgate.main.min_steps").ok().and_then(|v| v.parse::<u32>().ok()).unwrap_or(1),
|
||||
"currency": router.uci_get("tollgate.main.currency").unwrap_or_default(),
|
||||
"mint_url": router.uci_get("tollgate.main.mint_url").unwrap_or_default(),
|
||||
})
|
||||
@ -183,6 +187,7 @@ impl RpcHandler {
|
||||
.get("min_steps")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(1) as u32,
|
||||
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
|
||||
};
|
||||
|
||||
let router = Router::connect_password(&host, 22, &ssh_user, &ssh_password)?;
|
||||
|
||||
@ -21,6 +21,8 @@ pub struct TollGateConfig {
|
||||
pub step_size_ms: u64,
|
||||
/// Minimum steps a customer must purchase at once.
|
||||
pub min_steps: u32,
|
||||
/// Whether the TollGate service should be running and enabled at boot.
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for TollGateConfig {
|
||||
@ -31,6 +33,7 @@ impl Default for TollGateConfig {
|
||||
price_sats: 10,
|
||||
step_size_ms: 60_000,
|
||||
min_steps: 1,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,7 +46,7 @@ pub fn apply(router: &Router, cfg: &TollGateConfig) -> Result<()> {
|
||||
"tollgate",
|
||||
&[
|
||||
("tollgate.main", "tollgate"),
|
||||
("tollgate.main.enabled", "1"),
|
||||
("tollgate.main.enabled", if cfg.enabled { "1" } else { "0" }),
|
||||
("tollgate.main.metric", "milliseconds"),
|
||||
("tollgate.main.step_size", &cfg.step_size_ms.to_string()),
|
||||
("tollgate.main.min_steps", &cfg.min_steps.to_string()),
|
||||
|
||||
@ -58,11 +58,14 @@ pub fn install_tollgate(router: &Router) -> Result<()> {
|
||||
pub fn install_tollgate_apk_native(router: &Router) -> Result<()> {
|
||||
info!("[{}] Installing {} (apk-native mode)", router.host, TOLLGATE_PACKAGE);
|
||||
|
||||
// Already installed?
|
||||
// Already installed? The service binary is /usr/bin/tollgate-wrt (per its
|
||||
// init.d script) — TOLLGATE_PACKAGE is only the opkg/apk package name,
|
||||
// never an on-disk filename, so it can't be used for the file-existence
|
||||
// fallback below.
|
||||
let (_, code) = router.run(&format!(
|
||||
"apk list --installed 2>/dev/null | grep -q '^{}' || \
|
||||
test -f /usr/bin/{} 2>/dev/null",
|
||||
TOLLGATE_PACKAGE, TOLLGATE_PACKAGE
|
||||
test -f /usr/bin/tollgate-wrt 2>/dev/null",
|
||||
TOLLGATE_PACKAGE
|
||||
))?;
|
||||
if code == 0 {
|
||||
info!("[{}] {} already installed", router.host, TOLLGATE_PACKAGE);
|
||||
@ -162,23 +165,14 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
|
||||
"cd /tmp/_tg_install && ar x {} 2>&1", ipk_path
|
||||
))?;
|
||||
|
||||
if ar_code == 0 {
|
||||
// ar succeeded: unpack data.tar.gz from the inner archive.
|
||||
let (tar_out, tar_code) = router.run(
|
||||
"tar -xzf /tmp/_tg_install/data.tar.gz -C / 2>&1"
|
||||
)?;
|
||||
if tar_code != 0 {
|
||||
anyhow::bail!("TollGate installation failed: data extract failed: {}", tar_out.trim());
|
||||
}
|
||||
// Run postinst if present (optional — failures are non-fatal).
|
||||
router.run_ok(
|
||||
"if tar -xzf /tmp/_tg_install/control.tar.gz -C /tmp/_tg_install 2>/dev/null; then \
|
||||
chmod +x /tmp/_tg_install/postinst 2>/dev/null; \
|
||||
/tmp/_tg_install/postinst configure 2>/dev/null || true; \
|
||||
fi"
|
||||
)?;
|
||||
} else {
|
||||
// Fallback: some packages ship .ipk as a plain gzip tarball.
|
||||
if ar_code != 0 {
|
||||
// Fallback: some builds produce the .ipk as a gzip tarball rather than
|
||||
// a classic `ar` archive. This can still contain the same three ipk
|
||||
// members (debian-binary/data.tar.gz/control.tar.gz) one level deep —
|
||||
// just gzip-tarred together instead of ar'd — or, less commonly, a
|
||||
// flat tarball of the real package files with no ipk structure at
|
||||
// all. Extract to the scratch dir and check which shape it is before
|
||||
// deciding how to install it.
|
||||
info!("[{}] ar failed ({}), trying tar -xzf", router.host, ar_out.trim());
|
||||
|
||||
// List contents first — validates format without writing anything.
|
||||
@ -194,29 +188,67 @@ fn install_ipk(router: &Router, ipk_path: &str) -> Result<()> {
|
||||
}
|
||||
info!("[{}] ipk contents:\n{}", router.host, list_out.trim());
|
||||
|
||||
// Check free space on the root overlay before writing.
|
||||
let (ov_df, _) = router.run("df / 2>/dev/null | awk 'NR==2{print $4}'")?;
|
||||
let overlay_free_kb: u64 = ov_df.trim().parse().unwrap_or(0);
|
||||
if overlay_free_kb < 5120 {
|
||||
anyhow::bail!(
|
||||
"Not enough space to install TollGate: only {}kB free on /. \
|
||||
Need at least 5MB. Free up flash space on the router first \
|
||||
(e.g. remove unused packages with `apk del …`).",
|
||||
overlay_free_kb
|
||||
);
|
||||
}
|
||||
router.run_ok(&format!("tar -xzf {} -C /tmp/_tg_install 2>&1", ipk_path))?;
|
||||
|
||||
let (tar_out, tar_code) = router.run(&format!(
|
||||
"tar -xzf {} -C / 2>&1", ipk_path
|
||||
))?;
|
||||
if tar_code != 0 {
|
||||
anyhow::bail!(
|
||||
"TollGate installation failed: tar extract failed: {}",
|
||||
tar_out.trim()
|
||||
);
|
||||
let (_, nested) = router.run("test -f /tmp/_tg_install/data.tar.gz")?;
|
||||
if nested != 0 {
|
||||
// Genuinely flat tarball, no ipk structure — its contents are the
|
||||
// real package files, already unpacked into the scratch dir.
|
||||
let (ov_df, _) = router.run("df / 2>/dev/null | awk 'NR==2{print $4}'")?;
|
||||
let overlay_free_kb: u64 = ov_df.trim().parse().unwrap_or(0);
|
||||
if overlay_free_kb < 5120 {
|
||||
anyhow::bail!(
|
||||
"Not enough space to install TollGate: only {}kB free on /. \
|
||||
Need at least 5MB. Free up flash space on the router first \
|
||||
(e.g. remove unused packages with `apk del …`).",
|
||||
overlay_free_kb
|
||||
);
|
||||
}
|
||||
let (cp_out, cp_code) = router.run("cp -a /tmp/_tg_install/. / 2>&1")?;
|
||||
if cp_code != 0 {
|
||||
anyhow::bail!("TollGate installation failed: file copy failed: {}", cp_out.trim());
|
||||
}
|
||||
// No package-manager postinst ran for these files either — see
|
||||
// the uci-defaults note below.
|
||||
router.run_ok(
|
||||
"for f in /etc/uci-defaults/*; do \
|
||||
[ -f \"$f\" ] && ( cd \"$(dirname \"$f\")\" && . \"$f\" ) && rm -f \"$f\"; \
|
||||
done; uci commit 2>/dev/null; true"
|
||||
)?;
|
||||
router.run_ok(&format!("rm -rf /tmp/_tg_install {}", ipk_path))?;
|
||||
return Ok(());
|
||||
}
|
||||
// Nested ipk-member layout — fall through to the shared unpack below.
|
||||
}
|
||||
|
||||
// Unpack data.tar.gz (the real payload) from either the `ar`-extracted or
|
||||
// gzip-tar-extracted scratch dir, then run control.tar.gz's postinst.
|
||||
let (tar_out, tar_code) = router.run(
|
||||
"tar -xzf /tmp/_tg_install/data.tar.gz -C / 2>&1"
|
||||
)?;
|
||||
if tar_code != 0 {
|
||||
anyhow::bail!("TollGate installation failed: data extract failed: {}", tar_out.trim());
|
||||
}
|
||||
// Run postinst if present (optional — failures are non-fatal).
|
||||
router.run_ok(
|
||||
"if tar -xzf /tmp/_tg_install/control.tar.gz -C /tmp/_tg_install 2>/dev/null; then \
|
||||
chmod +x /tmp/_tg_install/postinst 2>/dev/null; \
|
||||
/tmp/_tg_install/postinst configure 2>/dev/null || true; \
|
||||
fi"
|
||||
)?;
|
||||
// `default_postinst` (what most packages' postinst calls, including
|
||||
// this one) only runs pending /etc/uci-defaults/* scripts for packages
|
||||
// it finds in opkg/apk's own file-list records. Since these files were
|
||||
// extracted manually rather than through a real package-manager install,
|
||||
// no such record exists, so run any pending scripts directly — this is
|
||||
// exactly what opkg's install path (or the next reboot) would otherwise
|
||||
// do for them, just without waiting for either.
|
||||
router.run_ok(
|
||||
"for f in /etc/uci-defaults/*; do \
|
||||
[ -f \"$f\" ] && ( cd \"$(dirname \"$f\")\" && . \"$f\" ) && rm -f \"$f\"; \
|
||||
done; uci commit 2>/dev/null; true"
|
||||
)?;
|
||||
|
||||
router.run_ok(&format!("rm -rf /tmp/_tg_install {}", ipk_path))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -31,14 +31,31 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
|
||||
}
|
||||
config::apply(router, config)?;
|
||||
wifi::provision_ssid(router, config)?;
|
||||
restart_services(router)?;
|
||||
restart_services(router, config.enabled)?;
|
||||
|
||||
info!("[{}] TollGate provisioning complete", router.host);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restart_services(router: &Router) -> Result<()> {
|
||||
router.run_ok("/etc/init.d/tollgate restart || true")?;
|
||||
/// Applies `enabled` to the actual running service, not just the UCI value —
|
||||
/// the tollgate-wrt init script doesn't consult `tollgate.main.enabled`
|
||||
/// itself, so toggling it requires an explicit enable/start or disable/stop.
|
||||
///
|
||||
/// The service's init script is `/etc/init.d/tollgate-wrt` (its actual
|
||||
/// on-disk name — "tollgate" alone does not exist).
|
||||
fn restart_services(router: &Router, enabled: bool) -> Result<()> {
|
||||
if enabled {
|
||||
router.run_ok("/etc/init.d/tollgate-wrt enable")?;
|
||||
router.run_ok(
|
||||
"/etc/init.d/tollgate-wrt restart || /etc/init.d/tollgate-wrt start"
|
||||
)?;
|
||||
} else {
|
||||
router.run_ok("/etc/init.d/tollgate-wrt stop || true")?;
|
||||
router.run_ok("/etc/init.d/tollgate-wrt disable || true")?;
|
||||
}
|
||||
router.run_ok("/etc/init.d/network restart")?;
|
||||
// Reload wireless so wireless.tollgate.disabled takes effect on the radio —
|
||||
// `network restart` alone doesn't reliably reconfigure wifi interfaces.
|
||||
router.run_ok("wifi down 2>&1; wifi up 2>&1")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -4,27 +4,30 @@ use tracing::info;
|
||||
use crate::tollgate::TollGateConfig;
|
||||
use crate::Router;
|
||||
|
||||
/// Create a dedicated pay-as-you-go WiFi interface for TollGate.
|
||||
/// Create (or update) the dedicated pay-as-you-go WiFi interface for TollGate.
|
||||
///
|
||||
/// Adds a new `wifi-iface` section on the first detected radio, sets the SSID,
|
||||
/// marks it as an open network, and ties it to a TollGate firewall zone.
|
||||
/// Uses a fixed named section (`wireless.tollgate`) rather than `uci add`, so
|
||||
/// re-provisioning (e.g. editing price/mint URL after install) updates the
|
||||
/// same interface in place instead of piling up a new `wifi-iface` section —
|
||||
/// and therefore a new duplicate broadcast SSID — on every call.
|
||||
pub fn provision_ssid(router: &Router, cfg: &TollGateConfig) -> Result<()> {
|
||||
let radio = detect_radio(router).context("detect WiFi radio")?;
|
||||
info!("[{}] Using radio {} for TollGate SSID", router.host, radio);
|
||||
|
||||
// Add a new wifi-iface section; uci add returns the section name (e.g. "cfg123456").
|
||||
let section = router.uci_add("wireless", "wifi-iface")?;
|
||||
|
||||
router.uci_apply(
|
||||
"wireless",
|
||||
&[
|
||||
(&format!("wireless.{}.device", section), &radio),
|
||||
(&format!("wireless.{}.mode", section), "ap"),
|
||||
(&format!("wireless.{}.ssid", section), &cfg.ssid),
|
||||
(&format!("wireless.{}.encryption", section), "none"),
|
||||
(&format!("wireless.{}.network", section), "tollgate"),
|
||||
("wireless.tollgate", "wifi-iface"),
|
||||
("wireless.tollgate.device", &radio),
|
||||
("wireless.tollgate.mode", "ap"),
|
||||
("wireless.tollgate.ssid", &cfg.ssid),
|
||||
("wireless.tollgate.encryption", "none"),
|
||||
("wireless.tollgate.network", "tollgate"),
|
||||
// Disable 802.11r/k/v — unnecessary for transient pay-as-you-go clients.
|
||||
(&format!("wireless.{}.ieee80211r", section), "0"),
|
||||
("wireless.tollgate.ieee80211r", "0"),
|
||||
// Stop broadcasting entirely when disabled, rather than leaving an
|
||||
// open SSID up that leads nowhere once the backend is stopped.
|
||||
("wireless.tollgate.disabled", if cfg.enabled { "0" } else { "1" }),
|
||||
],
|
||||
)?;
|
||||
|
||||
|
||||
@ -45,6 +45,12 @@ impl Router {
|
||||
|
||||
/// Batch: apply a list of `(key, value)` pairs then commit the config.
|
||||
pub fn uci_apply(&self, config: &str, pairs: &[(&str, &str)]) -> Result<()> {
|
||||
// `uci set config.section=type` fails with "Entry not found" if
|
||||
// /etc/config/<config> doesn't exist yet — true for any config file
|
||||
// shipped by the base system (wireless, network, dhcp, ...) but not
|
||||
// for a package-defined namespace like "tollgate" that nothing has
|
||||
// created a default for. `touch` is a no-op if it already exists.
|
||||
self.run_ok(&format!("touch /etc/config/{}", config))?;
|
||||
for (key, value) in pairs {
|
||||
self.uci_set(key, value)?;
|
||||
}
|
||||
|
||||
@ -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 -->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user