mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-06 20:14:10 +08:00
🐶 Sync 2026-10-03 11:32:07
This commit is contained in:
1 parent
a5bb91e6ed
commit
66e3d835c4
72 files changed
+5107
-1799
No files matched your search
+63
-8
@@ -52,15 +52,38 @@ function render(ctx) {
|
||||
so.default = 'remote';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.ListValue, 'format', _('Format'));
|
||||
so = ss.option(form.ListValue, 'format', _('Format'),
|
||||
_('Rule-set file format. Leave it empty and HomeProxy reads it from the file itself; a value that does not match the file is corrected and reported in the log.'));
|
||||
so.value('binary', _('Binary file'));
|
||||
so.value('source', _('Source file'));
|
||||
so.default = 'binary';
|
||||
so.rmempty = false;
|
||||
/* rmempty, and no default.
|
||||
*
|
||||
* A default of 'binary' plus rmempty=false meant EVERY local rule-set
|
||||
* carried an explicit format - and an explicit value beats sing-box's
|
||||
* extension inference, so "add a local rule-set, pick my example.json,
|
||||
* forget to change Format" produced a configuration that parsed JSON as
|
||||
* a compiled .srs and was rejected at apply time.
|
||||
*
|
||||
* Leaving the field empty is now the recommended state rather than an
|
||||
* accident: the generator reads the first bytes of the file and declares
|
||||
* the format itself (generator/ruleset.uc's resolveFormat), correcting a
|
||||
* wrong value and saying so in the log. Writing a default here would
|
||||
* put a value back on every rule-set for that code to undo. */
|
||||
so.rmempty = true;
|
||||
|
||||
so = ss.option(form.Value, 'path', _('Path'));
|
||||
so = ss.option(form.Value, 'path', _('Path'),
|
||||
_('Rule-set file in %s. Copy it there first: the generator refuses a rule-set whose file is missing.').format('/etc/homeproxy-pro/ruleset/'));
|
||||
so.datatype = 'file';
|
||||
so.placeholder = '/etc/homeproxy-pro/ruleset/example.json';
|
||||
/* The archive is the only directory the backend admits (RULE_PATH_ROOTS),
|
||||
* and it is created for the user - at install time and again before every
|
||||
* generation. Offering it as the datalist entry is what turns "point at a
|
||||
* file under a directory that may not exist" into a one-click default.
|
||||
* The same field used to carry a placeholder naming example.json while
|
||||
* the UI defaulted the format to binary, so the file it suggested was one
|
||||
* the generated configuration would then refuse to parse. */
|
||||
so.value(hp.rule_path_default);
|
||||
so.placeholder = hp.rule_path_default;
|
||||
so.validate = hp.validateRuleSetPath;
|
||||
so.rmempty = false;
|
||||
so.depends('type', 'local');
|
||||
so.modalonly = true;
|
||||
@@ -108,8 +131,11 @@ function render(ctx) {
|
||||
so.depends('type', 'remote');
|
||||
|
||||
so = ss.option(form.Value, 'initial_path', _('Initial path'),
|
||||
_('Local file with initial rule-set content; avoids blocking startup on first download (1.14).'));
|
||||
_('Local file with initial rule-set content; avoids blocking startup on first download (1.14). Must be under %s.').format('/etc/homeproxy-pro/ruleset/'));
|
||||
so.datatype = 'file';
|
||||
so.value(hp.rule_path_default);
|
||||
so.placeholder = hp.rule_path_default;
|
||||
so.validate = hp.validateRuleSetPath;
|
||||
so.depends('type', 'remote');
|
||||
so.modalonly = true;
|
||||
|
||||
@@ -130,8 +156,37 @@ function render(ctx) {
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Value, 'update_interval', _('Update interval'),
|
||||
_('Update interval of rule set.'));
|
||||
so.placeholder = '1d';
|
||||
_('Update interval of rule set, e.g. 24h or 3600 (seconds).'));
|
||||
/* 24h, not 1d.
|
||||
*
|
||||
* sing-box parses update_interval as a Go duration, and Go's units are
|
||||
* ns/us/ms/s/m/h - there is no day. The placeholder used to read "1d",
|
||||
* which is the one value a user is most likely to copy verbatim into a
|
||||
* field that then rejects the whole configuration. 24h is the same
|
||||
* interval, is a unit Go reads, and is exactly what this generator emits
|
||||
* for its own built-in rule-sets (generator/route.uc).
|
||||
*
|
||||
* A bare number is accepted because the generator normalises it: the
|
||||
* value is passed through strToTime(), which appends "s" to anything
|
||||
* that does not already end in a unit - that is the fix for the measured
|
||||
* `time: missing unit in duration "3600"` failure. */
|
||||
so.placeholder = '24h';
|
||||
so.validate = function(section_id, value) {
|
||||
/* Empty means "no update_interval", which sing-box reads as its own
|
||||
* default - the field is optional, so it must not be made required. */
|
||||
if (!section_id || !value)
|
||||
return true;
|
||||
|
||||
/* What Go's time.ParseDuration accepts: a sequence of
|
||||
* number+unit groups (so 1h30m is fine), or a bare number, which
|
||||
* strToTime() will turn into seconds. Everything else - "1d", "1w",
|
||||
* "daily", "24 h" - is refused here, on the page, instead of becoming
|
||||
* a rejected configuration the user only learns about from a log. */
|
||||
if (/^\d+$/.test(value) || /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/.test(value))
|
||||
return true;
|
||||
|
||||
return _('Expecting: %s').format(_('update interval like 24h, 1h30m or 3600'));
|
||||
};
|
||||
so.depends('type', 'remote');
|
||||
/* Rule set settings end */
|
||||
}
|
||||
|
||||
@@ -75,6 +75,77 @@ const ensureLogPoll = hp.statusPoller({
|
||||
}
|
||||
});
|
||||
|
||||
/* --- rule-set download failures ------------------------------------------
|
||||
*
|
||||
* With ruleset_safe_start on, a remote rule-set that cannot be downloaded no
|
||||
* longer stops the service: sing-box reads the empty initial_path fallback,
|
||||
* binds, and retries in the background. The instance is healthy and the LAN is
|
||||
* proxied - and the rule-set that keeps mainland traffic direct matches
|
||||
* nothing, so all of it goes to `final`.
|
||||
*
|
||||
* That is the trade, and it is the reason this block exists: the degradation
|
||||
* has no other symptom. The health gate does not look at rule-sets at all
|
||||
* (there is nothing in health.sh that could), so the service reports itself
|
||||
* healthy while a rule-set is silently empty.
|
||||
*
|
||||
* sing-box-c.log is read rather than a new RPC: the ACL already grants the
|
||||
* browser read on that file for the log viewer below, so this costs no
|
||||
* permission and no new method to keep in step with the method table.
|
||||
*
|
||||
* Whether a fallback is actually in play is read from UCI rather than guessed:
|
||||
* with the opt-in OFF, a failing rule-set means the instance would not have
|
||||
* started at all, which is a different and much more visible problem - so the
|
||||
* wording has to differ, not just the colour. */
|
||||
const ruleSetNode = E('div', { 'id': 'ruleset_status' },
|
||||
E('img', {
|
||||
'src': L.resource('icons/loading.svg'),
|
||||
'alt': _('Loading'),
|
||||
'style': 'vertical-align:middle'
|
||||
}, _('Collecting data...'))
|
||||
);
|
||||
|
||||
function renderRuleSetStatus() {
|
||||
/* Same poller as the log views, so there is still one handler per view
|
||||
* no matter how often render() runs. */
|
||||
ensureLogPoll();
|
||||
|
||||
fs.read_direct(`${hp_dir}/sing-box-c.log`, 'text')
|
||||
.then((text) => {
|
||||
const failures = hp.parseRuleSetFetchFailures(text);
|
||||
const safeStart = (uci.get('homeproxy-pro', 'config', 'ruleset_safe_start') || '0') === '1';
|
||||
|
||||
if (!failures.length) {
|
||||
dom.content(ruleSetNode, E('span', { 'style': 'color:green' },
|
||||
[_('No rule-set download failures in the current log.')]));
|
||||
return;
|
||||
}
|
||||
|
||||
dom.content(ruleSetNode, E('div', [
|
||||
E('p', { 'style': 'color:red' }, [
|
||||
_('%d rule-set(s) cannot be downloaded.').format(failures.length)
|
||||
]),
|
||||
E('p', { 'style': 'color:gray' }, [
|
||||
safeStart
|
||||
? _('Startup fallback in use: these rule-sets are EMPTY, so the traffic they would have split falls through to the default route. The service is up, but the split is not in effect until the download succeeds.')
|
||||
: _('Startup fallback is OFF, so a rule-set that cannot be downloaded prevents the service from starting at all. The service is running, so the log below is from an earlier attempt or the download has since recovered.')
|
||||
]),
|
||||
E('ul', {}, failures.map((f) => E('li', [
|
||||
E('code', [ f.tag ]),
|
||||
' — ',
|
||||
/* Text nodes, never markup: the reason is a string
|
||||
* sing-box wrote, and it contains a URL. */
|
||||
f.reason || _('(no reason recorded)'),
|
||||
f.at ? E('small', { 'style': 'color:gray' }, [ ' — ' + f.at ]) : ''
|
||||
])))
|
||||
]));
|
||||
})
|
||||
.catch((err) => {
|
||||
dom.content(ruleSetNode, E('span', { 'style': 'color:gray' }, [
|
||||
_('Cannot read the sing-box client log (%s).').format(String(err))
|
||||
]));
|
||||
});
|
||||
}
|
||||
|
||||
function getConnStat(o, site) {
|
||||
o.default = E('div', { 'style': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
@@ -301,6 +372,20 @@ return view.extend({
|
||||
return node;
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy-pro', _('Rule sets'));
|
||||
s.anonymous = true;
|
||||
|
||||
/* A dedicated section rather than a fourth log view: the question is
|
||||
* "is any rule-set currently broken", and the answer has to be a
|
||||
* verdict plus a per-tag reason, not a wall of log the user has to read
|
||||
* a timestamp out of. */
|
||||
o = s.option(form.DummyValue, '_ruleset_status');
|
||||
o.rawhtml = true;
|
||||
o.render = function() {
|
||||
renderRuleSetStatus();
|
||||
return ruleSetNode;
|
||||
};
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy-pro');
|
||||
s.anonymous = true;
|
||||
|
||||
|
||||
Reference in new issue
Block a user