diff --git a/etc/config/quickstart b/etc/config/quickstart index 77fb483..dfaf952 100644 --- a/etc/config/quickstart +++ b/etc/config/quickstart @@ -6,119 +6,23 @@ config quickstart options config page 'welcome' option pageTitle 'Welcome' - option topheader 'Welcome to the Commotion Quick Start wizard' - list modules 'welcome' - list modules 'adminPassword' + option topheader 'Welcome to the Commotion Quick Start' + list modules 'adminPassword' + list modules 'name' list buttons 'noBack' - list buttons '2:settingsFile,|Manual Configuration >,| ,| ' - list buttons '3:makeItWork,|Just Make it Work >,| ,| ' + list buttons '2:finish,|Finish >,| ,| ' + list buttons '3:settingsFile,|Use a pre-built network file >,| ,| ' + config page 'settingsFile' option pageTitle 'Using a Settings File' option topheader 'Using a Settings File' - option subheading 'Do you have a settings file? Your network administrator may have provided a settings file. If you have a settings file, please upload it below. Or, you can enter the settings manually.' + option subheading 'If you have a settings file, please upload it below.' option fileType 'config' list modules 'upload' - list buttons '3:noConfigUploaded,|Continue >,|Continue without uploading a file,| ,|' - list buttons '4:wifiSharing,|Upload & Continue >>,|Upload a settings file and continue,| ,|' - list buttons '5:startOver,|<< Start Over,| ,|plus' - -config page 'wifiSharing' - option pageTitle 'Setup Wireless Connection sharing' - option topheader 'Setup Wireless Connection sharing' - option subheading 'Would you like to allow anyone to use your wifi access point?' - list buttons '3:accessPoint,|Yes >,|Set up an open access point,| ,|' - list buttons '4:secAccessPoint,|No >,|Set up a secure access point,| ,|' + list buttons '4:finish,|Upload and Finish,|,| ,|' list buttons '5:startOver,|<< Start Over,| ,|plus' -config page 'accessPoint' - option pageTitle 'Setup an Open Access Point' - option topheader 'Setup an Open Access Point' - option subheading '' - list modules 'accessPoint' - list buttons '2:internetCheck,|Save >,|Continue with this setting,| ,|' - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'secAccessPoint' - option pageTitle 'Setup a Private Access Point' - option topheader 'Setup a Private Access Point' - option subheading '' - list modules 'secAccessPoint' - list buttons '2:internetCheck,|Save >,|Continue with this setting,| ,|' - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'internetCheck' - option pageTitle 'Internet Connectivity' - option topheader 'Internet Connectivity' - option subheading 'Are you planning to connect a cable between this Router and your home or business Internet connection?' - list buttons '2:internetSharing,|Yes >,|Configure your internet gateway,| ,|' - list buttons '3:splashPage,|No >,|Skip this step,| ,|' - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'internetSharing' - option pageTitle 'Sharing your Internet Connection' - option topheader 'Sharing your Internet Connection' - option subheading 'Internet access will be available to those that connect to the mesh.
You may choose to advertise it to the mesh, or keep it hidden.' - list buttons '2:splashPage,|Please advertise my Internet access. >,|,| ,|' - list buttons "3:gatewayShareOff,|Please DON'T advertise my Internet access. >,|Note that access will still be available, just not advertised,| ,|" - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'splashPage' - option pageTitle 'Setup a Splash Page' - option topheader 'Setup a Splash Page' - option subheading 'Would you like people connecting to your node to first see a splash page on their web browser?
This is the only way to ask people connecting to the node to agree to a Community Standards agreement or Terms of Service.' - list modules 'splashPage' - list buttons '2:meshApplications,|Submit Splash Text>,|Use the above for your nodes splash page,| ,|' - list buttons '3:noSplash,|No >,|Skip this step,| ,|' - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'meshApplications' - option pageTitle 'Setup Mesh Application Advertising' - option topheader 'Setup Mesh Application Advertising' - option subheading 'Would you like to advertise mesh applications to people connected to your node?
You will be alerted when new applications are available and need approval.' - list modules 'applications' - list buttons '2:netSec,|Yes >,|I would like to create an application portal,| ,|' - list buttons "3:noApplications,|No >,|I don't want to let my node advertise applications,| ,|" - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'keyfilesAndSecurity' - option pageTitle 'Network Security Settings' - option topheader 'Network Security Settings' - option subheading 'This network configuration requires a key and/or password.
Remember, other nodes will have to use this key and/or password to connect to this mesh network.' - option fileType 'key' - list modules 'networkSecurity' - list modules 'upload' - list buttons '3:naming,|Continue >,|,| ,|' - list buttons '2:checkKeyFile,|Download Key File,|,| ,|' - list buttons '4:continueInsecure,|No >,|Continue with a insecure network,| ,|' - list buttons '5:startOver,|<< Start Over,| ,|plus' - -config page 'naming' - option pageTitle 'Please Choose your Mesh Names' - option topheader 'Please Choose your Mesh Names' - list modules 'nodeNaming' - list buttons '3:yourNetwork,|Continue >,|Set this name for your node and continue,| ,|' - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'makeItNaming' - option pageTitle 'Please Choose your Mesh Names' - option topheader 'Please Choose your Mesh Names' - list modules 'nodeNaming' - list modules 'accessPoint' - list buttons '3:yourNetwork,|Continue >,|Set this name for your node and continue,| ,|' - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'yourNetwork' - option pageTitle 'Your Network' - option topheader 'Your Network' - list modules 'yourNetwork' - list buttons '3:setupComplete,|Continue >,|Apply these settings to your node,| ,|' - list buttons '4:startOver,|<< Start Over,| ,|plus' - -config page 'setupComplete' - option pageTitle 'Setup is complete!' - option topheader 'Setup is complete!' - option subheading 'You have completed the Commotion quickstart. Check below to see if your node is connected to any other nodes in the area.' - list module 'complete' - list buttons '3:finish,|Finish >,|Go to node admin login,| ,|' - list buttons '4:startOver,|<< Start Over,| If there is a problem, Start Over,|plus' +#TODO +#create "name" module that creates a AP and a hostname from one name value (hostname adds a number) +# make sure "complete" module is always processed after all other modules diff --git a/usr/lib/lua/luci/controller/QS/QS.lua b/usr/lib/lua/luci/controller/QS/QS.lua index 05f5013..3f10878 100644 --- a/usr/lib/lua/luci/controller/QS/QS.lua +++ b/usr/lib/lua/luci/controller/QS/QS.lua @@ -89,12 +89,12 @@ end function pages(command, next, skip) --manipulates the rendered pages for a user - log("pages command: " .. command) + --log("pages command: " .. command) local uci = luci.model.uci.cursor() local page = uci:get('quickstart', 'options', 'pageNo') local lastPg = uci:get('quickstart', 'options', 'lastPg') - log(page) - log("last="..lastPg) + --log(page) + --log("last="..lastPg) if next == 'back' then uci:set('quickstart', 'options', 'pageNo', lastPg) uci:set('quickstart', 'options', 'lastPg', 'welcome') @@ -123,7 +123,6 @@ function wirelessController(profiles) function(s) table.insert(dev, s['.name']) end) - --Create interfaces for devNum,device in ipairs(dev) do --Make sure wireless devices are on... because it starts them disabled for some reason @@ -157,7 +156,9 @@ end function checkPage() local returns = luci.http.formvalue() + --log(returns) errors = parseSubmit(returns) + --log(errors) return errors end @@ -202,7 +203,8 @@ function parseSubmit(returns) pages('next', button) end if next(errors) ~= nil then - log("errors HERE") + --log("errors HERE") + --log(errors) pages('next','back') return(errors) end @@ -213,12 +215,20 @@ function runParser(modules) errors = {} local returns = luci.http.formvalue() --log(returns) + --log(modules) if modules then for _,value in ipairs(modules) do for i,x in pairs(luci.controller.QS.modules) do if i == (value .. "Parser") then --log(value) - errors[value]= luci.controller.QS.modules[value .. "Parser"](returns) + errors[value] = luci.controller.QS.modules[value .. "Parser"](returns) + if next(errors) then + for i,x in ipairs(modules) do + if x == 'complete' then + modules[i] = nil + end + end + end end end end @@ -226,7 +236,8 @@ function runParser(modules) --log(errors) return(errors) end - + + function keyCheck() local uci = luci.model.uci.cursor() --check if a key is required in a config file and compare the current key to it. diff --git a/usr/lib/lua/luci/controller/QS/buttons.lua b/usr/lib/lua/luci/controller/QS/buttons.lua index 276b3e8..9b3ce65 100644 --- a/usr/lib/lua/luci/controller/QS/buttons.lua +++ b/usr/lib/lua/luci/controller/QS/buttons.lua @@ -6,7 +6,7 @@ end function networkSecuritySettings(modules) local QS = luci.controller.QS.QS local error = luci.controller.QS.QS.keyCheck() - QS:log(error) + --QS:log(error) return(modules) end @@ -14,6 +14,16 @@ function gatewayShare(modules) return modules end +function keepGoing(modules) + for i,x in ipairs(modules) do + if x == 'complete' then + rem = i + end + end + table.remove(modules, rem) + +end + function back() return {} end @@ -105,7 +115,7 @@ function continueInsecure(modules) end function noConfigUploaded(modules) - luci.controller.QS.QS.log(modules) + --luci.controller.QS.QS.log(modules) for i,x in ipairs(modules) do if x == 'upload' then rem = i @@ -117,7 +127,7 @@ function noConfigUploaded(modules) end function noSplash(modules) - luci.controller.QS.QS.log(modules) + --luci.controller.QS.QS.log(modules) for i,x in ipairs(modules) do if x == 'splashPage' then rem = i @@ -164,12 +174,13 @@ function checkKeyFile(modules) end function finish(modules) - environment = luci.http.getenv("SERVER_NAME") - if not environment then - environment = "thisnode" + --luci.controller.QS.QS.log(modules) + mod = {} + for i,x in pairs(modules) do + table.insert(mod, x) end - luci.template.render("QS/module/applyreboot", {redirect_location=("http://" .. environment .. "/cgi-bin/luci/admin")}) - luci.http.close() - return({'complete'}) + table.insert(mod, 'complete') + --luci.controller.QS.QS.log(mod) + return mod end diff --git a/usr/lib/lua/luci/controller/QS/modules.lua b/usr/lib/lua/luci/controller/QS/modules.lua index fadbc61..21e8d11 100644 --- a/usr/lib/lua/luci/controller/QS/modules.lua +++ b/usr/lib/lua/luci/controller/QS/modules.lua @@ -1,10 +1,13 @@ module("luci.controller.QS.modules", package.seeall) --to have a html page render you must return a value or it wont. +require "commotion_helpers" + function index() --This function is required for LuCI --we don't need to define any pages in this file end + function welcomeRenderer() return 'true' end @@ -17,22 +20,227 @@ function completeRenderer() return 'true' end +function nameRenderer() + luci.sys.call("echo '' > /etc/commotion/profiles.d/quickstartSettings") + return 'true' +end + +function nameParser() + local QS = luci.controller.QS.QS + QS.log("nameParser running") + errors = nil + local val = luci.http.formvalue() + --QS.log(val) + if val.nodeName and val.nodeName ~= "" and string.len(val.nodeName) < 20 then + if is_hostname(val.nodeName) then + nodeID = luci.sys.exec("commotion nodeid") + --luci.controller.QS.QS.log(val.nodeName) + hostName = tostring(val.nodeName) .. nodeID + --QS.log(name) + file = io.open("/etc/commotion/profiles.d/quickstartSettings", "a") + file:write("hostname="..hostName.."\n") + --QS.log("wrote hostname") + if val.secure == 'true' then + --QS.log("passwords:"..val.pwd1.." & "..val.pwd2) + pass = checkPass(val.pwd1, val.pwd2) + if pass == nil then + if not luci.fs.isfile("/etc/commotion/profiles.d/quickstartSec") then + luci.sys.call('cp /etc/commotion/profiles.d/defaultSec /etc/commotion/profiles.d/quickstartSec') + end + file:write("pwd="..val.pwd1.."\n") + file:write("SSIDSec="..val.nodeName.."\n") + else + return pass + end + else + if not luci.fs.isfile("/etc/commotion/profiles.d/quickstartAP") then + luci.sys.call('cp /etc/commotion/profiles.d/defaultAP /etc/commotion/profiles.d/quickstartAP') + end + file:write("SSID="..val.nodeName.."\n") + end + file:close() + else + errors = "Please enter a correctly formatted name." + end + else + errors = "Please enter a name that is greater than 0 and less than 20 chars." + end + if errors ~= nil then + return errors + end +end + +function setAPPassword(pass) + local QS = luci.controller.QS.QS + QS.log("setAPPassword started") + + local file = "/etc/commotion/profiles.d/quickstartSec" + local find = '^wpakey=.*' + local replacement = "wpakey="..pass + replaceLine(file, find, replacement) + + --local file = "/etc/commotion/profiles.d/quickstartSec" + --local find = '^wpa=.*' + --local replacement = "wpa=true" + --replaceLine(file, find, replacement) +end + +function setSecAccessPoint(SSID) + local QS = luci.controller.QS.QS + QS.log("setSecAccessPoint started") + + local file = "/etc/commotion/profiles.d/quickstartSec" + local find = "^ssid=.*" + local replacement = 'ssid='..SSID + replaceLine(file, find, replacement) +end + + +function string.split(str, pat) + local t = {} + if pat == nil then pat=' ' end + local fpat = "(.-)" .. pat + local last_end = 1 + local s, e, cap = str:find(fpat, 1) + while s do + if s ~= 1 or cap ~= "" then + table.insert(t,cap) + end + last_end = e+1 + s, e, cap = str:find(fpat, last_end) end + if last_end <= #str then + cap = str:sub(last_end) + table.insert(t, cap) + end + return t +end + + +function setHostName(hostNamen) + local QS = luci.controller.QS.QS + QS.log("setHostName started") + local uci = luci.model.uci.cursor() + uci:foreach("system", "system", + function(s) + if s.hostname then + uci:set("system", s['.name'], "hostname", hostNamen) + uci:commit("system") + uci:save("system") + end + end) + hostnameWorks = luci.sys.call("echo " .. hostNamen .. " > /proc/sys/kernel/hostname") + QS.log("HostName was set correcty:"..tostring(hostnameWorks)) + QS.log("hostname set") +end + +function setAccessPoint(SSID) + local QS = luci.controller.QS.QS + QS.log("setAccessPoint started") + local file = "/etc/commotion/profiles.d/quickstartAP" + local find = "^ssid=.*" + local replacement = 'ssid='..SSID + replaceLine(file, find, replacement) + QS.log("Access Point Set") +end + +function loadingPage() + local QS = luci.controller.QS.QS + QS.log("loadingPage started") + + environment = luci.http.getenv("SERVER_NAME") + if not environment then + environment = "thisnode" + end + luci.template.render("QS/module/applyreboot", {redirect_location=("http://" .. environment .. "/cgi-bin/luci/admin")}) + luci.http.close() +end + +function setValues(setting, value) + --[=[ This function activates the setting value setting functions for defined values. + --]=] + --TODO how do we deal with functions that take multiple values? Lua allows passing of muiltiple values, may just need to make more ways to submit. + local QS = luci.controller.QS.QS + QS.log("setValue started") + settings = { + SSID = setAccessPoint, + hostname = setHostName, + pwd = setAPPassword, + SSIDSec = setSecAccessPoint, + } + settings[setting](value) + return +end + +function checkSettings() + --[=[ Checks the quickstart settings file and returns a table with setting, value pairs.--]=] + local QS = luci.controller.QS.QS + QS.log("checkSetttings started") + for line in io.lines("/etc/commotion/profiles.d/quickstartSettings") do + setting = line:split("=") + if setting[1] ~= "" and setting[1] ~= nil then + setValues(setting[1], setting[2]) + end + end + QS.log("quickstartSettings Completed") + return true +end + function completeParser() + --[=[ This function controls the final settings process--]=] local QS = luci.controller.QS.QS + QS.log("completeParser started") local uci = luci.model.uci.cursor() + loadingPage() + --This may be where we split the finction into smaller components + checkSettings() files = {{"mesh","quickstartMesh"}, {"secAp","quickstartSec"}, {"ap","quickstartAP"}} + QS.log("Wireless UCI Controller about to start") QS.wirelessController(files) - luci.controller.QS.QS.log("Quickstart restarting network") - --set quickstart to done so that it no longer allows access to these tools without admin password - luci.sys.call("/etc/init.d/commotiond restart") - luci.sys.call("sleep 2; /etc/init.d/network restart") + QS.log("Quickstart Final Countdown started") uci:set('quickstart', 'options', 'complete', 'true') uci:save('quickstart') uci:commit('quickstart') - luci.sys.call("sleep 20 && servald stop && servald start &") + p = luci.sys.reboot() end + + +function readProfile(name, obj) + --[=[ This function takes a profile name and the value desired and returns the result of that value. + --]=] + if type(name) == "string" then + offset = string.len(tostring(obj)) + for line in io.lines("/etc/commotion/profiles.d/"..name) do + b,c = string.find(line,"^"..obj.."=.*") + if b then + value = string.sub(line,b+offset,c) + end + end + return value + else + return nil + end +end + + +function replaceLine(fn, find, replacement) + --[=[ Function for replacing values in non-uci config files + replaceLine(File Name, search string, replacement text) + --]=] + errorCode = 1 + grepable = luci.sys.call('grep -q '..find..' '..fn) + if grepable == 1 then + errorCode = luci.sys.call("echo " .. replacement .. " >> ".. fn) + else + errorCode = luci.sys.call('sed -i s/'..find..'/'..replacement..'/g '..fn) + end + return errorCode +end + + + function adminPasswordParser(val) + --[=[ --]=] errors = {} local p1 = val.adminPassword_pwd1 local p2 = val.adminPassword_pwd2 @@ -149,6 +357,26 @@ function secAccessPointParser() end end +function checkPass(p1, p2) + --[=[ This function takes two values and compares them. It returns error text for password pages. It needs some serious refactoring, but it works --]=] + QS = luci.controller.QS.QS + if p1 and p2 then + if p1 == p2 then + if p1 == '' then + return "Please enter a password" + elseif string.len(p1) < 8 then + return "Please enter a password that is more than 8 chars long" + elseif not tostring(p1):match("^[%p%w]+$") then + return "Your password has spaces in it. You can't have spaces." + + end + else + return "Given password confirmation did not match, password not changed!" + end + end +end + + function splashPageRenderer() return 'true' end @@ -357,7 +585,7 @@ function networkSecurityRenderer() if d then --I bet I could find an even more difficult set of variables to differentiate than b and d, but ill leave it at this :) servald = string.sub(line,d+8,e) - luci.controller.QS.QS.log('servald = '..servald) + --luci.controller.QS.QS.log('servald = '..servald) end end if servald=='true' then @@ -395,6 +623,20 @@ function networkSecurityParser() end end +function replaceLine(fn, find, replacement) + --[=[ Function for replacing values in non-uci config files + replaceLine(File Name, search string, replacement text) + --]=] + errorCode = 1 + grepable = luci.sys.call('grep -q '..find..' '..fn) + if grepable == 1 then + errorCode = luci.sys.call("echo " .. replacement .. " >> ".. fn) + else + errorCode = luci.sys.call('sed -i s/'..find..'/'..replacement..'/g '..fn) + end + return errorCode +end + function uploadRenderer() --creates an uploader based upon the fileType of the page config @@ -439,17 +681,3 @@ function uploadParser() return error end end - -function replaceLine(fn, find, replacement) - --Function for replacing values in non-uci config files - --replaceLine(File Name, search string, replacement text) - errorCode = 1 - grepable = luci.sys.call('grep -q '..find..' '..fn) - if grepable == 1 then - errorCode = luci.sys.call("echo " .. replacement .. " >> ".. fn) - else - errorCode = luci.sys.call('sed -i s/'..find..'/'..replacement..'/g '..fn) - end - return errorCode -end - diff --git a/usr/lib/lua/luci/view/QS/module/adminPassword.htm b/usr/lib/lua/luci/view/QS/module/adminPassword.htm index 60e662a..d5bc099 100644 --- a/usr/lib/lua/luci/view/QS/module/adminPassword.htm +++ b/usr/lib/lua/luci/view/QS/module/adminPassword.htm @@ -1,18 +1,21 @@ <%- - if pv.errorMsg then + if pv.errorMsg and pv.errorMsg.adminPassword then if pv.errorMsg.adminPassword.pw then pwErr = pv.errorMsg.adminPassword.pw end end -%> <%- if luci.sys.user.getuser("root") and not luci.sys.user.getpasswd("root") then -%>
- -

