luci-app-travelmate: sync with travelmate 0.9.0 1238/head
authorDirk Brenken <dev@brenken.org>
Wed, 19 Jul 2017 20:36:47 +0000 (22:36 +0200)
committerDirk Brenken <dev@brenken.org>
Thu, 20 Jul 2017 05:44:15 +0000 (07:44 +0200)
* add a wireless station manager to edit and delete existing
  interfaces or scan for new uplinks

Signed-off-by: Dirk Brenken <dev@brenken.org>
applications/luci-app-travelmate/luasrc/controller/travelmate.lua
applications/luci-app-travelmate/luasrc/model/cbi/travelmate/overview_tab.lua
applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_add.lua [new file with mode: 0644]
applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_delete.lua [new file with mode: 0644]
applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_edit.lua [new file with mode: 0644]
applications/luci-app-travelmate/luasrc/view/travelmate/runtime.htm
applications/luci-app-travelmate/luasrc/view/travelmate/stations.htm [new file with mode: 0644]
applications/luci-app-travelmate/luasrc/view/travelmate/wifi_scan.htm [new file with mode: 0644]

index 86382f6ae06332d42fa7381ed34358395306cbea..71fb179961dd81c9f3fbb2fda132f10d2bf56e24 100644 (file)
@@ -5,8 +5,8 @@ module("luci.controller.travelmate", package.seeall)
 
 local fs = require("nixio.fs")
 local util = require("luci.util")
-local template = require("luci.template")
 local i18n = require("luci.i18n")
+local templ = require("luci.template")
 
 function index()
        if not nixio.fs.access("/etc/config/travelmate") then
@@ -14,15 +14,21 @@ function index()
        end
        entry({"admin", "services", "travelmate"}, firstchild(), _("Travelmate"), 40).dependent = false
        entry({"admin", "services", "travelmate", "tab_from_cbi"}, cbi("travelmate/overview_tab", {hideresetbtn=true, hidesavebtn=true}), _("Overview"), 10).leaf = true
-       entry({"admin", "services", "travelmate", "logfile"}, call("logread"), _("View Logfile"), 20).leaf = true
+       entry({"admin", "services", "travelmate", "stations"}, template("travelmate/stations"), _("Wireless Stations"), 20).leaf = true
+       entry({"admin", "services", "travelmate", "logfile"}, call("logread"), _("View Logfile"), 30).leaf = true
        entry({"admin", "services", "travelmate", "advanced"}, firstchild(), _("Advanced"), 100)
        entry({"admin", "services", "travelmate", "advanced", "configuration"}, cbi("travelmate/configuration_tab"), _("Edit Travelmate Configuration"), 110).leaf = true
        entry({"admin", "services", "travelmate", "advanced", "cfg_wireless"}, cbi("travelmate/cfg_wireless_tab"), _("Edit Wireless Configuration"), 120).leaf = true
        entry({"admin", "services", "travelmate", "advanced", "cfg_network"}, cbi("travelmate/cfg_network_tab"), _("Edit Network Configuration"), 130).leaf = true
        entry({"admin", "services", "travelmate", "advanced", "cfg_firewall"}, cbi("travelmate/cfg_firewall_tab"), _("Edit Firewall Configuration"), 140).leaf = true
+
+       entry({"admin", "services", "travelmate", "wifiscan"}, template("travelmate/wifi_scan")).leaf = true
+       entry({"admin", "services", "travelmate", "wifiadd"}, cbi("travelmate/wifi_add", {hideresetbtn=true, hidesavebtn=true})).leaf = true
+       entry({"admin", "services", "travelmate", "wifiedit"}, cbi("travelmate/wifi_edit", {hideresetbtn=true, hidesavebtn=true})).leaf = true
+       entry({"admin", "services", "travelmate", "wifidelete"}, cbi("travelmate/wifi_delete", {hideresetbtn=true, hidesavebtn=true})).leaf = true
 end
 
 function logread()
        local logfile = util.trim(util.exec("logread -e 'travelmate'"))
-       template.render("travelmate/logread", {title = i18n.translate("Travelmate Logfile"), content = logfile})
+       templ.render("travelmate/logread", {title = i18n.translate("Travelmate Logfile"), content = logfile})
 end
