Luci development

Hi Everybody,

I've developed a script (forum topic) that allows me to restrict internet access across multiple devices for a given user (my kids). I wish to build a UI to make configuration easier, but I've only been able to find very basic (at least it's not given me the information I was looking for) information on developing for Luci, e.g. Luci Github. I wish to develop the following:

  • Log summary page that reads the logs from the SQLite log table and refreshes automagically
  • Configuration page that has a tab per table in the DB that allows me to edit individual values and add/remove rows
  • Summary page for a user detailing how many minutes of internet access they have left for that day

I have been able to cobble together something using Lua and basic HTML. However, I wish to make these pages to have the same look/feel and functionality of the rest of the Luci interface. I was therefore wondering if anyone was able to give me any pointers? Things I am looking for assistance on are:

  • Lazy loading - most of the pages load (with the header/footer etc) and are then populated after the initial load (i.e. you see a loading spinner)
  • Auto refresh of pages
  • Page styling
  • Forms - is there a specific way these should be handled?
  • Are there any reusable widgets/modules available?

Thanks in advance!

Tim

The WebUI moved to js a long time ago, lua-based pages are still supported, but if you're developing something new, a good idea would be to use js.

From your description it sounds like you could benefit from perusing the source for luci-app-nlbwmon.

3 Likes

Thanks for the reply. I've finally got round to trying this out, but I am struggling if I am being honest. I have looked into nlbwmon and created/placed files into directories where nlbwmon has files created:

  • bwl.js -> /www/luci-static/resources/view/bwl
  • luci-app-bwl.json -> /usr/share/luci/menu.d
  • luci-app-bwl.json -> /usr/share/rpcd/acl.d

I have not been able to get a new menu item to appear in the UI however and I am unable to find out how nlbwmon achieves this - doing a find for *nlbw* results in nothing interesting.

Another thing that is a complete mystery to me ATM is how can I interact with SQLite - looking at nlbw, I can see there is a database in /var/lib/nlbwmon, but I am unable to fathom how it's accessed.

TIA

Tim

Tim, you're a smart man, you must understand nobody can help you unless you post the files which are not working. :wink:

I'd suggest paste sites or links to github/gitlab.

Fair point! However, my assumption was that I had missed something really obvious (like you need to create an extra file or something). My code is as follows...

/www/luci-static/resources/view/bwl/bwl.js

'use strict';
'require view';
'require form';
'require uci';
'require fs';
'require validation';
'require tools.widgets as widgets';

return view.extend({
    load: function() { return uci.load('bwl'); },
    render: function() {
        var m, s, o;
        m = new form.Map('bwl', _('Bandwidth Limiter'), _('Test'));
        s = m.section(form.TypedSection, 'nlbwmon');
        s.anonymous = true;
        s.addremove = false;
        s.tab('user', _('Users'), _('Test'));
        s.tab('device', _('Devices'));
        s.tab('schedule', _('Schedules'));
        s.tab('scheduleActiveTime', _('Active Times'));
        s.tab('configuration', _('Configuration'));
        s.tab('log', _('Usage Log'));
        return m.render();
    }
});

/usr/share/luci/menu.d/luci-app-bwl.json

{
    "admin/bwl": {
        "title": "Bandwidth Limiter",
        "order": 90,
        "action": {
            "type": "firstchild"
        },
        "depends": {
            "acl": ["luci-app-bwl"],
            "uci": { "bwl": true }
        }
    },
    "admin/bwl/bwl": {
        "title": "Display",
        "order": 1,
        "action": {
            "type": "view",
            "path": "bwl/bwl"
        }
    }
}

/usr/share/rpcd/acl.d/luci-app-bwl.json (This is pretty much a straight copy from nlbw with everything nlbw related replaced with bwl - not really sure what's going on here if I'm honest)

{
    "luci-app-bwl": {
        "description": "Grant UCI access for luci-app-bwl",
        "read": {
            "cgi-io": ["exec"],
            "file": {
                "/proc/sys/kernel/hostname": ["read"],
                "/usr/libexec/bwl-action backup": ["exec"],
                "/usr/libexec/bwl-action download *": ["exec"],
                "/usr/libexec/bwl-action periods": ["exec"],
                "/usr/share/bwl/protocols": ["read"]
            },
            "ubus": {
                "file": ["read"],
                "luci-rpc": ["getHostHints"],
                "network.rrdns": ["lookup"]
            },
            "uci": ["bwl"]
        },
        "write": {
            "file": {
                "/tmp/nlbw-restore.tar.gz": ["write"],
                "/usr/libexec/bwl-action commit": ["exec"],
                "/usr/libexec/bwl-action restore": ["exec"],
                "/usr/share/bwl/protocols": ["write"]
            },
            "ubus": {
                "file": ["write"]
            },
            "uci": ["bwl"]
        }
    }
}

Did you do:

/etc/init.d/rpcd reload
rm -rf /var/luci-modulecache/; rm -f /var/luci-indexcache;

?

Thanks.

I hadn't tried that, no. I have now tried it, and still no joy.

EDIT: Reboot also doesn't work.

I'd try:

{
    "admin/bwl/bwl": {
        "title": "Display",
        "order": 1,
        "action": {
            "type": "view",
            "path": "bwl/bwl"
        },
        "depends": {
            "acl": ["luci-app-bwl"]
        }
    }
}

If that doesn't work, I'd try switching admin/bwl/bwl to admin/services/bwl. Other than that, I'm out of ideas, if that doesn't work we can tag devs with more js experience.

Thanks for your input so far @stangri - very much appreciated.

I have tried the suggestions you made and neither of them appear to work (I also ran the shell commands you mentioned after each edit) :frowning:

HOWEVER: PROGRESS! I have managed to get a menu item to appear in the Services menu - hurrah! The reason is due to the fact my editor had defaulted CRLF rather than just LF - schoolboy! I then revisited my previous efforts, but none of those worked and even your initial suggestion in the previous post does not yield a new main menu item.

I am now getting this error:

Any idea?

Do you have all those resources (and config files) listed in the ACL json?

To be honest, I have no idea what I am doing with this file. I have stripped it right back:

{
    "luci-app-bwl": {
        "description": "Grant UCI access for luci-app-bwl",
        "read": {
            "uci": ["bwl"]
        }
    }
}

I have no idea if the above is correct however. What is the purpose of this file? What should it contain?

So do you have the /etc/config/bwl file?

Nope! What's that then? Am I right in assuming that it can be a shell script that defines functions and acts as a form of API? Is there any documentation for the ACL JSON?

Assume that, apart from the files/content that I have already mentioned in this thread, no other required code exists!

It's a config file for your app. Check out other files in /etc/config to see what they are. I don't think there's a doc for ACL JSON, but it's not that hard to figure out based on ACL JSON files in the repo.

Resource not found means the file you were trying to use was not present on the file system. Make sure the path is correct and that the file actually exists

i was going to ask if your picture was a variant of rose sauce and fettuccine, but i googled your name and gathered it's exactly that.

either way, it looks Good.

1 Like

Hi All,

Ok, so I seem to be getting somewhere... I now have a basic JS page that is able to retrieve data from the SQLite DB and present it in some HTML forms:

'use strict';
'require view';
'require form';
'require uci';
'require fs';
'require validation';
'require tools.widgets as widgets';
'require poll';

return view.extend({
    //load: function() { return uci.load('bwl'); },
    load: function() { return Promise.all(L.resolveDefault(fs.exec_direct('/etc/init.d/bwl' ['config', 'downloadLimit']), '')), uci.load('bwl'); },
    render: function() {
        function setVar(varName) {
            L.resolveDefault(fs.exec_direct('/etc/init.d/bwl', ['get_config_var', varName])).then(function(res) {
                var log = document.getElementById(varName);
                if (res) { log.value = res; } else { log.value = _('No adblock related logs yet!'); }
            });
        }

        setVar('downloadLimit');
        setVar('uploadLimit');
        setVar('interfaces');
        setVar('sleep');
        setVar('maxBandwidth');
        setVar('tcCommand');
        setVar('resetTime');
        setVar('chainName');

        return E('div', { class: 'cbi-map' },
            E('div', { class: 'cbi-section' }, [
                E('div', { class: 'cbi-section-descr' }, _('The syslog output, pre-filtered for adblock related messages only.')),
                E('input', { 'id': 'downloadLimit', 'style': 'padding: 5px; font-family: monospace' }),
                E('input', { 'id': 'uploadLimit', 'style': 'padding: 5px; font-family: monospace' }),
                E('input', { 'id': 'sleep', 'style': 'padding: 5px; font-family: monospace' }),
                E('input', { 'id': 'maxBandwidth', 'style': 'padding: 5px; font-family: monospace' }),
                E('input', { 'id': 'tcCommand', 'style': 'padding: 5px; font-family: monospace' }),
                E('input', { 'id': 'resetTime', 'style': 'padding: 5px; font-family: monospace' }),
                E('input', { 'id': 'interfaces', 'style': 'padding: 5px; font-family: monospace' })
            ])
        );
    }
});

My /usr/share/rpcd/acl.d/luci-app-bwl.json file now looks like this:

{
    "luci-app-bwl": {
        "description": "Grant UCI access for luci-app-bwl",
        "read": {
            "file": {
                "/etc/init.d/bwl get_config_var [a-zA-Z]*": ["exec"]
            },
            "uci": ["bwl"]
        }
    }
}

I am not 100% happy with the way have managed to render the data however - is there a better way that I am not aware of? Is there a UCI way of querying a DB, or is it as I suspect, and only able to work with files?

Thanks!

Tim

Hello Brain Trust!

So I am making some progress and decided to try and implement a rudimentary update functionality to update the database. I think I have all the required files/config in place (going by what's in the repo), but I am getting a 403 status when the update is attempted:

403 Access to command denied by ACL

I have the following in /www/luci-static/resources/view/bwl/bwl.js:

'use strict';
'require view';
'require form';
'require uci';
'require fs';
'require validation';
'require tools.widgets as widgets';
'require poll';

var timer

function handleAction(ev) {
    clearTimeout(timer)
    timer = setTimeout(function() {
        var varName = ev.target.getAttribute('id')
        var varValue = ev.target.value
        L.resolveDefault(fs.exec_direct('/etc/init.d/bwl', ['upd_config_var', varName, varValue]))
    }, 3000)
}
return view.extend({
    //load: function() { return Promise.all(L.resolveDefault(fs.exec_direct('/etc/init.d/bwl' ['config', 'downloadLimit']), '')), uci.load('bwl'); },
    render: function() {
        var varNames = ['downloadLimit', 'uploadLimit', 'interfaces', 'sleep', 'maxBandwidth', 'tcCommand', 'resetTime', 'chainName']
        var container = E('div', { class: 'cbi-section' }, [
            E('div', { class: 'cbi-section-descr' }, _('The syslog output, pre-filtered for adblock related messages only.'), [])
        ])

        function renderVar(varName, index) {
            var cfgItem = E('div', { class: 'cbi-value' }, [
                E('label', { class: 'cbi-value-title' }, _(varName)),
                E('input', { 'id': varName, 'class': 'cbi-input-text', 'input': handleAction })
            ])
            container.appendChild(cfgItem)
        }

        function setVar(varName, index) {
            var log = document.getElementById(varName);

            L.resolveDefault(fs.exec_direct('/etc/init.d/bwl', ['get_config_var', varName])).then(function(res) {
                var log = document.getElementById(varName);
                if (res) { log.value = res; } else { log.value = _('No adblock related logs yet!'); }
            });
        }

        varNames.forEach(setVar)
        varNames.forEach(renderVar)

        var root = E('div', { class: 'cbi-map' },
            container
        );

        return root
    },
    handleSaveApply: null,
    handleSave: null,
    handleReset: null
});

The following in /usr/share/rpcd/acl.d/luci-app-bwl.json:

{
    "luci-app-bwl": {
        "description": "Grant UCI access for luci-app-bwl",
        "write": {
            "file": {
                "/etc/init.d/bwl upd_config_var [a-zA-Z]* +": ["exec"]
            }
        },
        "read": {
            "file": {
                "/etc/init.d/bwl get_config_var [a-zA-Z]*": ["exec"]
            },
            "uci": ["bwl"]
        }
    }
}

Finally, the following in /etc/init.d/bwl:

if [ -n "$(type -t extra_command)" ]
then
        extra_command "get_config_var" "Suspend adblock processing"
        extra_command "upd_config_var" "Suspend adblock processing"
fi

SCRIPT_HOME='/root'
DB_FILE="${SCRIPT_HOME}/bandwidth_limit.db"
SQLITE='/usr/bin/sqlite3'

get_config_var()
{
        local var_name=$1
        local result=`${SQLITE} ${DB_FILE} "select configValue from config where configName='${var_name}';"`
        echo ${result}
}

upd_config_var()
{
        local var_name=$1
        local var_value=$2
        local result=`${SQLITE} ${DB_FILE} "update config set configValue = '${var_value}' where configName='${var_name}';"`
        echo ${result}
}

I have tested upd_config_var from the command line and it works without issue, so it's not that. What have I missed?!

Cheers

Tim