Admin Password

+ +
+ +

This password is used to access the administration menus on this device.

<%=pwErr%>

-

Choose a password that is between 9 and 30 characters. This name can be numbers, letters, and special characters.

Retype your password here

+
<%- end -%> + diff --git a/usr/lib/lua/luci/view/QS/module/applyreboot.htm b/usr/lib/lua/luci/view/QS/module/applyreboot.htm index 78b606c..31f9e00 100644 --- a/usr/lib/lua/luci/view/QS/module/applyreboot.htm +++ b/usr/lib/lua/luci/view/QS/module/applyreboot.htm @@ -1,6 +1,34 @@ <%=luci.sys.hostname()%> - <% if title then %><%=title%><% else %><%:Redirecting...%><% end %> +<%- + if luci.fs.isfile("/etc/commotion/profiles.d/quickstartSettings") then + for line in io.lines("/etc/commotion/profiles.d/quickstartSettings") do + b,c = string.find(line,"^SSID=.*") + d,e = string.find(line, "^SSIDSec=.*") + f,g = string.find(line, "^hostname=.*") + if b then + apName = string.sub(line,b+5,c) + elseif d then + apName = string.sub(line,d+8,e) + end + if f then + hostname = string.sub(line,f+9,g) + end + end + end + if not luci.fs.isfile("/etc/commotion/profiles.d/quickstartMesh") then + luci.sys.call('cp /etc/commotion/profiles.d/defaultMesh /etc/commotion/profiles.d/quickstartMesh') + end + for line in io.lines("/etc/commotion/profiles.d/quickstartMesh") do + b,c = string.find(line,"^ssid=.*") + if b then + netName = string.sub(line,b+5,c) + end + end + local meshName = netName + local nodeName = hostname +-%> @@ -29,10 +57,25 @@