index 236bbb0de5669ed96c2d8be063614c3538267bc1..64ab880c4d8a4ca7ad8cb6e22ff18deecd126d38 100644 (file)
@@ -2,12 +2,13 @@
 -- This is free software, licensed under the Apache License, Version 2.0
 
 local fs = require("nixio.fs")
-local uci = require("uci")
+local uci = require("luci.model.uci").cursor()
 local json = require("luci.jsonc")
 local nw  = require("luci.model.network").init()
 local fw  = require("luci.model.firewall").init()
-local uplink = uci.get("network", "trm_wwan") or ""
+local trmiface = uci.get("travelmate", "global", "trm_iface") or "trm_wwan"
 local trminput = uci.get("travelmate", "global", "trm_rtfile") or "/tmp/trm_runtime.json"
+local uplink = uci.get("network", trmiface) or ""
 local parse = json.parse(fs.readfile(trminput) or "")
 
 m = Map("travelmate", translate("Travelmate"),
@@ -21,70 +22,81 @@ function m.on_after_commit(self)
        luci.http.redirect(luci.dispatcher.build_url("admin", "services", "travelmate"))
 end
 
--- Main travelmate options
-
 s = m:section(NamedSection, "global", "travelmate")
 
-o1 = s:option(Flag, "trm_enabled", translate("Enable travelmate"))
-o1.default = o1.disabled
-o1.rmempty = false
-
-o2 = s:option(Flag, "trm_automatic", translate("Enable 'automatic' mode"),
-       translate("Keep travelmate in an active state."))
-o2.default = o2.enabled
-o2.rmempty = false
+-- Interface Wizard
 
-o3 = s:option(Value, "trm_iface", translate("Restrict interface trigger to certain interface(s)"),
-       translate("Space separated list of interfaces that trigger travelmate processing. "..
-       "To disable event driven (re-)starts remove all entries."))
-o3.rmempty = true
+if uplink == "" then
+       dv = s:option(DummyValue, "nil", translate("Interface Wizard"))
+       dv.template = "cbi/nullsection"
 
-o4 = s:option(Value, "trm_triggerdelay", translate("Trigger delay"),
-       translate("Additional trigger delay in seconds before travelmate processing begins."))
-o4.default = 2
-o4.datatype = "range(1,90)"
-o4.rmempty = false
+       o = s:option(Value, "trm_iface", translate("Uplink interface"))
+       o.datatype = "and(uciname,rangelength(3,15))"
+       o.default = "trm_wwan"
+       o.rmempty = false
 
-o5 = s:option(Flag, "trm_debug", translate("Enable verbose debug logging"))
-o5.default = o5.disabled
-o5.rmempty = false
+       function o.validate(self, value)
+               iface = value
+               return iface
+       end
 
--- Interface setup
+       function o.write(self, section, value)
+               uci:set("travelmate", section, "trm_iface", iface)
+               uci:save("travelmate")
+               uci:commit("travelmate")
+       end
 
-if uplink == "" then
-       dv = s:option(DummyValue, "_dummy", translate("Interface Setup"))
-       dv.template = "cbi/nullsection"
        btn = s:option(Button, "", translate("Create Uplink Interface"),
-               translate("Automatically create a new wireless wan uplink interface 'trm_wwan', configure it to use dhcp and ")
+               translate("Create a new wireless wan uplink interface, configure it to use dhcp and ")
                .. translate("add it to the wan zone of the firewall. This step has only to be done once."))
        btn.inputtitle = translate("Add Interface")
        btn.inputstyle = "apply"
        btn.disabled = false
        function btn.write()
-               local name = "trm_wwan"
-               local net = nw:add_network(name, { proto = "dhcp" })
+               local net = nw:add_network(iface, { proto = "dhcp" })
                if net then
                        nw:save("network")
                        nw:commit("network")
                        local zone = fw:get_zone_by_network("wan")
                        if zone then
-                               zone:add_network(name)
+                               zone:add_network(iface)
                                fw:save("firewall")
                                fw:commit("firewall")
+                               luci.sys.call("env -i /bin/ubus call network reload >/dev/null 2>&1")
                        end
-                       luci.sys.call("env -i /bin/ubus call network reload >/dev/null 2>&1")
-                       luci.http.redirect(luci.dispatcher.build_url("admin", "services", "travelmate"))
                end
+               luci.http.redirect(luci.dispatcher.build_url("admin", "services", "travelmate"))
        end
-else
-       dv = s:option(DummyValue, "_dummy", translate("Interface Setup"),
-               translate("<br />&nbsp;Network Interface 'trm_wwan' created successfully. ")
-               .. translatef("Scan &amp; Add new wireless stations via standard "
-               .. "<a href=\"%s\">"
-               .. "Wireless Setup</a>", luci.dispatcher.build_url("admin/network/wireless")))
-       dv.template = "cbi/nullsection"
+       return m
 end
 
+-- Main travelmate options
+
+o1 = s:option(Flag, "trm_enabled", translate("Enable travelmate"))
+o1.default = o1.disabled
+o1.rmempty = false
+
+o2 = s:option(Flag, "trm_automatic", translate("Enable 'automatic' mode"),
+       translate("Keep travelmate in an active state."))
+o2.default = o2.enabled
+o2.rmempty = false
+
+o3 = s:option(Value, "trm_iface", translate("Uplink / Trigger interface"),
+       translate("Name of the uplink interface that triggers travelmate processing."))
+o3.datatype = "and(uciname,rangelength(3,15))"
+o3.default = "trm_wwan"
+o3.rmempty = false
+
+o4 = s:option(Value, "trm_triggerdelay", translate("Trigger delay"),
+       translate("Additional trigger delay in seconds before travelmate processing begins."))
+o4.default = 2
+o4.datatype = "range(1,90)"
+o4.rmempty = false
+
+o5 = s:option(Flag, "trm_debug", translate("Enable verbose debug logging"))
+o5.default = o5.disabled
+o5.rmempty = false
+
 -- Runtime information
 
 ds = s:option(DummyValue, "_dummy", translate("Runtime information"))
diff --git a/applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_add.lua b/applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_add.lua
new file mode 100644 (file)
index 0000000..361027f
--- /dev/null
@@ -0,0 +1,65 @@
+-- Copyright 2017 Dirk Brenken (dev@brenken.org)
+-- This is free software, licensed under the Apache License, Version 2.0
+
+local fs = require("nixio.fs")
+local uci = require("luci.model.uci").cursor()
+local http = require("luci.http")
+local trmiface = uci.get("travelmate", "global", "trm_iface") or "trm_wwan"
+
+m = SimpleForm("add", translate("Add Wireless Uplink Configuration"))
+m.cancel = translate("Back to overview")
+m.reset = false
+
+function m.on_cancel()
+       http.redirect(luci.dispatcher.build_url("admin/services/travelmate/stations"))
+end
+
+m.hidden = {
+       device      = http.formvalue("device"),
+       ssid        = http.formvalue("ssid"),
+       wep         = http.formvalue("wep"),
+       wpa_suites      = http.formvalue("wpa_suites"),
+       wpa_version = http.formvalue("wpa_version")
+}
+
+wssid = m:field(Value, "ssid", translate("SSID"))
+wssid.default = m.hidden.ssid
+
+if (tonumber(m.hidden.wep) or 0) == 1 then
+       wkey = m:field(Value, "key", translate("WEP passphrase"),
+               translate("Specify the secret encryption key here."))
+       wkey.password = true
+       wkey.datatype = "wepkey"
+elseif (tonumber(m.hidden.wpa_version) or 0) > 0 and
+       (m.hidden.wpa_suites == "PSK" or m.hidden.wpa_suites == "PSK2")
+then
+       wkey = m:field(Value, "key", translate("WPA passphrase"),
+               translate("Specify the secret encryption key here."))
+       wkey.password = true
+       wkey.datatype = "wpakey"
+end
+
+function wssid.write(self, section, value)
+       newsection = uci:section("wireless", "wifi-iface", nil, {
+               mode       = "sta",
+               network    = trmiface,
+               device     = m.hidden.device,
+               ssid       = wssid:formvalue(section),
+               disabled   = "1"
+       })
+       if (tonumber(m.hidden.wep) or 0) == 1 then
+               uci:set("wireless", newsection, "encryption", "wep-open")
+               uci:set("wireless", newsection, "key", "1")
+               uci:set("wireless", newsection, "key1", wkey:formvalue(section))
+       elseif (tonumber(m.hidden.wpa_version) or 0) > 0 then
+               uci:set("wireless", newsection, "encryption", "psk2")
+               uci:set("wireless", newsection, "key", wkey:formvalue(section))
+       else
+               uci:set("wireless", newsection, "encryption", "none")
+       end
+       uci:save("wireless")
+       uci:commit("wireless")
+       http.redirect(luci.dispatcher.build_url("admin/services/travelmate/stations"))
+end
+
+return m
diff --git a/applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_delete.lua b/applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_delete.lua
new file mode 100644 (file)
index 0000000..97ec1ca
--- /dev/null
@@ -0,0 +1,14 @@
+-- Copyright 2017 Dirk Brenken (dev@brenken.org)
+-- This is free software, licensed under the Apache License, Version 2.0
+
+local uci = require("luci.model.uci").cursor()
+local http = require("luci.http")
+local cfg = http.formvalue("cfg")
+
+if cfg ~= nil then
+       uci:delete("wireless", cfg)
+       uci:save("wireless")
+       uci:commit("wireless")
+end
+
+http.redirect(luci.dispatcher.build_url("admin/services/travelmate/stations"))
diff --git a/applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_edit.lua b/applications/luci-app-travelmate/luasrc/model/cbi/travelmate/wifi_edit.lua
new file mode 100644 (file)
index 0000000..0bae984
--- /dev/null
@@ -0,0 +1,49 @@
+-- Copyright 2017 Dirk Brenken (dev@brenken.org)
+-- This is free software, licensed under the Apache License, Version 2.0
+
+local fs = require("nixio.fs")
+local uci = require("luci.model.uci").cursor()
+local http = require("luci.http")
+
+m = SimpleForm("edit", translate("Edit Wireless Uplink Configuration"))
+m.cancel = translate("Back to overview")
+m.reset = false
+
+function m.on_cancel()
+       http.redirect(luci.dispatcher.build_url("admin/services/travelmate/stations"))
+end
+
+m.hidden = {
+       cfg = http.formvalue("cfg")
+}
+
+local s = uci:get_all("wireless", m.hidden.cfg)
+if s ~= nil then
+       wssid = m:field(Value, "ssid", translate("SSID"))
+       wssid.default = s.ssid
+       
+       if s.encryption and s.key then
+               wkey = m:field(Value, "key", translatef("Passphrase (%s)", s.encryption))
+               wkey.password = true
+               wkey.default = s.key
+               if s.encryption == "wep" then
+                       wkey.datatype = "wepkey"
+               else
+                       wkey.datatype = "wpakey"
+               end
+       end
+else
+       http.redirect(luci.dispatcher.build_url("admin/services/travelmate/stations"))
+end
+
+function wssid.write(self, section, value)
+       uci:set("wireless", m.hidden.cfg, "ssid", wssid:formvalue(section))
+       if s.encryption and s.key then
+               uci:set("wireless", m.hidden.cfg, "key", wkey:formvalue(section))
+       end
+       uci:save("wireless")
+       uci:commit("wireless")
+       http.redirect(luci.dispatcher.build_url("admin/services/travelmate/stations"))
+end
+
+return m
index ee3a4553a825b5543f9b05926ebedd42c2aa2e38..2b9885567a3eac331171e8102e6ef294ca4d253a 100644 (file)
@@ -5,6 +5,6 @@ This is free software, licensed under the Apache License, Version 2.0
 
 <%+cbi/valueheader%>
 
-<input name="runtime" id="runtime" type="text" class="cbi-input-text" style="border: none; box-shadow: none; background-color: #ffffff; color: #0069d6;" value="<%=self:cfgvalue(section)%>" disabled="disabled" />
+<input name="runtime" id="runtime" type="text" class="cbi-input-text" style="border:none; box-shadow:none; background-color:#ffffff; color:#0069d6;" value="<%=self:cfgvalue(section)%>" disabled="disabled" />
 
 <%+cbi/valuefooter%>
diff --git a/applications/luci-app-travelmate/luasrc/view/travelmate/stations.htm b/applications/luci-app-travelmate/luasrc/view/travelmate/stations.htm
new file mode 100644 (file)
index 0000000..e79fb0c
--- /dev/null
@@ -0,0 +1,73 @@
+<%#
+Copyright 2017 Dirk Brenken (dev@brenken.org)
+This is free software, licensed under the Apache License, Version 2.0
+-%>
+
+
+<%-
+  local write = io.write
+  local uci = require "luci.model.uci".cursor()
+  local trmiface = uci:get("travelmate", "global", "trm_iface") or "trm_wwan"
+-%>
+
+<%+header%>
+
+<div class="cbi-map">
+<h2 name="content"><%:Wireless Stations%></h2>
+<div class="cbi-map-descr"><%:Provides an overview of all configured uplink interfaces for travelmate. You can edit and delete existing interfaces or scan for new uplinks.%></div>
+
+<fieldset class="cbi-section">
+  <table class="cbi-section-table" style="empty-cells:hide">
+    <tr class="cbi-section-table-titles">
+      <th class="cbi-section-table-cell" style="text-align:left"><%:Device%></th>
+      <th class="cbi-section-table-cell" style="text-align:left"><%:Mode%></th>
+      <th class="cbi-section-table-cell" style="text-align:left"><%:Uplink Interface%></th>
+      <th class="cbi-section-table-cell" style="text-align:left"><%:SSID%></th>
+      <th class="cbi-section-table-cell" style="text-align:left"><%:Encryption%></th>
+      <th class="cbi-section-table-cell" style="text-align:left" colspan="2"><%:Disabled%></th>
+    </tr>
+<%
+  uci:foreach("wireless", "wifi-iface", function(s)
+    local section = s['.name']
+    local device = s.device or ""
+    local mode = s.mode or ""
+    local iface = s.network or ""
+    local ssid = s.ssid or ""
+    local encryption = s.encryption or ""
+    local disabled = s.disabled or ""
+    if iface == trmiface then
+%>
+    <tr class="cbi-section-table-row cbi-rowstyle-6"> 
+      <td style="text-align:left"><%=device%></td>
+      <td style="text-align:left"><%=mode%></td>
+      <td style="text-align:left"><%=iface%></td>
+      <td style="text-align:left"><%=ssid%></td>
+      <td style="text-align:left"><%=encryption%></td>
+      <td style="text-align:left"><%=disabled%></td>
+      <td class="cbi-value-field" style="width:200px;text-align:right">
+        <input type="button" class="cbi-button cbi-button-edit" style="width:85px" onclick="location.href='<%=url('admin/services/travelmate/wifiedit')%>?cfg=<%=section%>'" title="<%:Edit this Uplink%>" value="<%:Edit%>" />
+        <input type="button" class="cbi-button cbi-button-remove" style="width:85px" onclick="location.href='<%=url('admin/services/travelmate/wifidelete')%>?cfg=<%=section%>'" title="<%:Delete this Uplink%>" value="<%:Delete%>"/>
+      </td>
+    </tr>
+<% 
+    end
+  end)
+%>
+  </table>
+</fieldset>
+<div class="cbi-page-actions right">
+<%
+  uci:foreach("wireless", "wifi-device", function(s)
+    local device = s[".name"]
+%>
+  <form class="inline" action="<%=url('admin/services/travelmate/wifiscan')%>" method="post">
+    <input type="hidden" name="device" value="<%=device%>" />
+    <input type="hidden" name="token" value="<%=token%>" />
+    <input type="submit" class="cbi-button cbi-button-find" style="width:110px" title="<%:Find and join network on %><%=device%>" value="<%:Scan %><%=device%>" />
+  </form>
+<%
+  end)
+%>
+</div>
+
+<%+footer%>
diff --git a/applications/luci-app-travelmate/luasrc/view/travelmate/wifi_scan.htm b/applications/luci-app-travelmate/luasrc/view/travelmate/wifi_scan.htm
new file mode 100644 (file)
index 0000000..e1818a0
--- /dev/null
@@ -0,0 +1,108 @@
+<%#
+Copyright 2017 Dirk Brenken (dev@brenken.org)
+This is free software, licensed under the Apache License, Version 2.0
+-%>
+
+<%-
+    local sys = require "luci.sys"
+    local utl = require "luci.util"
+    local dev = luci.http.formvalue("device")
+    local iw = luci.sys.wifi.getiwinfo(dev)
+
+    if not iw then
+        luci.http.redirect(luci.dispatcher.build_url("admin/services/travelmate/stations"))
+    end
+
+    function format_wifi_encryption(info)
+        if info.wep == true then
+            return translate("WEP")
+        elseif info.wpa > 0 then
+            return translate("WPA / WPA2")
+        elseif info.enabled then
+            return translate("Unknown")
+        else
+            return translate("Open")
+        end
+    end
+
+    function percent_wifi_signal(info)
+        local qc = info.quality or 0
+        local qm = info.quality_max or 0
+        if info.bssid and qc > 0 and qm > 0 then
+            return math.floor((100 / qm) * qc)
+        else
+            return 0
+        end
+    end
+
+    function scanlist(times)
+        local i, k, v
+        local l = { }
+        local s = { }
+
+        for i = 1, times do
+            for k, v in ipairs(iw.scanlist or { }) do
+                if not s[v.bssid] then
+                    l[#l+1] = v
+                    s[v.bssid] = true
+                end
+            end
+        end
+        return l
+    end
+-%>
+
+<%+header%>
+
+<div class="cbi-map">
+<h2 name="content"><%:Wireless Scan%></h2>
+
+    <fieldset class="cbi-section">
+        <table class="cbi-section-table" style="empty-cells:hide">
+            <tr class="cbi-section-table-titles">
+                <th class="cbi-section-table-cell" style="text-align:left"><%:Uplink SSID%></th>
+                <th class="cbi-section-table-cell" style="text-align:left"><%:Encryption%></th>
+                <th class="cbi-section-table-cell" style="text-align:left" colspan="2"><%:Signal strength%></th>
+            </tr>
+            <% for i, net in ipairs(scanlist(3)) do net.encryption = net.encryption or { } %>
+            <tr class="cbi-section-table-row cbi-rowstyle-4">
+                <td class="cbi-value-field" style="text-align:left">
+                    <strong><%=net.ssid and utl.pcdata(net.ssid) or "<em>%s</em>" % translate("hidden")%></strong>
+                </td>
+                <td class="cbi-value-field" style="text-align:left">
+                    <%=format_wifi_encryption(net.encryption)%>
+                </td>
+                <td class="cbi-value-field" style="text-align:left">
+                    <%=percent_wifi_signal(net)%> %
+                </td>
+                <td class="cbi-value-field" style="width:120px; text-align:right">
+                    <form class="inline" action="<%=url('admin/services/travelmate/wifiadd')%>" method="post">
+                        <input type="hidden" name="token" value="<%=token%>" />
+                        <input type="hidden" name="device" value="<%=utl.pcdata(dev)%>" />
+                        <input type="hidden" name="ssid" value="<%=utl.pcdata(net.ssid)%>" />
+                        <input type="hidden" name="wep" value="<%=net.encryption.wep and 1 or 0%>" />
+                        <% if net.encryption.wpa then %>
+                        <input type="hidden" name="wpa_version" value="<%=net.encryption.wpa%>" />
+                        <% for _, v in ipairs(net.encryption.auth_suites) do %><input type="hidden" name="wpa_suites" value="<%=v%>" />
+                        <% end; for _, v in ipairs(net.encryption.group_ciphers) do %><input type="hidden" name="wpa_group" value="<%=v%>" />
+                        <% end; for _, v in ipairs(net.encryption.pair_ciphers) do %><input type="hidden" name="wpa_pairwise" value="<%=v%>" />
+                        <% end; end %>
+                        <input class="cbi-button cbi-button-apply" style="width:110px" type="submit" value="<%:Add Uplink%>" />
+                    </form>
+                </td>
+            </tr>
+            <% end %>
+        </table>
+    </fieldset>
+<div class="cbi-page-actions right">
+    <form class="inline" action="<%=url('admin/services/travelmate/stations')%>" method="post">
+        <input class="cbi-button cbi-button-reset" type="submit" value="<%:Back to overview%>" />
+    </form>
+    <form class="inline" action="<%=url('admin/services/travelmate/wifiscan')%>" method="post">
+        <input type="hidden" name="token" value="<%=token%>" />
+        <input type="hidden" name="device" value="<%=utl.pcdata(dev)%>" />
+        <input class="cbi-button cbi-input-find" type="submit" value="<%:Repeat scan%>" />
+    </form>
+</div>
+
+<%+footer%>