<%:System%> - <% if title then %><%=title%><%

<%:Loading%> + <%:Waiting for changes to be applied...%> +

+
+

The name of your mesh network is:

+

<%=meshName%>

+ +

Your node name is:

+

<%=nodeName%>

+ +

When people connect to your node wirelessly, they will connect using this name:

+

<%=apName%>

+ +

After configuring your node, you can change the node name on the node administration page.

+
+

- +
diff --git a/usr/lib/lua/luci/view/QS/module/complete.htm b/usr/lib/lua/luci/view/QS/module/complete.htm new file mode 100644 index 0000000..6db5aa7 --- /dev/null +++ b/usr/lib/lua/luci/view/QS/module/complete.htm @@ -0,0 +1 @@ + diff --git a/usr/lib/lua/luci/view/QS/module/name.htm b/usr/lib/lua/luci/view/QS/module/name.htm new file mode 100644 index 0000000..2aa9dc9 --- /dev/null +++ b/usr/lib/lua/luci/view/QS/module/name.htm @@ -0,0 +1,58 @@ +<%- + if pv.errorMsg then + if pv.errorMsg.name then + nameErr = pv.errorMsg.name + end + end + nodeID = luci.sys.exec("commotion nodeid") +-%> + + + + + +

<%=nameErr%>

+
+
+ +
+ +

This value is used by other users and devices to find your network wirelessly and over the mesh. This value can be composed of letter, number and hyphens. (Hyphens can not be used at the beggining or end of the name.)

+ + Require a password for others to connect to your node. +
+ +
+
diff --git a/usr/lib/lua/luci/view/QS/module/upload.htm b/usr/lib/lua/luci/view/QS/module/upload.htm index 6d3e5db..6c7038e 100644 --- a/usr/lib/lua/luci/view/QS/module/upload.htm +++ b/usr/lib/lua/luci/view/QS/module/upload.htm @@ -15,10 +15,12 @@
<%=upErr%>
-

Upload <%=uploadTitle%>

- -
- Upload <%=pv.modules.upload.fileInstructions%> +
+ + +
+ Upload <%=pv.modules.upload.fileInstructions%> +
diff --git a/www/luci-static/commotion/QS_reset_button.png b/www/luci-static/commotion/QS_reset_button.png new file mode 100644 index 0000000..04ad473 Binary files /dev/null and b/www/luci-static/commotion/QS_reset_button.png differ diff --git a/www/luci-static/commotion/QS_reset_button_hover.png b/www/luci-static/commotion/QS_reset_button_hover.png new file mode 100644 index 0000000..b394b03 Binary files /dev/null and b/www/luci-static/commotion/QS_reset_button_hover.png differ