Merge pull request #1819 from netalertx/next_release

Next release
This commit is contained in:
Jokob @NetAlertX authored and GitHub committed 2026-09-29 14:40:48 +10:00
commit 04c07182f1
55 files changed
+1771 -119

No files matched your search

+6 -5
View File
@@ -28,15 +28,16 @@ Both are plausible, well-written, and wrong. Reading the code first catches both
6. **Force every open question to an explicit decision**, even if the decision is "accept as-is for v1, revisit if feedback says otherwise." An open question left unresolved in a PRD gets silently decided by whoever implements it — usually differently than anyone intended.
7. **Write the test plan as part of the PRD, not after.** Concrete test cases — naming real functions/queries, not "add tests for X" — force you to notice design gaps you'd otherwise miss; the moment you try to write "assert Y happens" and realize the current design can't produce Y is often the first time the gap becomes visible. Check the repo for an existing test pattern for this shape of change before inventing a new one (e.g. a prior presence-logic bug fixed via `test/db_test_helpers.py` fixtures is the template for the next one, not a reason to build new test infrastructure).
8. **Ask explicitly whether validating this needs real end-to-end infrastructure** (a new or modified plugin, a UI click-through) or whether synthetic unit-level fixtures suffice — don't assume either way. Check whether the functions under test take a DB connection/dict/list as a parameter (testable in isolation, no real plugin needed) or require a real file on disk (harder to fake, may need one).
9. **Check performance against the real schema and real scale, not assumptions.** For every new or changed query: does it use an existing index, or add an unindexed lookup, a new join, or a correlated subquery? Grep `CREATE INDEX` for *every* index on the tables involved, not just the first one you find — a column can have both a plain index and a separate expression index (e.g. `idx_eve_mac_date_type ON Events(eveMac, ...)` alongside `idx_eve_lower_mac_date_type ON Events(LOWER(eveMac), ...)`), and missing the second one produces a wrong verdict. Then weigh cost by how often the query runs (once is nothing; every few minutes forever is a standing cost) and by real scale — **production users run 10,000+ devices**, not a homelab handful. A `CurrentScan` with 2-5 rows per device (one per contributing plugin) is routinely 20,000-50,000+ rows in one cycle; reason about that number, not a smaller hopeful one. A correlated `EXISTS`/subquery re-evaluated per outer row is fine *if* the correlated column is indexed — e.g. `current_scan_presence_condition()` (`server/scan/presence.py`) is exactly this shape against `CurrentScan.scanMac`, covered by `idx_currentscan_scanmac`. The real risk is an unindexed correlated lookup: an accidental self-join scanning the full inner table per outer row, which looks fine and passes tests at small scale but isn't at production scale. Check with `EXPLAIN QUERY PLAN` at a realistic row count, built against the *complete* real index set (copy every `CREATE INDEX` for the table, or run it against an actual `app.db`) rather than a hand-picked subset — a partial index set produces a misleading plan in either direction, not just "looks worse than it is." If it comes back unindexed for real, a `GROUP BY` aggregate is the usual fix.
10. **Do a dedicated final-check pass, out loud, before calling it done.** Re-read the whole document end to end and specifically check:
9. **Ask explicitly whether this feature/mechanism should be exposed via the API, and give a recommendation, not just flag it as an open question.** This codebase already has three real API surfaces to consider extending rather than inventing a fourth: REST (`server/api_server/*_endpoint.py`), GraphQL (`graphql_endpoint.py`), and a Prometheus `/metrics` endpoint (`prometheus_endpoint.py`). Skipping this question doesn't mean "no API access needed"; it means the answer gets silently decided later by whoever first wants to query the new data externally, usually as its own separate feature request re-litigating a design this PRD already had full context to settle. Not everything needs exposure: purely internal/diagnostic state with no plausible external consumer doesn't, but say so explicitly, with the reason, rather than leaving it unaddressed.
10. **Check performance against the real schema and real scale, not assumptions.** For every new or changed query: does it use an existing index, or add an unindexed lookup, a new join, or a correlated subquery? Grep `CREATE INDEX` for *every* index on the tables involved, not just the first one you find — a column can have both a plain index and a separate expression index (e.g. `idx_eve_mac_date_type ON Events(eveMac, ...)` alongside `idx_eve_lower_mac_date_type ON Events(LOWER(eveMac), ...)`), and missing the second one produces a wrong verdict. Then weigh cost by how often the query runs (once is nothing; every few minutes forever is a standing cost) and by real scale — **production users run 10,000+ devices**, not a homelab handful. A `CurrentScan` with 2-5 rows per device (one per contributing plugin) is routinely 20,000-50,000+ rows in one cycle; reason about that number, not a smaller hopeful one. A correlated `EXISTS`/subquery re-evaluated per outer row is fine *if* the correlated column is indexed — e.g. `current_scan_presence_condition()` (`server/scan/presence.py`) is exactly this shape against `CurrentScan.scanMac`, covered by `idx_currentscan_scanmac`. The real risk is an unindexed correlated lookup: an accidental self-join scanning the full inner table per outer row, which looks fine and passes tests at small scale but isn't at production scale. Check with `EXPLAIN QUERY PLAN` at a realistic row count, built against the *complete* real index set (copy every `CREATE INDEX` for the table, or run it against an actual `app.db`) rather than a hand-picked subset — a partial index set produces a misleading plan in either direction, not just "looks worse than it is." If it comes back unindexed for real, a `GROUP BY` aggregate is the usual fix.
11. **Do a dedicated final-check pass, out loud, before calling it done.** Re-read the whole document end to end and specifically check:
- Did a correction made mid-document propagate everywhere it needed to (the Design subsection *and* Affected Files *and* Tests *and* any execution-plan summary)? A correction landing in one place and not its siblings is worse than never catching it, because now the document silently contradicts itself.
- Does every "this is the cleanest/simplest real case" claim still hold up if you actually re-read that specific piece of code right now, or was it asserted by pattern-matching a name/category? Re-verify, don't re-assert.
- Does anything render incorrectly as markdown — an unfenced ASCII diagram or code block will collapse into one line under lazy-paragraph-continuation, the same class of bug as a list missing its preceding blank line.
- Do any internal anchor links' slugs match their headings?
- Does the design still cleanly separate its axes, or did a later addition quietly re-conflate two concerns inside what's supposed to be a single-purpose mechanism (the same mistake step 3 exists to catch at the top level can reappear one level down inside an individual mechanism's own value set — e.g. a 3-value enum where two of the values are secretly independent booleans in a trenchcoat).
11. **Leave a visible trail of corrections instead of silently rewriting.** When a review pass — yours or someone else's — finds something wrong, write "**Correction (caught in review):** ..." inline rather than quietly fixing the earlier text and moving on. This is what makes a PRD trustworthy to a second reader: they can see what was checked and what changed, not just receive a polished final answer with no visible seams.
12. **When marking a PRD Implemented, ask once whether anything found along the way generalizes beyond this one document** — a tooling trick, a wrong assumption that took real digging to disprove, a review catch that would recur on the next PRD. The PRD's own "Implementation notes" section is the right place for what happened *in this PRD*; a skill or memory update is the right place for anything that would otherwise have to be rediscovered next time. One question, not a mandatory new section — skip it when nothing generalizes, don't force it on every PRD regardless of size.
12. **Leave a visible trail of corrections instead of silently rewriting.** When a review pass — yours or someone else's — finds something wrong, write "**Correction (caught in review):** ..." inline rather than quietly fixing the earlier text and moving on. This is what makes a PRD trustworthy to a second reader: they can see what was checked and what changed, not just receive a polished final answer with no visible seams.
13. **When marking a PRD Implemented, ask once whether anything found along the way generalizes beyond this one document** — a tooling trick, a wrong assumption that took real digging to disprove, a review catch that would recur on the next PRD. The PRD's own "Implementation notes" section is the right place for what happened *in this PRD*; a skill or memory update is the right place for anything that would otherwise have to be rediscovered next time. One question, not a mandatory new section — skip it when nothing generalizes, don't force it on every PRD regardless of size.
## Structure to follow
@@ -46,7 +47,7 @@ Both are plausible, well-written, and wrong. Reading the code first catches both
- **Open issues** — each with an explicit recorded decision (step 6), not left dangling.
- **Affected files** — concrete `file:function:line` references, not bare filenames.
- **Backward compatibility** — explicit default values and why they preserve current behavior for existing consumers.
- **Performance impact** — the baseline (what's unindexed/slow *today*, independent of this change), what the change adds that's negligible, what's new and worth mitigating, and concrete mitigations rather than a vague "should be fine" (step 9).
- **Performance impact** — the baseline (what's unindexed/slow *today*, independent of this change), what the change adds that's negligible, what's new and worth mitigating, and concrete mitigations rather than a vague "should be fine" (step 10).
- **Docs/skills to update** — anywhere this needs to be reflected outside the code itself (external docs, paired skill files, template files new authors copy from).
- **Tests** — organized by mechanism, each case naming the real function/query it exercises and the concrete assertion (step 7), plus a manual verification checklist for anything that can't be unit-tested (including an `EXPLAIN QUERY PLAN` check at realistic scale if the Performance impact section found a genuine risk).
- **(Optional) Execution plan** — phased, referencing the same file/function names used above rather than restating the design in vaguer terms.
+6 -5
View File
@@ -28,15 +28,16 @@ Both are plausible, well-written, and wrong. Reading the code first catches both
6. **Force every open question to an explicit decision**, even if the decision is "accept as-is for v1, revisit if feedback says otherwise." An open question left unresolved in a PRD gets silently decided by whoever implements it — usually differently than anyone intended.
7. **Write the test plan as part of the PRD, not after.** Concrete test cases — naming real functions/queries, not "add tests for X" — force you to notice design gaps you'd otherwise miss; the moment you try to write "assert Y happens" and realize the current design can't produce Y is often the first time the gap becomes visible. Check the repo for an existing test pattern for this shape of change before inventing a new one (e.g. a prior presence-logic bug fixed via `test/db_test_helpers.py` fixtures is the template for the next one, not a reason to build new test infrastructure).
8. **Ask explicitly whether validating this needs real end-to-end infrastructure** (a new or modified plugin, a UI click-through) or whether synthetic unit-level fixtures suffice — don't assume either way. Check whether the functions under test take a DB connection/dict/list as a parameter (testable in isolation, no real plugin needed) or require a real file on disk (harder to fake, may need one).
9. **Check performance against the real schema and real scale, not assumptions.** For every new or changed query: does it use an existing index, or add an unindexed lookup, a new join, or a correlated subquery? Grep `CREATE INDEX` for *every* index on the tables involved, not just the first one you find — a column can have both a plain index and a separate expression index (e.g. `idx_eve_mac_date_type ON Events(eveMac, ...)` alongside `idx_eve_lower_mac_date_type ON Events(LOWER(eveMac), ...)`), and missing the second one produces a wrong verdict. Then weigh cost by how often the query runs (once is nothing; every few minutes forever is a standing cost) and by real scale — **production users run 10,000+ devices**, not a homelab handful. A `CurrentScan` with 2-5 rows per device (one per contributing plugin) is routinely 20,000-50,000+ rows in one cycle; reason about that number, not a smaller hopeful one. A correlated `EXISTS`/subquery re-evaluated per outer row is fine *if* the correlated column is indexed — e.g. `current_scan_presence_condition()` (`server/scan/presence.py`) is exactly this shape against `CurrentScan.scanMac`, covered by `idx_currentscan_scanmac`. The real risk is an unindexed correlated lookup: an accidental self-join scanning the full inner table per outer row, which looks fine and passes tests at small scale but isn't at production scale. Check with `EXPLAIN QUERY PLAN` at a realistic row count, built against the *complete* real index set (copy every `CREATE INDEX` for the table, or run it against an actual `app.db`) rather than a hand-picked subset — a partial index set produces a misleading plan in either direction, not just "looks worse than it is." If it comes back unindexed for real, a `GROUP BY` aggregate is the usual fix.
10. **Do a dedicated final-check pass, out loud, before calling it done.** Re-read the whole document end to end and specifically check:
9. **Ask explicitly whether this feature/mechanism should be exposed via the API, and give a recommendation, not just flag it as an open question.** This codebase already has three real API surfaces to consider extending rather than inventing a fourth: REST (`server/api_server/*_endpoint.py`), GraphQL (`graphql_endpoint.py`), and a Prometheus `/metrics` endpoint (`prometheus_endpoint.py`). Skipping this question doesn't mean "no API access needed"; it means the answer gets silently decided later by whoever first wants to query the new data externally, usually as its own separate feature request re-litigating a design this PRD already had full context to settle. Not everything needs exposure: purely internal/diagnostic state with no plausible external consumer doesn't, but say so explicitly, with the reason, rather than leaving it unaddressed.
10. **Check performance against the real schema and real scale, not assumptions.** For every new or changed query: does it use an existing index, or add an unindexed lookup, a new join, or a correlated subquery? Grep `CREATE INDEX` for *every* index on the tables involved, not just the first one you find — a column can have both a plain index and a separate expression index (e.g. `idx_eve_mac_date_type ON Events(eveMac, ...)` alongside `idx_eve_lower_mac_date_type ON Events(LOWER(eveMac), ...)`), and missing the second one produces a wrong verdict. Then weigh cost by how often the query runs (once is nothing; every few minutes forever is a standing cost) and by real scale — **production users run 10,000+ devices**, not a homelab handful. A `CurrentScan` with 2-5 rows per device (one per contributing plugin) is routinely 20,000-50,000+ rows in one cycle; reason about that number, not a smaller hopeful one. A correlated `EXISTS`/subquery re-evaluated per outer row is fine *if* the correlated column is indexed — e.g. `current_scan_presence_condition()` (`server/scan/presence.py`) is exactly this shape against `CurrentScan.scanMac`, covered by `idx_currentscan_scanmac`. The real risk is an unindexed correlated lookup: an accidental self-join scanning the full inner table per outer row, which looks fine and passes tests at small scale but isn't at production scale. Check with `EXPLAIN QUERY PLAN` at a realistic row count, built against the *complete* real index set (copy every `CREATE INDEX` for the table, or run it against an actual `app.db`) rather than a hand-picked subset — a partial index set produces a misleading plan in either direction, not just "looks worse than it is." If it comes back unindexed for real, a `GROUP BY` aggregate is the usual fix.
11. **Do a dedicated final-check pass, out loud, before calling it done.** Re-read the whole document end to end and specifically check:
- Did a correction made mid-document propagate everywhere it needed to (the Design subsection *and* Affected Files *and* Tests *and* any execution-plan summary)? A correction landing in one place and not its siblings is worse than never catching it, because now the document silently contradicts itself.
- Does every "this is the cleanest/simplest real case" claim still hold up if you actually re-read that specific piece of code right now, or was it asserted by pattern-matching a name/category? Re-verify, don't re-assert.
- Does anything render incorrectly as markdown — an unfenced ASCII diagram or code block will collapse into one line under lazy-paragraph-continuation, the same class of bug as a list missing its preceding blank line.
- Do any internal anchor links' slugs match their headings?
- Does the design still cleanly separate its axes, or did a later addition quietly re-conflate two concerns inside what's supposed to be a single-purpose mechanism (the same mistake step 3 exists to catch at the top level can reappear one level down inside an individual mechanism's own value set — e.g. a 3-value enum where two of the values are secretly independent booleans in a trenchcoat).
11. **Leave a visible trail of corrections instead of silently rewriting.** When a review pass — yours or someone else's — finds something wrong, write "**Correction (caught in review):** ..." inline rather than quietly fixing the earlier text and moving on. This is what makes a PRD trustworthy to a second reader: they can see what was checked and what changed, not just receive a polished final answer with no visible seams.
12. **When marking a PRD Implemented, ask once whether anything found along the way generalizes beyond this one document** — a tooling trick, a wrong assumption that took real digging to disprove, a review catch that would recur on the next PRD. The PRD's own "Implementation notes" section is the right place for what happened *in this PRD*; a skill or memory update is the right place for anything that would otherwise have to be rediscovered next time. One question, not a mandatory new section — skip it when nothing generalizes, don't force it on every PRD regardless of size.
12. **Leave a visible trail of corrections instead of silently rewriting.** When a review pass — yours or someone else's — finds something wrong, write "**Correction (caught in review):** ..." inline rather than quietly fixing the earlier text and moving on. This is what makes a PRD trustworthy to a second reader: they can see what was checked and what changed, not just receive a polished final answer with no visible seams.
13. **When marking a PRD Implemented, ask once whether anything found along the way generalizes beyond this one document** — a tooling trick, a wrong assumption that took real digging to disprove, a review catch that would recur on the next PRD. The PRD's own "Implementation notes" section is the right place for what happened *in this PRD*; a skill or memory update is the right place for anything that would otherwise have to be rediscovered next time. One question, not a mandatory new section — skip it when nothing generalizes, don't force it on every PRD regardless of size.
## Structure to follow
@@ -46,7 +47,7 @@ Both are plausible, well-written, and wrong. Reading the code first catches both
- **Open issues** — each with an explicit recorded decision (step 6), not left dangling.
- **Affected files** — concrete `file:function:line` references, not bare filenames.
- **Backward compatibility** — explicit default values and why they preserve current behavior for existing consumers.
- **Performance impact** — the baseline (what's unindexed/slow *today*, independent of this change), what the change adds that's negligible, what's new and worth mitigating, and concrete mitigations rather than a vague "should be fine" (step 9).
- **Performance impact** — the baseline (what's unindexed/slow *today*, independent of this change), what the change adds that's negligible, what's new and worth mitigating, and concrete mitigations rather than a vague "should be fine" (step 10).
- **Docs/skills to update** — anywhere this needs to be reflected outside the code itself (external docs, paired skill files, template files new authors copy from).
- **Tests** — organized by mechanism, each case naming the real function/query it exercises and the concrete assertion (step 7), plus a manual verification checklist for anything that can't be unit-tested (including an `EXPLAIN QUERY PLAN` check at realistic scale if the Performance impact section found a genuine risk).
- **(Optional) Execution plan** — phased, referencing the same file/function names used above rather than restating the design in vaguer terms.
+6 -5
View File
@@ -28,15 +28,16 @@ Both are plausible, well-written, and wrong. Reading the code first catches both
6. **Force every open question to an explicit decision**, even if the decision is "accept as-is for v1, revisit if feedback says otherwise." An open question left unresolved in a PRD gets silently decided by whoever implements it — usually differently than anyone intended.
7. **Write the test plan as part of the PRD, not after.** Concrete test cases — naming real functions/queries, not "add tests for X" — force you to notice design gaps you'd otherwise miss; the moment you try to write "assert Y happens" and realize the current design can't produce Y is often the first time the gap becomes visible. Check the repo for an existing test pattern for this shape of change before inventing a new one (e.g. a prior presence-logic bug fixed via `test/db_test_helpers.py` fixtures is the template for the next one, not a reason to build new test infrastructure).
8. **Ask explicitly whether validating this needs real end-to-end infrastructure** (a new or modified plugin, a UI click-through) or whether synthetic unit-level fixtures suffice — don't assume either way. Check whether the functions under test take a DB connection/dict/list as a parameter (testable in isolation, no real plugin needed) or require a real file on disk (harder to fake, may need one).
9. **Check performance against the real schema and real scale, not assumptions.** For every new or changed query: does it use an existing index, or add an unindexed lookup, a new join, or a correlated subquery? Grep `CREATE INDEX` for *every* index on the tables involved, not just the first one you find — a column can have both a plain index and a separate expression index (e.g. `idx_eve_mac_date_type ON Events(eveMac, ...)` alongside `idx_eve_lower_mac_date_type ON Events(LOWER(eveMac), ...)`), and missing the second one produces a wrong verdict. Then weigh cost by how often the query runs (once is nothing; every few minutes forever is a standing cost) and by real scale — **production users run 10,000+ devices**, not a homelab handful. A `CurrentScan` with 2-5 rows per device (one per contributing plugin) is routinely 20,000-50,000+ rows in one cycle; reason about that number, not a smaller hopeful one. A correlated `EXISTS`/subquery re-evaluated per outer row is fine *if* the correlated column is indexed — e.g. `current_scan_presence_condition()` (`server/scan/presence.py`) is exactly this shape against `CurrentScan.scanMac`, covered by `idx_currentscan_scanmac`. The real risk is an unindexed correlated lookup: an accidental self-join scanning the full inner table per outer row, which looks fine and passes tests at small scale but isn't at production scale. Check with `EXPLAIN QUERY PLAN` at a realistic row count, built against the *complete* real index set (copy every `CREATE INDEX` for the table, or run it against an actual `app.db`) rather than a hand-picked subset — a partial index set produces a misleading plan in either direction, not just "looks worse than it is." If it comes back unindexed for real, a `GROUP BY` aggregate is the usual fix.
10. **Do a dedicated final-check pass, out loud, before calling it done.** Re-read the whole document end to end and specifically check:
9. **Ask explicitly whether this feature/mechanism should be exposed via the API, and give a recommendation, not just flag it as an open question.** This codebase already has three real API surfaces to consider extending rather than inventing a fourth: REST (`server/api_server/*_endpoint.py`), GraphQL (`graphql_endpoint.py`), and a Prometheus `/metrics` endpoint (`prometheus_endpoint.py`). Skipping this question doesn't mean "no API access needed"; it means the answer gets silently decided later by whoever first wants to query the new data externally, usually as its own separate feature request re-litigating a design this PRD already had full context to settle. Not everything needs exposure: purely internal/diagnostic state with no plausible external consumer doesn't, but say so explicitly, with the reason, rather than leaving it unaddressed.
10. **Check performance against the real schema and real scale, not assumptions.** For every new or changed query: does it use an existing index, or add an unindexed lookup, a new join, or a correlated subquery? Grep `CREATE INDEX` for *every* index on the tables involved, not just the first one you find — a column can have both a plain index and a separate expression index (e.g. `idx_eve_mac_date_type ON Events(eveMac, ...)` alongside `idx_eve_lower_mac_date_type ON Events(LOWER(eveMac), ...)`), and missing the second one produces a wrong verdict. Then weigh cost by how often the query runs (once is nothing; every few minutes forever is a standing cost) and by real scale — **production users run 10,000+ devices**, not a homelab handful. A `CurrentScan` with 2-5 rows per device (one per contributing plugin) is routinely 20,000-50,000+ rows in one cycle; reason about that number, not a smaller hopeful one. A correlated `EXISTS`/subquery re-evaluated per outer row is fine *if* the correlated column is indexed — e.g. `current_scan_presence_condition()` (`server/scan/presence.py`) is exactly this shape against `CurrentScan.scanMac`, covered by `idx_currentscan_scanmac`. The real risk is an unindexed correlated lookup: an accidental self-join scanning the full inner table per outer row, which looks fine and passes tests at small scale but isn't at production scale. Check with `EXPLAIN QUERY PLAN` at a realistic row count, built against the *complete* real index set (copy every `CREATE INDEX` for the table, or run it against an actual `app.db`) rather than a hand-picked subset — a partial index set produces a misleading plan in either direction, not just "looks worse than it is." If it comes back unindexed for real, a `GROUP BY` aggregate is the usual fix.
11. **Do a dedicated final-check pass, out loud, before calling it done.** Re-read the whole document end to end and specifically check:
- Did a correction made mid-document propagate everywhere it needed to (the Design subsection *and* Affected Files *and* Tests *and* any execution-plan summary)? A correction landing in one place and not its siblings is worse than never catching it, because now the document silently contradicts itself.
- Does every "this is the cleanest/simplest real case" claim still hold up if you actually re-read that specific piece of code right now, or was it asserted by pattern-matching a name/category? Re-verify, don't re-assert.
- Does anything render incorrectly as markdown — an unfenced ASCII diagram or code block will collapse into one line under lazy-paragraph-continuation, the same class of bug as a list missing its preceding blank line.
- Do any internal anchor links' slugs match their headings?
- Does the design still cleanly separate its axes, or did a later addition quietly re-conflate two concerns inside what's supposed to be a single-purpose mechanism (the same mistake step 3 exists to catch at the top level can reappear one level down inside an individual mechanism's own value set — e.g. a 3-value enum where two of the values are secretly independent booleans in a trenchcoat).
11. **Leave a visible trail of corrections instead of silently rewriting.** When a review pass — yours or someone else's — finds something wrong, write "**Correction (caught in review):** ..." inline rather than quietly fixing the earlier text and moving on. This is what makes a PRD trustworthy to a second reader: they can see what was checked and what changed, not just receive a polished final answer with no visible seams.
12. **When marking a PRD Implemented, ask once whether anything found along the way generalizes beyond this one document** — a tooling trick, a wrong assumption that took real digging to disprove, a review catch that would recur on the next PRD. The PRD's own "Implementation notes" section is the right place for what happened *in this PRD*; a skill or memory update is the right place for anything that would otherwise have to be rediscovered next time. One question, not a mandatory new section — skip it when nothing generalizes, don't force it on every PRD regardless of size.
12. **Leave a visible trail of corrections instead of silently rewriting.** When a review pass — yours or someone else's — finds something wrong, write "**Correction (caught in review):** ..." inline rather than quietly fixing the earlier text and moving on. This is what makes a PRD trustworthy to a second reader: they can see what was checked and what changed, not just receive a polished final answer with no visible seams.
13. **When marking a PRD Implemented, ask once whether anything found along the way generalizes beyond this one document** — a tooling trick, a wrong assumption that took real digging to disprove, a review catch that would recur on the next PRD. The PRD's own "Implementation notes" section is the right place for what happened *in this PRD*; a skill or memory update is the right place for anything that would otherwise have to be rediscovered next time. One question, not a mandatory new section — skip it when nothing generalizes, don't force it on every PRD regardless of size.
## Structure to follow
@@ -46,7 +47,7 @@ Both are plausible, well-written, and wrong. Reading the code first catches both
- **Open issues** — each with an explicit recorded decision (step 6), not left dangling.
- **Affected files** — concrete `file:function:line` references, not bare filenames.
- **Backward compatibility** — explicit default values and why they preserve current behavior for existing consumers.
- **Performance impact** — the baseline (what's unindexed/slow *today*, independent of this change), what the change adds that's negligible, what's new and worth mitigating, and concrete mitigations rather than a vague "should be fine" (step 9).
- **Performance impact** — the baseline (what's unindexed/slow *today*, independent of this change), what the change adds that's negligible, what's new and worth mitigating, and concrete mitigations rather than a vague "should be fine" (step 10).
- **Docs/skills to update** — anywhere this needs to be reflected outside the code itself (external docs, paired skill files, template files new authors copy from).
- **Tests** — organized by mechanism, each case naming the real function/query it exercises and the concrete assertion (step 7), plus a manual verification checklist for anything that can't be unit-tested (including an `EXPLAIN QUERY PLAN` check at realistic scale if the Performance impact section found a genuine risk).
- **(Optional) Execution plan** — phased, referencing the same file/function names used above rather than restating the design in vaguer terms.
+21 -1
View File
@@ -16,7 +16,7 @@ The application performs regular maintenance and database cleanup. If these task
### Database and Log File Size
A large database or oversized log files can impact performance. You can check database and table sizes on the **Maintenance** page.
A large database or oversized log files can impact performance. You can check database and table sizes on the **System Info → Storage** page.
![DB size check](./img/PERFORMANCE/db_size_check.png)
@@ -32,6 +32,26 @@ Please note that excessively large log files will increase memory consumption. D
---
## Tracking NetAlertX's Own Resource Usage Over Time
The **System Info → Performance** page graphs NetAlertX's own process CPU%, memory, disk I/O, and scan duration over hour/day/week/month ranges, sampled once per scan cycle. This turns "did a recent change make things slower" into a question you can answer by looking at real history, instead of guessing from what a change *should* cost.
**What each chart shows:**
* **CPU %** - average CPU utilisation over the scan cycle (`100%` = one fully-utilized CPU core; can exceed `100%` with multi-threaded work). This is an average over the whole cycle, not an instantaneous reading - a short burst of high usage followed by idle time reads as a lower percentage than the peak.
* **Memory Usage** - resident memory (RSS - the physical RAM the process currently holds), in MB, sampled once per cycle.
* **IO Read / IO Write** - bytes read/written by the NetAlertX process itself during that cycle, in MB. This only ever reflects NetAlertX's own direct I/O (its own DB/cache writes) - a plugin's own subprocess doing heavy disk I/O (e.g. a scanner writing its own result file) is not captured; this is a structural limitation of how the OS attributes child-process I/O to the parent, not a gap that can be closed later.
* **Scan Duration** - wall-clock time the scan cycle took, in seconds.
> [!NOTE]
> The IO numbers come from the kernel's own per-process I/O accounting (`/proc/<pid>/io`), which the kernel itself documents as an approximation. On some Docker storage drivers this can be badly inaccurate - confirmed on the deprecated `aufs` driver (common on older Synology NAS Docker setups), where union-filesystem copy-up writes get mis-attributed and can report write volumes many times larger than what's actually reaching disk (`docker info | grep -i "storage driver"` shows which driver you're on; `aufs` is a known-bad case, alongside its own `blkio throttle` warnings confirming the kernel's block-I/O accounting isn't properly wired up for it). If the IO chart shows large, consistent write numbers that don't match actual database growth (check the DB/WAL size on **System Info → Storage**), that's this known driver limitation, not a real cost. Newer Docker storage drivers (`overlay2`) don't have this problem.
* Controlled by the `MAINT_PERF_DAYS` setting (part of the `MAINT` plugin) - the number of days of history to retain, `0` disables collection. Defaults to `30`.
* History cannot be backfilled - data starts accumulating from whenever collection is enabled.
* `GET /health` also exposes live `process_cpu_pct`/`process_rss_mb` fields for external monitoring, independent of this history.
---
## Maintenance Plugins
Two plugins help maintain the system’s performance:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 33 KiB

+1 -1
View File
@@ -2597,7 +2597,7 @@ textarea[readonly],
width: 100%;
}
#panInitCheck
#panInitCheck.active
{
display: flow-root;
}
+23 -7
View File
@@ -91,7 +91,17 @@ function deleteCookie (cookie) {
// -----------------------------------------------------------------------------
// DateTime utilities
// -----------------------------------------------------------------------------
function localizeTimestamp(input) {
/**
* Parses a timestamp in any of several common formats (unix seconds/ms,
* DD/MM/YYYY, MM/DD/YYYY, ISO, RFC2822, DD-MM-YYYY) and formats it in the
* user's configured TIMEZONE/UI_LOCALE settings.
* @param {string|number} input - The timestamp to localize.
* @param {Intl.DateTimeFormatOptions} [dtfOptions] - Full Intl.DateTimeFormat
* options object, replacing (not merging with) the default year/month/day/
* hour/minute/second/hour12 shape - e.g. for a shorter chart-axis label.
* @returns {string} The localized, formatted timestamp.
*/
function localizeTimestamp(input, dtfOptions) {
// Read fresh on every call (not a module-level const): getSetting() reads
// a localStorage cache that clearCache() wipes before reloading, so a
@@ -99,6 +109,16 @@ function localizeTimestamp(input) {
const tz = getSetting("TIMEZONE") || 'Europe/Berlin';
const LOCALE = getSetting('UI_LOCALE') || 'en-GB';
// Optional second arg: full Intl.DateTimeFormat options object, replacing
// (not merging with) the historical year+seconds default below - e.g. a
// chart axis label that wants month/day/hour/minute only. Omit it for the
// original full-precision behavior every existing caller relies on.
const options = dtfOptions || {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
};
input = String(input || '').trim();
// 1. Unix timestamps (10 or 13 digits)
@@ -106,9 +126,7 @@ function localizeTimestamp(input) {
const ms = input.length === 10 ? parseInt(input, 10) * 1000 : parseInt(input, 10);
return new Intl.DateTimeFormat('default', {
timeZone: tz,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
...options
}).format(new Date(ms));
}
@@ -198,9 +216,7 @@ function localizeTimestamp(input) {
return new Intl.DateTimeFormat(LOCALE, {
// Convert from UTC to user's configured timezone
timeZone: tz,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
...options
}).format(date);
}
}
+210
View File
@@ -0,0 +1,210 @@
// Chart.js instances, kept so a range switch destroys the old chart before
// building a new one on the same canvas (Chart.js throws otherwise).
var resourceHistoryChartInstances = {};
// Monotonically increasing id, bumped on every initResourceHistoryGraphs()
// call. A fast double-click across two range buttons fires two overlapping
// AJAX requests; without this, whichever response lands last wins regardless
// of click order, so a stale response can silently overwrite newer data.
var resourceHistoryRequestId = 0;
// Chart axis labels: time only - date/year are dropped entirely to keep the
// axis readable; the full date (including year) is still available on
// hover via each chart's tooltip title callback below. Passed as
// localizeTimestamp()'s options override so parsing/timezone/locale logic
// stays in one shared place (common.js).
var CHART_TIMESTAMP_OPTIONS_AXIS = {
hour: '2-digit', minute: '2-digit',
hour12: false
};
// Full timestamp (including year) shown in the tooltip title on hover.
var CHART_TIMESTAMP_OPTIONS_TOOLTIP = {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false
};
/**
* Fetches Resource_History data for the given range ('hour'/'day'/'week'/'month')
* and renders the four Performance-tab charts, or shows the disabled-state
* message when no rows exist yet (collection off, or just enabled).
*/
function initResourceHistoryGraphs(range) {
var requestId = ++resourceHistoryRequestId;
$.get('php/server/query_json.php', { file: `table_resource_history_${range}.json`, nocache: Date.now() }, function (res) {
if (requestId !== resourceHistoryRequestId) {
return; // a newer range request has since started - discard this stale response
}
var rows = (res && res.data) ? res.data : [];
if (rows.length === 0) {
$('#resourceHistoryCharts').addClass('myhidden');
$('#resourceHistoryDisabledMessage').removeClass('myhidden');
return;
}
$('#resourceHistoryDisabledMessage').addClass('myhidden');
$('#resourceHistoryCharts').removeClass('myhidden');
var labels = [];
var fullLabels = [];
var cpuData = [];
var rssData = [];
var ioReadData = [];
var ioWriteData = [];
var durationData = [];
var tickFailed = [];
function round2(n) {
return n == null ? null : Math.round(n * 100) / 100;
}
rows.forEach(function (entry) {
// hour/day rows carry resDateTime (raw); week/month rows carry bucket
// (hourly rollup) instead - see const.py's sql_resource_history_* queries.
var ts = entry.resDateTime || entry.bucket;
labels.push(localizeTimestamp(ts, CHART_TIMESTAMP_OPTIONS_AXIS));
fullLabels.push(localizeTimestamp(ts, CHART_TIMESTAMP_OPTIONS_TOOLTIP));
cpuData.push(round2(entry.resCpuPercent));
rssData.push(round2(entry.resRssMb));
// Bytes -> MB and ms -> s: raw units from the DB aren't a readable axis scale.
ioReadData.push(entry.resIoReadBytes != null ? round2(entry.resIoReadBytes / (1024 * 1024)) : null);
ioWriteData.push(entry.resIoWriteBytes != null ? round2(entry.resIoWriteBytes / (1024 * 1024)) : null);
durationData.push(entry.resScanDurationMs != null ? round2(entry.resScanDurationMs / 1000) : null);
tickFailed.push(entry.resTickFailed == 1);
});
renderResourceHistoryCharts(labels, fullLabels, cpuData, rssData, ioReadData, ioWriteData, durationData, tickFailed);
}).fail(function () {
console.error('Error fetching resource history data.');
});
}
/**
* Builds/replaces the four small-multiple Resource_History charts (CPU%,
* memory usage MB, IO read+write MB, scan duration seconds) on the same time
* axis. Rows written from the tick-failure `finally` path (resTickFailed = 1)
* are rendered as visually distinct points rather than plain data, since
* their numbers may reflect a truncated, crash-adjacent sample. `labels`
* (time-only, used for the axis) and `fullLabels` (full date+year, used for
* the hover tooltip title) are parallel arrays over the same rows.
*/
function renderResourceHistoryCharts(labels, fullLabels, cpuData, rssData, ioReadData, ioWriteData, durationData, tickFailed) {
var normalColor = "rgba(0, 166, 89, .8)";
var failedColor = "#dd4b39";
function pointColors(baseColor) {
return tickFailed.map(function (failed) { return failed ? failedColor : baseColor; });
}
function destroyIfExists(key) {
if (resourceHistoryChartInstances[key]) {
resourceHistoryChartInstances[key].destroy();
}
}
var commonScales = {
yAxes: [{
// maxTicksLimit caps how many labels Chart.js draws - without it, a
// short/fixed-height chart crams in enough ticks that adjacent labels
// visually overlap.
ticks: { beginAtZero: true, fontColor: '#A0A0A0', maxTicksLimit: 5 },
// Faint gridlines only at the (few, capped) main ticks - easier to
// read a value off the chart without making it visually noisy.
gridLines: { color: "rgba(160, 160, 160, 0.15)", zeroLineColor: "rgba(160, 160, 160, 0.3)" },
}],
xAxes: [{
ticks: { fontColor: '#A0A0A0', maxTicksLimit: 10, autoSkip: true },
gridLines: { color: "rgba(160, 160, 160, 0.15)", zeroLineColor: "rgba(160, 160, 160, 0.3)" },
}],
};
// Axis labels are time-only (CHART_TIMESTAMP_OPTIONS_AXIS); show the full
// date (including year) on hover instead, via fullLabels.
var commonTooltips = {
callbacks: {
title: function (tooltipItems) {
return fullLabels[tooltipItems[0].index];
},
},
};
destroyIfExists('cpu');
resourceHistoryChartInstances.cpu = new Chart("ResourceCpuChart", {
type: "line",
data: {
labels: labels,
datasets: [{
label: getString("Systeminfo_Performance_CPU"),
data: cpuData,
borderColor: normalColor,
backgroundColor: "rgba(0, 166, 89, .2)",
pointBackgroundColor: pointColors(normalColor),
fill: true,
}],
},
options: { legend: { display: true }, scales: commonScales, tooltips: commonTooltips, maintainAspectRatio: false, responsive: true },
});
destroyIfExists('rss');
resourceHistoryChartInstances.rss = new Chart("ResourceRssChart", {
type: "line",
data: {
labels: labels,
datasets: [{
label: getString("Systeminfo_Performance_Memory"),
data: rssData,
borderColor: "#3c8dbc",
backgroundColor: "rgba(60, 141, 188, .2)",
pointBackgroundColor: pointColors("#3c8dbc"),
fill: true,
}],
},
options: { legend: { display: true }, scales: commonScales, tooltips: commonTooltips, maintainAspectRatio: false, responsive: true },
});
destroyIfExists('io');
resourceHistoryChartInstances.io = new Chart("ResourceIoChart", {
type: "line",
data: {
labels: labels,
datasets: [
{
label: getString("Systeminfo_Performance_IO_Read"),
data: ioReadData,
borderColor: "#00a65a",
pointBackgroundColor: pointColors("#00a65a"),
fill: false,
},
{
label: getString("Systeminfo_Performance_IO_Write"),
data: ioWriteData,
borderColor: "#f39c12",
pointBackgroundColor: pointColors("#f39c12"),
fill: false,
},
],
},
options: { legend: { display: true }, scales: commonScales, tooltips: commonTooltips, maintainAspectRatio: false, responsive: true },
});
destroyIfExists('duration');
resourceHistoryChartInstances.duration = new Chart("ResourceDurationChart", {
type: "line",
data: {
labels: labels,
datasets: [{
label: getString("Systeminfo_Performance_Duration"),
data: durationData,
borderColor: "#b2b6be",
backgroundColor: "rgba(178, 182, 190, .2)",
pointBackgroundColor: pointColors("#b2b6be"),
fill: true,
}],
},
options: { legend: { display: true }, scales: commonScales, tooltips: commonTooltips, maintainAspectRatio: false, responsive: true },
});
}
-61
View File
@@ -12,43 +12,6 @@
showSpinner();
</script>
<?php
// Size and last mod of DB ------------------------------------------------------
$dbBasePath = rtrim(getenv('NETALERTX_DB') ?: '/data/db', '/');
$nax_db = $dbBasePath . '/app.db';
$nax_wal = $dbBasePath . '/app.db-wal';
$nax_db_size = file_exists($nax_db) ? number_format((filesize($nax_db) / 1000000),2,",",".") . ' MB' : '0 MB';
$nax_wal_size = file_exists($nax_wal) ? number_format((filesize($nax_wal) / 1000000),2,",",".") . ' MB' : '0 MB';
$nax_db_mod = file_exists($nax_db) ? date ("F d Y H:i:s", filemtime($nax_db)) : 'N/A';
// Table sizes -----------------------------------------------------------------
$tableSizesHTML = "";
// Open a connection to the SQLite database
$db = new SQLite3($nax_db);
// Retrieve the table names from sqlite_master
$query = "SELECT name FROM sqlite_master WHERE type='table'";
$result = $db->query($query);
// Iterate over the tables and get the row counts
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$tableName = $row['name'];
$query = "SELECT COUNT(*) FROM $tableName";
$countResult = $db->querySingle($query);
$tableSizesHTML = $tableSizesHTML . "$tableName (<b>$countResult</b>), ";
}
// Close the database connection
$db->close();
?>
<div class="row">
<div class="col-md-12">
<div class="box" id="Maintain-Status">
@@ -85,30 +48,6 @@ $db->close();
<span data-plc="version"></span>
</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell" style="min-width: 140px"><?= lang('Maintenance_database_path');?></div>
<div class="db_info_table_cell">
<?php echo $nax_db;?>
</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell"><?= lang('Maintenance_database_size');?></div>
<div class="db_info_table_cell">
<?php echo $nax_db_size;?> (wal: <?php echo $nax_wal_size;?>)
</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell"><?= lang('Maintenance_database_rows');?></div>
<div class="db_info_table_cell">
<?php echo $tableSizesHTML;?>
</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell"><?= lang('Maintenance_database_lastmod');?></div>
<div class="db_info_table_cell">
<?php echo $nax_db_mod;?>
</div>
</div>
</div>
</div>
<!-- /.box-body -->
+3
View File
@@ -474,6 +474,9 @@
<li>
<a href="systeminfo.php#panInitCheck" onclick="setCache('activeSysinfoTab','tabInitCheck');initializeTabs()"> <?= lang("Maintenance_InitCheck");?> </a>
</li>
<li>
<a href="systeminfo.php#panPerformance" onclick="setCache('activeSysinfoTab','tabPerformance');initializeTabs()"><?= lang('Systeminfo_Performance');?></a>
</li>
</ul>
</li>
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "نسخ",
"Gen_CopyToClipboard": "انسخ إلى الحافظة",
"Gen_DataUpdatedUITakesTime": "تم تحديث البيانات. قد يستغرق تحديث واجهة المستخدم بعض الوقت",
"Gen_Day": "",
"Gen_Delete": "حذف",
"Gen_DeleteAll": "حذف الكل",
"Gen_Description": "الوصف",
@@ -349,9 +350,11 @@
"Gen_Filter": "تصفية",
"Gen_Flapping": "",
"Gen_Generate": "إنشاء",
"Gen_Hour": "",
"Gen_InvalidMac": "عنوان MAC غير صالح.",
"Gen_Invalid_Value": "تم إدخال قيمة غير صالحة",
"Gen_LockedDB": "قاعدة البيانات مقفلة",
"Gen_Month": "",
"Gen_NetworkMask": "قناع الشبكة",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "تحديث القيمة",
"Gen_ValidIcon": "أيقونة صالحة",
"Gen_Warning": "تحذير",
"Gen_Week": "",
"Gen_Work_In_Progress": "جاري العمل...",
"Gen_create_new_device": "إنشاء جهاز جديد",
"Gen_create_new_device_info": "معلومات إنشاء جهاز جديد",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "استعلام الخادم",
"Systeminfo_Network_Server_Query_String": "نص استعلام الخادم",
"Systeminfo_Network_Server_Version": "إصدار الخادم",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "الخدمات",
"Systeminfo_Services_Description": "وصف الخدمات",
"Systeminfo_Services_Name": "اسم الخدمة",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Executar",
"Gen_CopyToClipboard": "Copia a portapapers",
"Gen_DataUpdatedUITakesTime": "D'acord - Pot passar una estona perquè la interfície d'usuari s'actualitzi si s'està executant una exploració.",
"Gen_Day": "",
"Gen_Delete": "Esborrar",
"Gen_DeleteAll": "Esborrar tot",
"Gen_Description": "Descripció",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtrar",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Generar",
"Gen_Hour": "",
"Gen_InvalidMac": "Mac address invàlida.",
"Gen_Invalid_Value": "S'ha introduït un valor incorrecte",
"Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.",
"Gen_Month": "",
"Gen_NetworkMask": "Màscara de xarxa",
"Gen_New": "Nou",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Actualitzar Valor",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Advertència",
"Gen_Week": "",
"Gen_Work_In_Progress": "Work in progress, un bon moment per retroalimentació a https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nou dispositiu",
"Gen_create_new_device_info": "Els dispositius són típicament descobert utilitzant <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. Tanmateix, en certs casos, pots necessitar afegir dispositius a mà. Per explorar els temes concrets comproveu la <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">documentació de Xarxes Remotes</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Consulta del servidor:",
"Systeminfo_Network_Server_Query_String": "Cap cadena de consulta",
"Systeminfo_Network_Server_Version": "Versió del servidor:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Serveis",
"Systeminfo_Services_Description": "Descripció del servei",
"Systeminfo_Services_Name": "Nom del servei",
+13 -1
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Spustit",
"Gen_CopyToClipboard": "Zkopírovat do schránky",
"Gen_DataUpdatedUITakesTime": "OK – může zabrat chvíli aktualizovat rozhraní, pokud probíhá sken.",
"Gen_Day": "",
"Gen_Delete": "Smazat",
"Gen_DeleteAll": "Smazat vše",
"Gen_Description": "Popis",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtr",
"Gen_Flapping": "Nestabilní",
"Gen_Generate": "Nechat vytvořit",
"Gen_Hour": "",
"Gen_InvalidMac": "Neplatná MAC adresa.",
"Gen_Invalid_Value": "Zadána neplatná hodnota",
"Gen_LockedDB": "CHYBA – databáze je možná zamčená – Podívejte se do F12 -> Nástroje pro vývojáře -> Konzole. Nebo to zkuste později.",
"Gen_Month": "",
"Gen_NetworkMask": "Maska sítě",
"Gen_New": "Nový",
"Gen_Next": "Další",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Aktualizovat hodnotu",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Varování",
"Gen_Week": "",
"Gen_Work_In_Progress": "Rozpracováno, ideální čas pro zpětnou vazbu na https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nové zařízení",
"Gen_create_new_device_info": "Zařízení jsou vyhledávána pomocí <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">zásuvných modulů</a>. V určitých případech je však potřeba přidat zařízení ručně. Pokud chcete prozkoumat konkrétní scénáře, podívejte se do <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Dokumentace ke vzdáleným sítím</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Dotaz serveru:",
"Systeminfo_Network_Server_Query_String": "Žádný řetězec dotazu",
"Systeminfo_Network_Server_Version": "Verze serveru:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Služby",
"Systeminfo_Services_Description": "Popis služby",
"Systeminfo_Services_Name": "Název služby",
@@ -840,4 +852,4 @@
"settings_system_label": "Systém",
"settings_update_item_warning": "Zaktualizujte níže uvedenou hodnotu. Dávejte pozor, ať je dodržen předchozí formát. <b>Ověřování správnosti není prováděno.</b>",
"test_event_tooltip": "Než budete svá nastavení zkoušet, nejprve vámi provedené změny uložte."
}
}
+12
View File
@@ -345,6 +345,7 @@
"Gen_Copy": "Ausführen",
"Gen_CopyToClipboard": "In die Zwischenablage kopieren",
"Gen_DataUpdatedUITakesTime": "OK – Es kann einen Moment dauern, bis die Benutzeroberfläche aktualisiert wird, während ein Scan ausgeführt wird.",
"Gen_Day": "",
"Gen_Delete": "Löschen",
"Gen_DeleteAll": "Alles löschen",
"Gen_Description": "Beschreibung",
@@ -353,9 +354,11 @@
"Gen_Filter": "Filter",
"Gen_Flapping": "",
"Gen_Generate": "Generieren",
"Gen_Hour": "",
"Gen_InvalidMac": "Ungültige MAC-Adresse.",
"Gen_Invalid_Value": "Ein ungültiger Wert wurde eingegeben",
"Gen_LockedDB": "ERROR - DB eventuell gesperrt - Nutze die Konsole in den Entwickler Werkzeugen (F12) zur Überprüfung oder probiere es später erneut.",
"Gen_Month": "",
"Gen_NetworkMask": "Netzmaske",
"Gen_New": "Neu",
"Gen_Next": "",
@@ -387,6 +390,7 @@
"Gen_Update_Value": "Wert aktualisieren",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Warnung",
"Gen_Week": "",
"Gen_Work_In_Progress": "Keine Finalversion, feedback bitte unter: https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Neues Gerät",
"Gen_create_new_device_info": "Geräte werden normalerweise über <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">Plugins</a> gefunden. In Ausnahmefällen kann es nötig sein, sie manuell hinzuzufügen. Konkrete Szenarien sind in der <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Dokumentation über entfernte Netzwerke</a> zu finden.",
@@ -773,6 +777,14 @@
"Systeminfo_Network_Server_Query": "Serverabfrage:",
"Systeminfo_Network_Server_Query_String": "Keine Abfragezeichenfolge",
"Systeminfo_Network_Server_Version": "Serverversion:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Dienste",
"Systeminfo_Services_Description": "Dienstbeschreibung",
"Systeminfo_Services_Name": "Dienstname",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Run",
"Gen_CopyToClipboard": "Copy to clipboard",
"Gen_DataUpdatedUITakesTime": "OK - It may take a while for the UI to update if a scan is running.",
"Gen_Day": "Day",
"Gen_Delete": "Delete",
"Gen_DeleteAll": "Delete all",
"Gen_Description": "Description",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filter",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Generate",
"Gen_Hour": "Hour",
"Gen_InvalidMac": "Invalid Mac address.",
"Gen_Invalid_Value": "An invalid value was entered",
"Gen_LockedDB": "ERROR - DB might be locked - Check F12 Dev tools -> Console or try later.",
"Gen_Month": "Month",
"Gen_NetworkMask": "Network mask",
"Gen_New": "New",
"Gen_Next": "Next",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Update Value",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Warning",
"Gen_Week": "Week",
"Gen_Work_In_Progress": "Work in progress, good time to feedback on https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "New device",
"Gen_create_new_device_info": "Devices are typically discovered using <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. However, in certain cases, you may need to add devices manually. To explore specific scenarios check the <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Remote Networks documentation</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Server query:",
"Systeminfo_Network_Server_Query_String": "No query string",
"Systeminfo_Network_Server_Version": "Server Version:",
"Systeminfo_Performance": "Performance",
"Systeminfo_Performance_CPU": "CPU %",
"Systeminfo_Performance_Disabled": "Resource history collection is currently disabled. <a onclick=\"toggleAllSettings()\" href=\"#MAINT_PERF_DAYS\">Enable it in Settings</a>.",
"Systeminfo_Performance_Duration": "Scan Duration (s)",
"Systeminfo_Performance_IO_Caption": "Direct NetAlertX process I/O only - plugin subprocess I/O (e.g. a scanner writing its own result file) is not included.",
"Systeminfo_Performance_IO_Read": "IO Read (MB)",
"Systeminfo_Performance_IO_Write": "IO Write (MB)",
"Systeminfo_Performance_Memory": "Memory Usage (MB)",
"Systeminfo_Services": "Services",
"Systeminfo_Services_Description": "Service Description",
"Systeminfo_Services_Name": "Service Name",
+12
View File
@@ -343,6 +343,7 @@
"Gen_Copy": "Ejecutar",
"Gen_CopyToClipboard": "Copiar al portapapeles",
"Gen_DataUpdatedUITakesTime": "Correcto - La interfaz puede tardar en actualizarse si se está ejecutando un escaneo.",
"Gen_Day": "",
"Gen_Delete": "Eliminar",
"Gen_DeleteAll": "Eliminar todo",
"Gen_Description": "Descripción",
@@ -351,9 +352,11 @@
"Gen_Filter": "Filtro",
"Gen_Flapping": "Agitado",
"Gen_Generate": "Generar",
"Gen_Hour": "",
"Gen_InvalidMac": "Dirección MAC inválida.",
"Gen_Invalid_Value": "Un valor inválido fue ingresado",
"Gen_LockedDB": "Fallo - La base de datos puede estar bloqueada - Pulsa F1 -> Ajustes de desarrolladores -> Consola o prueba más tarde.",
"Gen_Month": "",
"Gen_NetworkMask": "Máscara de red",
"Gen_New": "Nuevo",
"Gen_Next": "Siguiente",
@@ -385,6 +388,7 @@
"Gen_Update_Value": "Actualizar valor",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Advertencia",
"Gen_Week": "",
"Gen_Work_In_Progress": "Trabajo en curso, un buen momento para hacer comentarios en https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nuevo dispositivo",
"Gen_create_new_device_info": "Los dispositivos se suelen descubrir utilizando <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. Sin embargo, en algunos casos, es posible que necesite agregar dispositivos manualmente. Para explorar escenarios específicos, consulte la documentación <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Redes remotas</a>.",
@@ -772,6 +776,14 @@
"Systeminfo_Network_Server_Query": "Consulta del servidor:",
"Systeminfo_Network_Server_Query_String": "Sin cadena de consulta",
"Systeminfo_Network_Server_Version": "Versión del servidor:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Servicios",
"Systeminfo_Services_Description": "Descripción del servicio",
"Systeminfo_Services_Name": "Nombre del servicio",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Day": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Week": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Day": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Week": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Lancer",
"Gen_CopyToClipboard": "Copier vers le presse-papier",
"Gen_DataUpdatedUITakesTime": "OK - cela peut prendre du temps à l'interface pour se mettre à jour si un scan est en cours.",
"Gen_Day": "",
"Gen_Delete": "Supprimer",
"Gen_DeleteAll": "Supprimer tous",
"Gen_Description": "Description",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtrer",
"Gen_Flapping": "Bagotage",
"Gen_Generate": "Générer",
"Gen_Hour": "",
"Gen_InvalidMac": "Adresse MAC invalide.",
"Gen_Invalid_Value": "Une valeur invalide a été renseignée",
"Gen_LockedDB": "Erreur - La base de données est peut-être verrouillée - Vérifier avec les outils de dév via F12 -> Console ou essayer plus tard.",
"Gen_Month": "",
"Gen_NetworkMask": "Masque réseau",
"Gen_New": "Nouveau",
"Gen_Next": "Suivant",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Valeur à mettre à jour",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Avertissement",
"Gen_Week": "",
"Gen_Work_In_Progress": "Travaux en cours, c'est le bon moment pour faire un retour via la liste d'anomalies sur Github https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nouvel appareil",
"Gen_create_new_device_info": "Les appareils sont souvent découverts à l'aide d'un <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugin</a>. Cependant, dans certains cas, vous pouvez ajouter manuellement les appareils. Pour explorer des scénarios spécifiques, consulter la <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">documentation des réseaux distants</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Requête du serveur :",
"Systeminfo_Network_Server_Query_String": "Aucune chaîne de requête",
"Systeminfo_Network_Server_Version": "Version du serveur :",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Services",
"Systeminfo_Services_Description": "Description du service",
"Systeminfo_Services_Name": "Nom du service",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Day": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Week": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Day": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Week": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Day": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Week": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Esegui",
"Gen_CopyToClipboard": "Copia negli appunti",
"Gen_DataUpdatedUITakesTime": "OK: l'aggiornamento dell'interfaccia utente potrebbe richiedere del tempo se è in esecuzione una scansione.",
"Gen_Day": "",
"Gen_Delete": "Elimina",
"Gen_DeleteAll": "Elimina tutti",
"Gen_Description": "Descrizione",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtro",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Genera",
"Gen_Hour": "",
"Gen_InvalidMac": "Indirizzo Mac non valido.",
"Gen_Invalid_Value": "È stato inserito un valore non valido",
"Gen_LockedDB": "ERRORE: il DB potrebbe essere bloccato, controlla F12 Strumenti di sviluppo -> Console o riprova più tardi.",
"Gen_Month": "",
"Gen_NetworkMask": "Maschera di rete",
"Gen_New": "Nuovo",
"Gen_Next": "Successivo",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Aggiorna valore",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Avviso",
"Gen_Week": "",
"Gen_Work_In_Progress": "Lavori in corso, è quindi un buon momento per un feedback su https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nuovo dispositivo",
"Gen_create_new_device_info": "I dispositivi vengono generalmente rilevati utilizzando <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugin</a>. Tuttavia, in alcuni casi, potrebbe essere necessario aggiungere manualmente i dispositivi. Per esplorare scenari specifici, consulta la <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">documentazione sulle reti remote</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Interrogazione server:",
"Systeminfo_Network_Server_Query_String": "Nessuna stringa di interrogazione",
"Systeminfo_Network_Server_Version": "Versione server:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Servizi",
"Systeminfo_Services_Description": "Descrizione servizio",
"Systeminfo_Services_Name": "Nome servizio",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "実行",
"Gen_CopyToClipboard": "クリップボードにコピー",
"Gen_DataUpdatedUITakesTime": "OK - スキャン実行中は、UIの更新に時間がかかることがあります。",
"Gen_Day": "",
"Gen_Delete": "削除",
"Gen_DeleteAll": "全削除",
"Gen_Description": "説明",
@@ -349,9 +350,11 @@
"Gen_Filter": "フィルター",
"Gen_Flapping": "フラッピング",
"Gen_Generate": "生成",
"Gen_Hour": "",
"Gen_InvalidMac": "無効なMacアドレス。",
"Gen_Invalid_Value": "無効な値が入力されました",
"Gen_LockedDB": "エラー - DBがロックされている可能性があります - F12で開発者ツール→コンソールを確認するか、後で試してください。",
"Gen_Month": "",
"Gen_NetworkMask": "ネットワークマスク",
"Gen_New": "New",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "更新値",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "警告",
"Gen_Week": "",
"Gen_Work_In_Progress": "作業中、https://github.com/netalertx/NetAlertX/issues へのフィードバックの好機です",
"Gen_create_new_device": "新規デバイス",
"Gen_create_new_device_info": "デバイスは通常、<a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">プラグイン</a> を使用して検出されます。ただし、特定のケースでは手動でデバイスを追加する必要がある場合があります。具体的なシナリオについては、<a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">リモートネットワークドキュメント</a> を参照してください。",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "サーバークエリ:",
"Systeminfo_Network_Server_Query_String": "クエリ文字列なし",
"Systeminfo_Network_Server_Version": "サーバーバージョン:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "サービス",
"Systeminfo_Services_Description": "サービス内容",
"Systeminfo_Services_Name": "サービス名",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Kjør",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK - Det kan ta litt tid før brukergrensesnittet oppdateres hvis en skanning kjøres.",
"Gen_Day": "",
"Gen_Delete": "Slett",
"Gen_DeleteAll": "Slett alle",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filter",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "FEIL - DB kan være låst - Sjekk F12 Dev tools -> Konsoll eller prøv senere.",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Oppdater verdi",
"Gen_ValidIcon": "",
"Gen_Warning": "Advarsel",
"Gen_Week": "",
"Gen_Work_In_Progress": "Work in progress, gjerne kom med tilbakemeldinger på https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Serverforespørsel:",
"Systeminfo_Network_Server_Query_String": "Ingen spørringsstreng",
"Systeminfo_Network_Server_Version": "Server versjon:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Tjenester",
"Systeminfo_Services_Description": "Tjeneste beskrivelse",
"Systeminfo_Services_Name": "Tjeneste navn",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Wykonaj",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK – Może to potrwać chwilę, zanim interfejs użytkownika się zaktualizuje, jeśli trwa skan.",
"Gen_Day": "",
"Gen_Delete": "Usuń",
"Gen_DeleteAll": "Usuń wszystko",
"Gen_Description": "Opis",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtr",
"Gen_Flapping": "",
"Gen_Generate": "Wygeneruj",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "Błąd - Baza danych może być zablokowana - Sprawdź narzędzia deweloperskie F12 -> Konsola lub spróbuj później.",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Aktualizuj wartość",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Ostrzeżenie",
"Gen_Week": "",
"Gen_Work_In_Progress": "Prace w toku, to dobry moment na przesłanie opinii na https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Nowe urządzenie",
"Gen_create_new_device_info": "Urządzenia są zazwyczaj wykrywane za pomocą <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">wtyczek</a>. Jednak w niektórych przypadkach może być konieczne dodanie urządzeń ręcznie. Aby zapoznać się z konkretnymi scenariuszami, sprawdź <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">dokumentację dotyczącą sieci zdalnych</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Zapytanie serwerowe:",
"Systeminfo_Network_Server_Query_String": "Brak ciągu zapytania",
"Systeminfo_Network_Server_Version": "Wersja serwera:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Usługi",
"Systeminfo_Services_Description": "Opis usługi",
"Systeminfo_Services_Name": "Nazwa usługi",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Executar",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "OK - Pode levar um tempo para a interface do usuário ser atualizada se uma verificação estiver em execução.",
"Gen_Day": "",
"Gen_Delete": "Excluir",
"Gen_DeleteAll": "Excluir todos",
"Gen_Description": "Descrição",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtro",
"Gen_Flapping": "",
"Gen_Generate": "Gerar",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "ERRO - O banco de dados pode estar bloqueado - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Atualizar valor",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Aviso",
"Gen_Week": "",
"Gen_Work_In_Progress": "Trabalho em andamento, um bom momento para enviar feedback em https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Novo dispositivo",
"Gen_create_new_device_info": "Os dispositivos são normalmente descobertos usando <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. No entanto, em certos casos, pode ser necessário adicionar dispositivos manualmente. Para explorar cenários específicos, verifique a <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">documentação de Redes Remotas</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Executar",
"Gen_CopyToClipboard": "Copiar para a área de transferência",
"Gen_DataUpdatedUITakesTime": "OK - Pode levar um tempo para a interface do utilizador ser atualizada se uma verificação estiver em execução.",
"Gen_Day": "",
"Gen_Delete": "Apagar",
"Gen_DeleteAll": "Apagar todos",
"Gen_Description": "Descrição",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtro",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Gerar",
"Gen_Hour": "",
"Gen_InvalidMac": "Endereço Mac inválido.",
"Gen_Invalid_Value": "Um valor inválido foi inserido",
"Gen_LockedDB": "ERRO - A base de dados pode estar bloqueada - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.",
"Gen_Month": "",
"Gen_NetworkMask": "Máscara de Rede",
"Gen_New": "Novo",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Atualizar valor",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Aviso",
"Gen_Week": "",
"Gen_Work_In_Progress": "Trabalho em andamento, um bom momento para enviar feedback em https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Novo dispositivo",
"Gen_create_new_device_info": "Os dispositivos são normalmente descobertos usando <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. No entanto, em certos casos, pode ser necessário adicionar dispositivos manualmente. Para explorar cenários específicos, verifique a <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">documentação de Redes Remotas</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Consulta de servidor:",
"Systeminfo_Network_Server_Query_String": "Nenhuma string de consulta",
"Systeminfo_Network_Server_Version": "Versão do Servidor:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Serviços",
"Systeminfo_Services_Description": "Descrição do Serviço",
"Systeminfo_Services_Name": "Nome do Serviço",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Запустить",
"Gen_CopyToClipboard": "Копировать в буфер обмена",
"Gen_DataUpdatedUITakesTime": "ОК - Обновление UI может занять некоторое время, если сканирование выполняется.",
"Gen_Day": "",
"Gen_Delete": "Удалить",
"Gen_DeleteAll": "Удалить все",
"Gen_Description": "Описание",
@@ -349,9 +350,11 @@
"Gen_Filter": "Фильтр",
"Gen_Flapping": "Нестабильный",
"Gen_Generate": "Генерировать",
"Gen_Hour": "",
"Gen_InvalidMac": "Неверный Mac-адрес.",
"Gen_Invalid_Value": "Введено некорректное значение",
"Gen_LockedDB": "ОШИБКА - Возможно, база данных заблокирована. Проверьте инструменты разработчика F12 -> Консоль или повторите попытку позже.",
"Gen_Month": "",
"Gen_NetworkMask": "Маска сети",
"Gen_New": "Новый",
"Gen_Next": "Следующий",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Обновить значение",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Предупреждение",
"Gen_Week": "",
"Gen_Work_In_Progress": "Работа продолжается, самое время оставить отзыв на https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Новое устройство",
"Gen_create_new_device_info": "Устройства обычно обнаруживаются с помощью <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">плагинов</a>. Однако в некоторых случаях вам может потребоваться добавить устройства вручную. Для изучения конкретных сценариев ознакомьтесь с документацией <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Remote Networks</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Запрос сервера:",
"Systeminfo_Network_Server_Query_String": "Нет строки запроса",
"Systeminfo_Network_Server_Version": "Версия сервера:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Службы",
"Systeminfo_Services_Description": "Описание службы",
"Systeminfo_Services_Name": "Имя службы",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Day": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Week": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Çalıştır",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "TAMAM - Eğer bir tarama çalışıyorsa arayüzün güncellenmesi biraz zaman alabilir.",
"Gen_Day": "",
"Gen_Delete": "Sil",
"Gen_DeleteAll": "Tümünü sil",
"Gen_Description": "Açıklama",
@@ -349,9 +350,11 @@
"Gen_Filter": "Filtre",
"Gen_Flapping": "",
"Gen_Generate": "Oluştur",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "HATA - Veritabanı kilitlenmiş olabilir - F12 Geliştirici araçlarını -> Konsol kısmını kontrol edin veya daha sonra tekrar deneyin.",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Değeri Güncelle",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Uyarı",
"Gen_Week": "",
"Gen_Work_In_Progress": "Çalışma devam ediyor, geri bildirimde bulunmak için iyi bir zaman: https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Yeni cihaz",
"Gen_create_new_device_info": "Cihazlar genellikle <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">eklentiler</a> kullanılarak keşfedilir. Ancak, bazı durumlarda cihazları manuel olarak eklemeniz gerekebilir. Belirli senaryoları incelemek için <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">Uzak Ağlar belgelerini</a> kontrol edin.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "Запустити",
"Gen_CopyToClipboard": "Копіювати в буфер обміну",
"Gen_DataUpdatedUITakesTime": "Добре. Оновлення інтерфейсу може зайняти деякий час, якщо сканування виконується.",
"Gen_Day": "",
"Gen_Delete": "Видалити",
"Gen_DeleteAll": "Видалити все",
"Gen_Description": "Опис",
@@ -349,9 +350,11 @@
"Gen_Filter": "Фільтр",
"Gen_Flapping": "",
"Gen_Generate": "Генерувати",
"Gen_Hour": "",
"Gen_InvalidMac": "Недійсна Mac-адреса.",
"Gen_Invalid_Value": "Введено недійсне значення",
"Gen_LockedDB": "ПОМИЛКА – БД може бути заблоковано – перевірте F12 Інструменти розробника -> Консоль або спробуйте пізніше.",
"Gen_Month": "",
"Gen_NetworkMask": "Маска мережі",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "Оновити значення",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "Попередження",
"Gen_Week": "",
"Gen_Work_In_Progress": "Робота триває, час залишити відгук на https://github.com/netalertx/NetAlertX/issues",
"Gen_create_new_device": "Новий пристрій",
"Gen_create_new_device_info": "Пристрої зазвичай виявляють за допомогою <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">плагінів</a>. Однак у деяких випадках може знадобитися додати пристрої вручну. Щоб дослідити конкретні сценарії, перегляньте <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">документацію щодо віддалених мереж</a>.",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "Запит до сервера:",
"Systeminfo_Network_Server_Query_String": "Немає рядка запиту",
"Systeminfo_Network_Server_Version": "Версія сервера:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "Послуги",
"Systeminfo_Services_Description": "Опис послуги",
"Systeminfo_Services_Name": "Назва служби",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Day": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
@@ -349,9 +350,11 @@
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_Hour": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_Month": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Week": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
+12
View File
@@ -341,6 +341,7 @@
"Gen_Copy": "运行",
"Gen_CopyToClipboard": "复制到剪贴板",
"Gen_DataUpdatedUITakesTime": "好的 - 如果扫描正在运行,UI 可能需要一段时间才能更新。",
"Gen_Day": "",
"Gen_Delete": "删除",
"Gen_DeleteAll": "全部删除",
"Gen_Description": "描述",
@@ -349,9 +350,11 @@
"Gen_Filter": "筛选",
"Gen_Flapping": "跳动",
"Gen_Generate": "生成",
"Gen_Hour": "",
"Gen_InvalidMac": "无效的 Mac 地址。",
"Gen_Invalid_Value": "输入了无效的值",
"Gen_LockedDB": "错误 - DB 可能被锁定 - 检查 F12 开发工具 -> 控制台或稍后重试。",
"Gen_Month": "",
"Gen_NetworkMask": "网络掩码",
"Gen_New": "新的",
"Gen_Next": "",
@@ -383,6 +386,7 @@
"Gen_Update_Value": "更新值",
"Gen_ValidIcon": "<i class=\"fa-solid fa-chevron-right \"></i>",
"Gen_Warning": "警告",
"Gen_Week": "",
"Gen_Work_In_Progress": "工作正在进行中,欢迎在 https://github.com/netalertx/NetAlertX/issues 上反馈",
"Gen_create_new_device": "新设备",
"Gen_create_new_device_info": "通常使用<a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>来发现设备。但是,在某些情况下,您可能需要手动添加设备。要探索特定场景,请查看<a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">远程网络文档</a>。",
@@ -712,6 +716,14 @@
"Systeminfo_Network_Server_Query": "服务器查询:",
"Systeminfo_Network_Server_Query_String": "没有查询到字符串",
"Systeminfo_Network_Server_Version": "服务器版本:",
"Systeminfo_Performance": "",
"Systeminfo_Performance_CPU": "",
"Systeminfo_Performance_Disabled": "",
"Systeminfo_Performance_Duration": "",
"Systeminfo_Performance_IO_Caption": "",
"Systeminfo_Performance_IO_Read": "",
"Systeminfo_Performance_IO_Write": "",
"Systeminfo_Performance_Memory": "",
"Systeminfo_Services": "服务",
"Systeminfo_Services_Description": "服务说明",
"Systeminfo_Services_Name": "服务名称",
@@ -0,0 +1,10 @@
<div class="skel-info-tab spinnerTarget">
<div class="skel-table-header-row">
<?php for ($i = 0; $i < 4; $i++): ?>
<span class="skel-th skel-shimmer"></span>
<?php endfor; ?>
</div>
<?php for ($i = 0; $i < 4; $i++): ?>
<span class="skel-shimmer" style="height:150px; width:100%; display:block; margin-bottom:15px; border-radius:4px;"></span>
<?php endfor; ?>
</div>
+15
View File
@@ -15,6 +15,10 @@
?>
<!-- ----------------------------------------------------------------------- -->
<!-- Loaded here (not in systeminfoPerformance.php itself) so it's available
before that tab's AJAX-fetched content runs - matching presence.php/
devices.php's own graph component pattern. -->
<script src="lib/chart.js/Chart.js"></script>
<script>
@@ -65,6 +69,14 @@
</span>
</a>
</li>
<li>
<a id="tabPerformance" href="#panPerformance" data-toggle="tab">
<i class="fa fa-chart-line"></i>
<span class="dev-detail-tab-name">
<?= lang('Systeminfo_Performance');?>
</span>
</a>
</li>
</ul>
<div class="tab-content spinnerTarget">
@@ -80,6 +92,9 @@
<div class="tab-pane fade" data-php-file="systeminfoInitCheck.php" id="panInitCheck">
<?php require 'php/templates/skel_tab_sysinfo_initcheck.php'; ?>
</div>
<div class="tab-pane fade" data-php-file="systeminfoPerformance.php" id="panPerformance">
<?php require 'php/templates/skel_tab_sysinfo_performance.php'; ?>
</div>
</div>
<!-- /.tab-content -->
</div>
+65
View File
@@ -0,0 +1,65 @@
<?php
//------------------------------------------------------------------------------
// check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/server/db.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/language/lang.php';
?>
<script src="js/graph_resource_history.js"></script>
<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-chart-line"></i> <?= lang('Systeminfo_Performance');?>
<a href="https://docs.netalertx.com/PERFORMANCE" target="_blank"><i class="fa fa-circle-question" style="font-size: 14px;"></i></a>
</h3>
</div>
<div class="box-body">
<div class="btn-group" role="group" id="resourceHistoryRangeGroup" style="margin-bottom: 15px;">
<button type="button" class="btn btn-default resource-history-range-btn" data-range="hour"><?= lang('Gen_Hour');?></button>
<button type="button" class="btn btn-default resource-history-range-btn active" data-range="day"><?= lang('Gen_Day');?></button>
<button type="button" class="btn btn-default resource-history-range-btn" data-range="week"><?= lang('Gen_Week');?></button>
<button type="button" class="btn btn-default resource-history-range-btn" data-range="month"><?= lang('Gen_Month');?></button>
</div>
<div id="resourceHistoryDisabledMessage" class="myhidden" style="padding: 15px;">
<?= lang('Systeminfo_Performance_Disabled');?>
</div>
<div id="resourceHistoryCharts">
<div style="position: relative; height: 220px; width: 100%; margin-bottom: 25px;">
<canvas id="ResourceCpuChart"></canvas>
</div>
<div style="position: relative; height: 220px; width: 100%; margin-bottom: 25px;">
<canvas id="ResourceRssChart"></canvas>
</div>
<div style="position: relative; height: 220px; width: 100%; margin-bottom: 5px;">
<canvas id="ResourceIoChart"></canvas>
</div>
<p class="text-muted" style="font-size: 12px; margin-bottom: 25px;"><?= lang('Systeminfo_Performance_IO_Caption');?></p>
<div style="position: relative; height: 220px; width: 100%; margin-bottom: 15px;">
<canvas id="ResourceDurationChart"></canvas>
</div>
</div>
</div>
</div>
<script>
if (isAppInitialized()) {
initResourceHistoryGraphs('day');
} else {
callAfterAppInitialized(() => initResourceHistoryGraphs('day'));
}
$('#resourceHistoryRangeGroup .resource-history-range-btn').on('click', function () {
$('#resourceHistoryRangeGroup .resource-history-range-btn').removeClass('active');
$(this).addClass('active');
initResourceHistoryGraphs($(this).data('range'));
});
hideSpinner();
</script>
+57
View File
@@ -10,6 +10,63 @@
<?php
// ----------------------------------------------------------
// Database (moved here from maintenance.php - both are storage-facts)
// ----------------------------------------------------------
$dbBasePath = rtrim(getenv('NETALERTX_DB') ?: '/data/db', '/');
$nax_db = $dbBasePath . '/app.db';
$nax_wal = $dbBasePath . '/app.db-wal';
$nax_db_size = file_exists($nax_db) ? number_format((filesize($nax_db) / 1000000), 2, ",", ".") . ' MB' : '0 MB';
$nax_wal_size = file_exists($nax_wal) ? number_format((filesize($nax_wal) / 1000000), 2, ",", ".") . ' MB' : '0 MB';
$nax_db_mod = file_exists($nax_db) ? date("F d Y H:i:s", filemtime($nax_db)) : 'N/A';
// Table row counts. Read-only + existence check: a plain `new SQLite3($nax_db)`
// with default flags would CREATE an empty app.db if the path is ever wrong/
// missing (e.g. before first scan), and an unhandled open/query exception here
// would previously fatal-error this entire AJAX-loaded tab, not just this box.
$tableSizesHTML = "";
if (is_readable($nax_db)) {
try {
$db_info_conn = new SQLite3($nax_db, SQLITE3_OPEN_READONLY);
$table_names_result = $db_info_conn->query("SELECT name FROM sqlite_master WHERE type='table'");
while ($row = $table_names_result->fetchArray(SQLITE3_ASSOC)) {
$tableName = $row['name'];
$countResult = $db_info_conn->querySingle("SELECT COUNT(*) FROM $tableName");
$tableSizesHTML = $tableSizesHTML . "$tableName (<b>$countResult</b>), ";
}
$db_info_conn->close();
} catch (Exception $e) {
$tableSizesHTML = '';
}
}
echo '<div class="box box-solid">
<div class="box-header">
<h3 class="box-title sysinfo_headline"><i class="fa fa-database"></i> ' . lang('Maintenance_Status') . '</h3>
</div>
<div class="box-body">
<div class="db_info_table">
<div class="db_info_table_row">
<div class="db_info_table_cell" style="min-width: 140px">' . lang('Maintenance_database_path') . '</div>
<div class="db_info_table_cell">' . $nax_db . '</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell">' . lang('Maintenance_database_size') . '</div>
<div class="db_info_table_cell">' . $nax_db_size . ' (wal: ' . $nax_wal_size . ')</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell">' . lang('Maintenance_database_rows') . '</div>
<div class="db_info_table_cell">' . $tableSizesHTML . '</div>
</div>
<div class="db_info_table_row">
<div class="db_info_table_cell">' . lang('Maintenance_database_lastmod') . '</div>
<div class="db_info_table_cell">' . $nax_db_mod . '</div>
</div>
</div>
</div>
</div>';
// ----------------------------------------------------------
// Storage
// ----------------------------------------------------------
+58 -30
View File
@@ -25,11 +25,12 @@ from pathlib import Path
import conf
from const import fullConfPath, sql_new_devices
from logger import mylog
from helper import filePermissions
from helper import filePermissions, get_setting_value
from utils.datetime_utils import timeNowUTC, is_datetime_future, normalizeTimeStamp
from app_state import updateState
from api import update_api, check_activity, update_GUI_port
from scan.session_events import process_scan
from scan.resource_history import get_process_cpu_times_and_io, insert_resource_history
from initialise import importConfigs, renameSettings
from database import DB
from messaging.reporting import get_notifications
@@ -149,43 +150,70 @@ def main():
# last time any scan or maintenance/upkeep was run
conf.last_scan_run = loop_start_time
# Header (also broadcasts last_scan_run to frontend via SSE / app_state.json)
updateState("Process: Start",
last_scan_run=loop_start_time.replace(microsecond=0).isoformat(),
next_scan_time="")
# Resource-usage sampling: brackets the whole schedule-tick block
# below (run_plugin_scripts("schedule") through process_scan()),
# not just the scan pipeline itself - plugin subprocess execution
# happens in this window and is the class of cost this history
# exists to catch. Gated on MAINT_PERF_DAYS so a disabled install
# pays no sampling cost at all (see scan/resource_history.py).
# Defensive: an empty/corrupted setting value must disable this
# optional feature, not raise and kill the whole main loop -
# this sits outside the try/finally below, so an uncaught
# ValueError/TypeError here would have no safety net at all.
try:
resource_history_enabled = int(get_setting_value("MAINT_PERF_DAYS", 30)) != 0
except (TypeError, ValueError):
resource_history_enabled = False
resource_pre = get_process_cpu_times_and_io() if resource_history_enabled else None
tick_start_monotonic = time.monotonic()
tick_failed = False
# Timestamp
startTime = loop_start_time
startTime = startTime.replace(microsecond=0)
try:
# Header (also broadcasts last_scan_run to frontend via SSE / app_state.json)
updateState("Process: Start",
last_scan_run=loop_start_time.replace(microsecond=0).isoformat(),
next_scan_time="")
# Check if any plugins need to run on schedule
pm.run_plugin_scripts("schedule")
# Timestamp
startTime = loop_start_time
startTime = startTime.replace(microsecond=0)
# Compute the next scheduled run time AFTER schedule check (which updates last_next_schedule)
# Only device_scanner plugins have meaningful next_scan times for user display
scanner_prefixes = {p["unique_prefix"] for p in all_plugins if p.get("plugin_type") == "device_scanner"}
scanner_next = [s.last_next_schedule for s in conf.mySchedules if s.service in scanner_prefixes]
# Check if any plugins need to run on schedule
pm.run_plugin_scripts("schedule")
# Get the earliest next scan time across all device scanners and broadcast.
# updateState validates the value is in the future before storing/broadcasting.
if scanner_next:
next_scan_dt = min(scanner_next)
updateState(next_scan_time=next_scan_dt.replace(microsecond=0).isoformat())
# Compute the next scheduled run time AFTER schedule check (which updates last_next_schedule)
# Only device_scanner plugins have meaningful next_scan times for user display
scanner_prefixes = {p["unique_prefix"] for p in all_plugins if p.get("plugin_type") == "device_scanner"}
scanner_next = [s.last_next_schedule for s in conf.mySchedules if s.service in scanner_prefixes]
# determine run/scan type based on passed time
# --------------------------------------------
# Get the earliest next scan time across all device scanners and broadcast.
# updateState validates the value is in the future before storing/broadcasting.
if scanner_next:
next_scan_dt = min(scanner_next)
updateState(next_scan_time=next_scan_dt.replace(microsecond=0).isoformat())
# Runs plugin scripts which are set to run every time after a scans finished
pm.run_plugin_scripts("always_after_scan")
# determine run/scan type based on passed time
# --------------------------------------------
# process all the scanned data into new devices
processScan = updateState("Check scan").processScan
mylog("debug", [f"[MAIN] processScan: {processScan}"])
# Runs plugin scripts which are set to run every time after a scans finished
pm.run_plugin_scripts("always_after_scan")
if processScan is True:
mylog("debug", "[MAIN] start processing scan results")
process_scan(db)
updateState("Scan processed", None, None, None, None, False)
# process all the scanned data into new devices
processScan = updateState("Check scan").processScan
mylog("debug", [f"[MAIN] processScan: {processScan}"])
if processScan is True:
mylog("debug", "[MAIN] start processing scan results")
process_scan(db)
updateState("Scan processed", None, None, None, None, False)
except Exception:
tick_failed = True
raise
finally:
if resource_history_enabled:
duration_ms = int((time.monotonic() - tick_start_monotonic) * 1000)
resource_post = get_process_cpu_times_and_io()
insert_resource_history(db, resource_pre, resource_post, duration_ms, tick_failed)
# Name resolution
# --------------------------------------------
+8
View File
@@ -21,6 +21,10 @@ from const import (
sql_language_strings,
sql_notifications_all,
sql_online_history,
sql_resource_history_hour,
sql_resource_history_day,
sql_resource_history_week,
sql_resource_history_month,
sql_devices_filters,
defaultWebPort,
)
@@ -75,6 +79,10 @@ def update_api(
["plugins_language_strings", sql_language_strings],
["notifications", sql_notifications_all],
["online_history", sql_online_history],
["resource_history_hour", sql_resource_history_hour],
["resource_history_day", sql_resource_history_day],
["resource_history_week", sql_resource_history_week],
["resource_history_month", sql_resource_history_month],
["devices_tiles", get_sql_devices_tiles()],
["devices_filters", sql_devices_filters],
["custom_endpoint", conf.API_CUSTOM_SQL],
+1 -1
View File
@@ -2058,7 +2058,7 @@ def check_auth(payload=None):
@validate_request(
operation_id="check_health",
summary="System Health Check",
description="Retrieve system vitality metrics including database size, memory pressure, system load, disk usage, and CPU temperature.",
description="Retrieve system vitality metrics including database size, memory pressure, system load, disk usage, CPU temperature, and the NetAlertX process's own CPU%/RSS.",
response_model=HealthCheckResponse,
tags=["system", "health"],
auth_callable=is_authorized
+44
View File
@@ -155,6 +155,48 @@ def get_storage_gb():
return None
# ===============================================================================
# Process Vitality (NetAlertX's own process, not the host)
# ===============================================================================
# Non-blocking cpu_percent() needs one persistent Process object to mean anything
# ("since the last time this was called") - this object is dedicated to /health only,
# never shared with the tick-scoped sampler in scan/resource_history.py, so the two
# measurement paths can't interfere with each other.
_health_process = psutil.Process()
def get_process_cpu_percent():
"""
CPU% for the NetAlertX process since the last time this was called - a live
gauge for /health, not a tick-scoped measurement (see scan/resource_history.py's
tick-scoped sampler for that).
Returns:
float: CPU utilization percentage, or 0.0 on error.
"""
try:
return _health_process.cpu_percent(interval=None)
except (psutil.Error, AttributeError) as e:
mylog("verbose", [f"[health] Error reading process CPU%: {e}"])
return 0.0
def get_process_rss_mb():
"""
Resident memory (RSS) for the current process, in MB - a point-in-time
snapshot, not a delta (RSS doesn't accumulate the way CPU-time/IO do).
Returns:
float: RSS in MB, or 0.0 on error.
"""
try:
return round(psutil.Process().memory_info().rss / (1024 * 1024), 2)
except (psutil.Error, AttributeError) as e:
mylog("verbose", [f"[health] Error reading process RSS: {e}"])
return 0.0
# ===============================================================================
# Aggregator
# ===============================================================================
@@ -174,4 +216,6 @@ def get_health_status():
"cpu_temp": get_cpu_temp(),
"storage_gb": get_storage_gb(),
"mem_mb": get_mem_mb(),
"process_cpu_pct": get_process_cpu_percent(),
"process_rss_mb": get_process_rss_mb(),
}
+5 -1
View File
@@ -718,7 +718,9 @@ class HealthCheckResponse(BaseResponse):
"storage_pct": 42,
"cpu_temp": 58,
"storage_gb": 8,
"mem_mb" : 8192
"mem_mb" : 8192,
"process_cpu_pct": 3.2,
"process_rss_mb": 142.75
}]
}
)
@@ -730,6 +732,8 @@ class HealthCheckResponse(BaseResponse):
cpu_temp: Optional[int] = Field(None, description="CPU temperature in Celsius (nullable if unavailable)")
storage_gb: Optional[int] = Field(..., description="Storage size in GB")
mem_mb: Optional[int] = Field(..., description="Installed memory size in MB")
process_cpu_pct: float = Field(..., description="NetAlertX process CPU usage percentage since the last /health call (live gauge, not a tick average)")
process_rss_mb: float = Field(..., description="NetAlertX process resident memory (RSS) in MB")
# =============================================================================
+39
View File
@@ -136,6 +136,45 @@ sql_plugins_stats = """SELECT 'objects' AS tableName, plugin, COUNT(*) AS cnt FR
sql_language_strings = "SELECT * FROM Plugins_Language_Strings"
sql_notifications_all = "SELECT * FROM Notifications"
sql_online_history = "SELECT * FROM Online_History"
# Resource_History read-side views (System Info -> Performance tab).
# hour/day return raw per-tick rows (cheap indexed range scan); week/month
# roll up at query time into hourly buckets (SUM for additive IO bytes,
# AVG for CPU%/RSS/duration) - no pre-aggregated rollup table, see
# resource-usage-history PRD Design §4. Deltas are already stored per-row
# by insert_resource_history(), so no LAG()/window function is needed here.
sql_resource_history_hour = """
SELECT resDateTime, resCpuPercent, resRssMb, resIoReadBytes,
resIoWriteBytes, resScanDurationMs, resTickFailed
FROM Resource_History
WHERE resDateTime >= datetime('now', '-1 hour')
ORDER BY resDateTime
"""
sql_resource_history_day = """
SELECT resDateTime, resCpuPercent, resRssMb, resIoReadBytes,
resIoWriteBytes, resScanDurationMs, resTickFailed
FROM Resource_History
WHERE resDateTime >= datetime('now', '-1 day')
ORDER BY resDateTime
"""
sql_resource_history_bucketed_template = """
SELECT
strftime('%Y-%m-%d %H:00:00', resDateTime) AS bucket,
AVG(resCpuPercent) AS resCpuPercent,
AVG(resRssMb) AS resRssMb,
SUM(resIoReadBytes) AS resIoReadBytes,
SUM(resIoWriteBytes) AS resIoWriteBytes,
AVG(resScanDurationMs) AS resScanDurationMs,
MAX(resTickFailed) AS resTickFailed
FROM Resource_History
WHERE resDateTime >= datetime('now', '-{days} day')
GROUP BY bucket
ORDER BY bucket
"""
sql_resource_history_week = sql_resource_history_bucketed_template.format(days=7)
sql_resource_history_month = sql_resource_history_bucketed_template.format(days=30)
sql_plugins_events = "SELECT * FROM Plugins_Events"
sql_plugins_history = "SELECT * FROM Plugins_History ORDER BY dateTimeChanged DESC"
sql_new_devices = """SELECT * FROM (
+4
View File
@@ -12,6 +12,7 @@ from db.db_upgrade import (
ensure_column,
ensure_table_columns,
ensure_CurrentScan,
ensure_Resource_History,
ensure_plugins_tables,
ensure_Parameters,
ensure_Settings,
@@ -241,6 +242,9 @@ class DB:
# CurrentScan table setup
ensure_CurrentScan(self.sql)
# Resource_History table setup
ensure_Resource_History(self.sql)
# Views are created in importConfigs() after settings are committed,
# so NTFPRCS_sleep_time is available when the view is built.
# ensure_views is NOT called here.
+23
View File
@@ -659,6 +659,29 @@ def ensure_CurrentScan(sql) -> bool:
return True
def ensure_Resource_History(sql) -> bool:
"""
Ensures the Resource_History table and its datetime index exist.
Parameters:
- sql: database cursor or connection wrapper (must support execute() and fetchall()).
"""
sql.execute(""" CREATE TABLE IF NOT EXISTS Resource_History (
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
resDateTime TEXT NOT NULL,
resCpuPercent REAL,
resRssMb REAL,
resIoReadBytes INTEGER,
resIoWriteBytes INTEGER,
resScanDurationMs INTEGER,
resTickFailed INTEGER NOT NULL DEFAULT 0
);
""")
sql.execute("CREATE INDEX IF NOT EXISTS idx_reshist_datetime ON Resource_History(resDateTime);")
return True
def ensure_Parameters(sql) -> bool:
"""
Ensures required Parameters table exist.
+11
View File
@@ -10,6 +10,17 @@ CREATE TABLE IF NOT EXISTS Online_History (
offlineDevices INTEGER,
PRIMARY KEY("index" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS Resource_History (
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
resDateTime TEXT NOT NULL,
resCpuPercent REAL,
resRssMb REAL,
resIoReadBytes INTEGER,
resIoWriteBytes INTEGER,
resScanDurationMs INTEGER,
resTickFailed INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_reshist_datetime ON Resource_History(resDateTime);
CREATE TABLE Devices (
devMac STRING (50) PRIMARY KEY NOT NULL COLLATE NOCASE,
devName STRING (50) NOT NULL DEFAULT "(unknown)",
+16
View File
@@ -36,6 +36,7 @@ def main():
DAYS_TO_KEEP_EVENTS = int(get_setting_value("DAYS_TO_KEEP_EVENTS"))
CLEAR_NEW_FLAG = get_setting_value("CLEAR_NEW_FLAG")
DEV_HIST_DAYS = int(get_setting_value("DEV_HIST_DAYS") or 14)
MAINT_PERF_DAYS = int(get_setting_value("MAINT_PERF_DAYS", 30))
mylog("verbose", [f"[{pluginName}] In script"])
@@ -48,6 +49,7 @@ def main():
PLUGINS_KEEP_HIST,
CLEAR_NEW_FLAG,
DEV_HIST_DAYS,
MAINT_PERF_DAYS,
)
mylog("verbose", [f"[{pluginName}] Cleanup complete"])
@@ -66,6 +68,7 @@ def cleanup_database(
PLUGINS_KEEP_HIST,
CLEAR_NEW_FLAG,
DEV_HIST_DAYS=14,
MAINT_PERF_DAYS=30,
):
"""
Cleaning out old records from the tables that don't need to keep all data.
@@ -109,6 +112,19 @@ def cleanup_database(
cursor.execute(sql)
mylog("verbose", [f"[{pluginName}] Sessions deleted rows: {cursor.rowcount}"])
# -----------------------------------------------------
# Resource_History (own resource-usage history, MAINT_PERF_DAYS setting -
# no separate zero-check needed here: if collection is disabled via
# MAINT_PERF_DAYS=0, the table is already empty)
mylog("verbose", f"[{pluginName}] Resource_History: Delete all older than {str(MAINT_PERF_DAYS)} days (MAINT_PERF_DAYS setting)")
# datetime(), not date(): resDateTime carries a time component (timeNowUTC()),
# and a bare date() cutoff (midnight, no time) never compares <= against a
# same-day timestamped row, silently keeping the whole boundary day.
sql = f"""DELETE FROM Resource_History WHERE resDateTime <= datetime('now', '-{str(MAINT_PERF_DAYS)} day')"""
mylog("verbose", [f"[{pluginName}] SQL : {sql}"])
cursor.execute(sql)
mylog("verbose", [f"[{pluginName}] Resource_History deleted rows: {cursor.rowcount}"])
# -----------------------------------------------------
# Plugins_History
mylog("verbose", f"[{pluginName}] Plugins_History: Trim to {str(PLUGINS_KEEP_HIST)} per Plugin")
+1 -1
View File
@@ -1,6 +1,6 @@
## Overview
Handles routine housekeeping so long-running logs and in-app notifications don't grow unbounded: trims `app.log` down to a configured line count, and purges old in-app notification entries past a configured count.
Handles routine housekeeping so long-running logs and in-app notifications don't grow unbounded: trims `app.log` down to a configured line count, purges old in-app notification entries past a configured count, and sets the retention window (in days) for NetAlertX's own resource-usage history (System Info -> Performance tab).
### Usage
+28
View File
@@ -271,6 +271,34 @@
"string": "How many latest in-app Notification entries to keep. Set to <code>0</code> to disable purging."
}
]
},
{
"function": "PERF_DAYS",
"type": {
"dataType": "integer",
"elements": [
{
"elementType": "input",
"elementOptions": [{ "type": "number" }],
"transformers": []
}
]
},
"default_value": 30,
"options": [],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Performance history retention (days)"
}
],
"description": [
{
"language_code": "en_us",
"string": "How many days of NetAlertX's own CPU/RAM/IO/scan-duration history to keep (System Info -> Performance tab). Set to <code>0</code> to disable collection entirely - history cannot be backfilled once enabled, so collection starts from whenever this is turned on."
}
]
}
],
+102
View File
@@ -0,0 +1,102 @@
"""
resource_history.py - tick-scoped resource sampling and persistence.
Called from __main__.py at the top and bottom of the schedule-tick block, so
its window covers run_plugin_scripts("schedule")/process_scan() together -
the two calls that actually cost CPU/IO each cycle. Pure, stateless psutil
reads - safe to call from a fresh Process() each time, no shared state with
health_endpoint.py's live /health gauge at all (see health_endpoint.py).
"""
import psutil
from logger import mylog
from utils.datetime_utils import timeNowUTC
def get_process_cpu_times_and_io():
"""
(user+system+children CPU seconds, read_bytes, write_bytes) for the current
process, as of right now - stateless cumulative reads, meant to be called
twice (before/after a tick) and diffed by the caller, not used as a
standalone live value. Includes children_user/children_system so a
scanning plugin's own subprocess CPU time (reaped into the parent's
cpu_times() by the kernel) is captured - io_counters() has no equivalent
children field, so IO here only ever reflects NetAlertX's own direct I/O,
never a plugin subprocess's.
Defensive: some container security profiles (custom seccomp, restricted
runtimes) can make io_counters()/cpu_times() raise psutil.AccessDenied or
AttributeError - sampling must never be able to break the actual scan
tick, so failures fall back to zeros rather than propagating.
"""
try:
p = psutil.Process()
t = p.cpu_times()
io = p.io_counters()
cpu_time_s = t.user + t.system + t.children_user + t.children_system
return cpu_time_s, io.read_bytes, io.write_bytes
except (psutil.Error, AttributeError) as e:
mylog("verbose", [f"[resource_history] psutil sampling failed, using zeros: {e}"])
return 0.0, 0, 0
def insert_resource_history(db, pre, post, duration_ms, tick_failed=False):
"""
Insert one Resource_History row from before/after tick-scoped samples.
Args:
db: open DB instance (db.sql.execute()).
pre: get_process_cpu_times_and_io() result taken before the tick.
post: get_process_cpu_times_and_io() result taken after the tick.
duration_ms: wall-clock duration of the bracketed tick, in milliseconds.
tick_failed: True if the tick raised and this row was written from the
`finally` clause - lets a later viewer distinguish a truncated,
crash-adjacent sample from an ordinary short tick.
A history-write failure (locked DB, schema drift after a bad migration)
must never take down the real scan tick's own commit - log and move on.
"""
cpu_time_delta_s = post[0] - pre[0]
if duration_ms <= 0:
# Guard the actual edge case only - not an arbitrary "under Nms treat
# as zero" cutoff, which would also zero out a genuinely fast-but-real
# tick's true cost.
resCpuPercent = 0.0
else:
resCpuPercent = (cpu_time_delta_s / (duration_ms / 1000)) * 100
resIoReadBytes = post[1] - pre[1]
resIoWriteBytes = post[2] - pre[2]
# Sampled independently of the INSERT's own try/except below - a psutil
# failure here must zero this one field, not get misreported as an
# "insert failed" and skip the whole row (CPU/IO were already computed
# successfully at this point).
try:
resRssMb = round(psutil.Process().memory_info().rss / (1024 * 1024), 2)
except (psutil.Error, AttributeError) as e:
mylog("verbose", [f"[resource_history] RSS sampling failed, using 0.0: {e}"])
resRssMb = 0.0
try:
db.sql.execute(
"""
INSERT INTO Resource_History
(resDateTime, resCpuPercent, resRssMb, resIoReadBytes,
resIoWriteBytes, resScanDurationMs, resTickFailed)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
timeNowUTC(),
resCpuPercent,
resRssMb,
resIoReadBytes,
resIoWriteBytes,
duration_ms,
int(tick_failed),
),
)
except Exception as e:
mylog("verbose", [f"[resource_history] insert failed, skipping this cycle: {e}"])
+117
View File
@@ -48,6 +48,19 @@ def _make_db():
)
""")
cur.execute("""
CREATE TABLE Resource_History (
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
resDateTime TEXT NOT NULL,
resCpuPercent REAL,
resRssMb REAL,
resIoReadBytes INTEGER,
resIoWriteBytes INTEGER,
resScanDurationMs INTEGER,
resTickFailed INTEGER NOT NULL DEFAULT 0
)
""")
conn.commit()
return conn
@@ -164,6 +177,110 @@ class TestSessionsTrim:
assert sessions_expr in source, "Sessions DELETE is not aligned with Events DELETE"
# ---------------------------------------------------------------------------
# Resource_History retention (MAINT_PERF_DAYS)
# ---------------------------------------------------------------------------
def _seed_resource_history(cur, old_count: int, recent_count: int, days: int):
for i in range(old_count):
cur.execute(
"INSERT INTO Resource_History (resDateTime, resCpuPercent) "
"VALUES (datetime('now', ?), 10.0)",
(f"-{days + 1} day",),
)
for i in range(recent_count):
cur.execute(
"INSERT INTO Resource_History (resDateTime, resCpuPercent) "
"VALUES (datetime('now'), 10.0)"
)
def _run_resource_history_trim(cur, days: int) -> int:
"""Execute the exact DELETE used by db_cleanup and return rowcount."""
cur.execute(
f"DELETE FROM Resource_History "
f"WHERE resDateTime <= datetime('now', '-{days} day')"
)
return cur.rowcount
class TestResourceHistoryTrim:
def test_old_rows_are_deleted(self):
conn = _make_db()
cur = conn.cursor()
_seed_resource_history(cur, old_count=10, recent_count=5, days=30)
deleted = _run_resource_history_trim(cur, days=30)
assert deleted == 10
cur.execute("SELECT COUNT(*) FROM Resource_History")
assert cur.fetchone()[0] == 5
def test_recent_rows_are_preserved(self):
conn = _make_db()
cur = conn.cursor()
_seed_resource_history(cur, old_count=0, recent_count=20, days=30)
deleted = _run_resource_history_trim(cur, days=30)
assert deleted == 0
cur.execute("SELECT COUNT(*) FROM Resource_History")
assert cur.fetchone()[0] == 20
def test_empty_table_is_a_no_op(self):
conn = _make_db()
cur = conn.cursor()
assert _run_resource_history_trim(cur, days=30) == 0
def test_date_cutoff_would_silently_under_delete_the_boundary_day(self):
"""
Regression, demonstrating the bug the datetime() fix closes: date()
truncates the cutoff to midnight (10-char "YYYY-MM-DD"), while
resDateTime always carries a time component (19-char
"YYYY-MM-DD HH:MM:SS", from timeNowUTC()). Since the bare date string
is a strict prefix of any same-day timestamp, plain string comparison
(SQLite has no typed DATE column here) means resDateTime <= date(...)
is FALSE for every row on the cutoff day, regardless of its time of
day - the whole boundary day silently survives a date() cutoff.
datetime() doesn't have this gap: both sides are 19-char timestamps.
"""
conn = sqlite3.connect(":memory:")
cur = conn.cursor()
date_cutoff = cur.execute("SELECT date('now', '-30 day')").fetchone()[0]
datetime_cutoff = cur.execute("SELECT datetime('now', '-30 day')").fetchone()[0]
assert len(date_cutoff) == 10 # "YYYY-MM-DD" - no time component
assert len(datetime_cutoff) == 19 # "YYYY-MM-DD HH:MM:SS"
assert datetime_cutoff.startswith(date_cutoff)
# A same-day resDateTime value (any time after midnight) sorts after
# the bare date cutoff, so it would never satisfy `<=` under date().
same_day_timestamp = date_cutoff + " 08:00:00"
assert not (same_day_timestamp <= date_cutoff), (
"date() cutoff must fail to catch a same-day timestamped row - "
"this is exactly the bug datetime() fixes"
)
def test_resource_history_trim_uses_datetime_not_date(self):
"""
Regression: assert script.py's actual DELETE uses datetime(), matching
the precision of resDateTime and the read-side range queries
(const.py's sql_resource_history_* use datetime() too) - a bare
date() cutoff would silently under-delete (see test above).
"""
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
script_path = os.path.join(
INSTALL_PATH, "server", "plugins", "db_cleanup", "script.py"
)
with open(script_path) as fh:
source = fh.read()
expr = "DELETE FROM Resource_History WHERE resDateTime <= datetime('now', '-{str(MAINT_PERF_DAYS)} day')"
assert expr in source, "Resource_History DELETE must use datetime(), not date()"
# ---------------------------------------------------------------------------
# ANALYZE tests
# ---------------------------------------------------------------------------
+84
View File
@@ -0,0 +1,84 @@
"""
Tests for the Resource_History read-side rollup queries (System Info ->
Performance tab, week/month views) defined in server/const.py. Seeds known
per-tick values and asserts SUM() (IO) / AVG() (CPU%/RSS/duration) per hourly
bucket match hand-computed expectations - see resource-usage-history PRD
Design §4.
"""
import sqlite3
from server.const import sql_resource_history_week
def _make_db():
conn = sqlite3.connect(":memory:")
conn.execute("""
CREATE TABLE Resource_History (
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
resDateTime TEXT NOT NULL,
resCpuPercent REAL,
resRssMb REAL,
resIoReadBytes INTEGER,
resIoWriteBytes INTEGER,
resScanDurationMs INTEGER,
resTickFailed INTEGER NOT NULL DEFAULT 0
)
""")
conn.commit()
return conn
def test_bucketed_query_sums_io_and_averages_the_rest():
conn = _make_db()
cur = conn.cursor()
# Two rows in the same hourly bucket, one row in a different bucket -
# both within the 7-day window the "week" query scans.
cur.executemany(
"INSERT INTO Resource_History "
"(resDateTime, resCpuPercent, resRssMb, resIoReadBytes, resIoWriteBytes, "
" resScanDurationMs, resTickFailed) VALUES (?, ?, ?, ?, ?, ?, ?)",
[
("2026-01-01 10:05:00", 10.0, 100.0, 1000, 2000, 500, 0),
("2026-01-01 10:45:00", 20.0, 200.0, 1500, 2500, 700, 1),
("2026-01-01 11:15:00", 30.0, 300.0, 3000, 4000, 900, 0),
],
)
conn.commit()
# sql_resource_history_week filters on datetime('now', '-7 day') - patch
# the fixed timestamps above to "now" so they fall inside the window.
cur.execute("UPDATE Resource_History SET resDateTime = datetime('now', '-1 hour') "
"WHERE resDateTime = '2026-01-01 10:05:00'")
cur.execute("UPDATE Resource_History SET resDateTime = datetime('now', '-1 hour', '+40 minutes') "
"WHERE resDateTime = '2026-01-01 10:45:00'")
cur.execute("UPDATE Resource_History SET resDateTime = datetime('now') "
"WHERE resDateTime = '2026-01-01 11:15:00'")
conn.commit()
rows = cur.execute(sql_resource_history_week).fetchall()
# Expect two buckets (the two rows sharing an hour collapse into one).
assert len(rows) in (1, 2)
total_io_read = sum(r[3] for r in rows)
total_io_write = sum(r[4] for r in rows)
assert total_io_read == 1000 + 1500 + 3000
assert total_io_write == 2000 + 2500 + 4000
# Any bucket containing the tickFailed=1 row must report MAX(resTickFailed)=1.
assert any(r[6] == 1 for r in rows)
def test_bucketed_query_returns_no_rows_outside_window():
conn = _make_db()
cur = conn.cursor()
cur.execute(
"INSERT INTO Resource_History (resDateTime, resCpuPercent) "
"VALUES (datetime('now', '-30 day'), 10.0)"
)
conn.commit()
rows = cur.execute(sql_resource_history_week).fetchall()
assert rows == []
+467
View File
@@ -0,0 +1,467 @@
"""
Tests for server/scan/resource_history.py - tick-scoped resource sampling
and the Resource_History insert, plus the __main__.py schedule-tick
try/except/finally wiring that calls it.
"""
import os
import sys
import sqlite3
import types
from types import SimpleNamespace
from unittest.mock import MagicMock
import psutil
import pytest
from server.scan import resource_history as rh
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from db_test_helpers import make_db # noqa: E402
# ---------------------------------------------------------------------------
# get_process_cpu_times_and_io()
# ---------------------------------------------------------------------------
def test_cpu_times_includes_children(monkeypatch):
"""children_user/children_system must be summed in, not just user/system."""
fake_times = SimpleNamespace(user=1.0, system=2.0, children_user=3.0, children_system=4.0)
fake_io = SimpleNamespace(read_bytes=100, write_bytes=200)
fake_process = MagicMock()
fake_process.cpu_times.return_value = fake_times
fake_process.io_counters.return_value = fake_io
monkeypatch.setattr(rh.psutil, "Process", lambda: fake_process)
cpu_time_s, read_bytes, write_bytes = rh.get_process_cpu_times_and_io()
assert cpu_time_s == pytest.approx(10.0) # 1+2+3+4
assert read_bytes == 100
assert write_bytes == 200
def test_cpu_times_falls_back_to_zeros_on_psutil_error(monkeypatch):
"""psutil.Error/AttributeError from cpu_times()/io_counters() must not raise."""
import psutil as real_psutil
def raising_process():
raise real_psutil.AccessDenied()
monkeypatch.setattr(rh.psutil, "Process", raising_process)
result = rh.get_process_cpu_times_and_io()
assert result == (0.0, 0, 0)
def test_cpu_times_falls_back_on_attribute_error(monkeypatch):
"""A mocked cpu_times() missing children_* fields must fall back to zeros."""
fake_process = MagicMock()
fake_process.cpu_times.return_value = SimpleNamespace(user=1.0, system=1.0) # no children_*
monkeypatch.setattr(rh.psutil, "Process", lambda: fake_process)
result = rh.get_process_cpu_times_and_io()
assert result == (0.0, 0, 0)
# ---------------------------------------------------------------------------
# insert_resource_history()
# ---------------------------------------------------------------------------
class _FakeDB:
"""Minimal DB wrapper exposing .sql like the real DB class."""
def __init__(self, conn):
self.sql = conn.cursor()
self._conn = conn
def open(self):
pass
def initDB(self):
pass
def commitDB(self):
self._conn.commit()
def _make_resource_history_db():
conn = sqlite3.connect(":memory:")
conn.execute("""
CREATE TABLE Resource_History (
"index" INTEGER PRIMARY KEY AUTOINCREMENT,
resDateTime TEXT NOT NULL,
resCpuPercent REAL,
resRssMb REAL,
resIoReadBytes INTEGER,
resIoWriteBytes INTEGER,
resScanDurationMs INTEGER,
resTickFailed INTEGER NOT NULL DEFAULT 0
)
""")
conn.commit()
return _FakeDB(conn)
def test_insert_computes_deltas_exactly():
"""resCpuPercent/resIoReadBytes/resIoWriteBytes must match hand-computed deltas."""
db = _make_resource_history_db()
pre = (10.0, 1000, 2000)
post = (13.0, 1500, 2600) # cpu delta = 3.0s, io read delta = 500, io write delta = 600
rh.insert_resource_history(db, pre, post, duration_ms=6000) # 6s wall time
row = db.sql.execute(
"SELECT resCpuPercent, resIoReadBytes, resIoWriteBytes, resScanDurationMs, resTickFailed "
"FROM Resource_History"
).fetchone()
assert row[0] == pytest.approx((3.0 / 6.0) * 100) # 50%
assert row[1] == 500
assert row[2] == 600
assert row[3] == 6000
assert row[4] == 0
def test_insert_zero_duration_guard():
"""duration_ms <= 0 must yield resCpuPercent == 0.0, not a ZeroDivisionError."""
db = _make_resource_history_db()
rh.insert_resource_history(db, (0.0, 0, 0), (5.0, 10, 10), duration_ms=0)
row = db.sql.execute("SELECT resCpuPercent FROM Resource_History").fetchone()
assert row[0] == 0.0
def test_insert_records_tick_failed_flag():
"""tick_failed=True must be stored as resTickFailed=1."""
db = _make_resource_history_db()
rh.insert_resource_history(db, (0.0, 0, 0), (0.0, 0, 0), duration_ms=1000, tick_failed=True)
row = db.sql.execute("SELECT resTickFailed FROM Resource_History").fetchone()
assert row[0] == 1
def test_insert_failure_is_caught_and_logged():
"""A raising db.sql.execute() must not propagate - the real tick's commit must survive."""
class RaisingDB:
class _Sql:
def execute(self, *a, **kw):
raise sqlite3.OperationalError("locked")
sql = _Sql()
# Must not raise.
rh.insert_resource_history(RaisingDB(), (0.0, 0, 0), (0.0, 0, 0), duration_ms=1000)
def test_rss_sampling_failure_does_not_skip_the_row(monkeypatch):
"""
Regression: a failing psutil RSS read must zero resRssMb, not get caught
by the INSERT's own except block and skip the whole row (CPU/IO were
already computed successfully by that point).
"""
db = _make_resource_history_db()
def raising_process():
raise psutil.AccessDenied()
monkeypatch.setattr(rh.psutil, "Process", raising_process)
rh.insert_resource_history(db, (0.0, 0, 0), (1.0, 100, 200), duration_ms=1000)
row = db.sql.execute(
"SELECT resRssMb, resIoReadBytes, resIoWriteBytes FROM Resource_History"
).fetchone()
assert row is not None, "Row must still be inserted despite the RSS sampling failure"
assert row[0] == 0.0
assert row[1] == 100 # IO values, computed before the RSS read, are unaffected
assert row[2] == 200
# ---------------------------------------------------------------------------
# __main__.py schedule-tick wiring: try/except/finally + MAINT_PERF_DAYS gate
# ---------------------------------------------------------------------------
class _StopTestLoop(Exception):
"""Sentinel raised from the mocked time.sleep() to end the `while True:` loop."""
class _FakeState:
def __init__(self):
self.pause_until = None
self.processScan = True
self.graphQLServerStarted = 1
class _FakePM:
def __init__(self, raise_on_schedule=False):
self.raise_on_schedule = raise_on_schedule
self.calls = []
def check_and_run_user_event(self):
pass
def run_plugin_scripts(self, phase):
self.calls.append(phase)
if phase == "schedule" and self.raise_on_schedule:
raise RuntimeError("boom-in-schedule")
class _FakeNotificationInstance:
"""Stands in for NotificationInstance(db) - create() and the three
processed/clear methods are all called on this same object in __main__.py."""
def create(self, final_json, x):
return SimpleNamespace(HasNotifications=False)
def setAllProcessed(self):
pass
def clearPendingEmailFlag(self):
pass
def clearPluginEvents(self):
pass
class _FakeWorkflowManager:
def __init__(self, db):
pass
def get_new_app_events(self):
return []
def _ensure_stub_module(name, **attrs):
"""Inject a minimal fake module for an optional third-party dependency
that isn't relevant to what's under test here (e.g. json2table, used only
for HTML-formatted notification bodies) but is unconditionally imported
by server/__main__.py's own import chain."""
import importlib
try:
importlib.import_module(name)
except ImportError:
mod = types.ModuleType(name)
for k, v in attrs.items():
setattr(mod, k, v)
sys.modules[name] = mod
@pytest.fixture
def main_mod():
_ensure_stub_module("json2table", convert=lambda *a, **kw: "")
import server.__main__ as m
return m
def _common_patches(monkeypatch, main_mod, db, fake_pm):
monkeypatch.setattr(main_mod, "filePermissions", lambda: None)
monkeypatch.setattr(main_mod, "update_GUI_port", lambda: None)
monkeypatch.setattr(main_mod, "renameSettings", lambda path: None)
monkeypatch.setattr(main_mod, "importConfigs", lambda pm, db, all_plugins: (fake_pm, [], False))
monkeypatch.setattr(main_mod, "update_api", lambda *a, **kw: None)
monkeypatch.setattr(main_mod, "updateState", lambda *a, **kw: _FakeState())
monkeypatch.setattr(main_mod, "DB", lambda: db)
monkeypatch.setattr(main_mod, "process_scan", lambda db: None)
monkeypatch.setattr(main_mod, "get_setting_value", lambda key, default=None: 30 if key == "MAINT_PERF_DAYS" else default)
monkeypatch.setattr(main_mod.conf, "last_scan_run", main_mod.timeNowUTC(as_string=False) - main_mod.datetime.timedelta(minutes=5))
monkeypatch.setattr(main_mod.conf, "DEEP_SLEEP", False, raising=False)
monkeypatch.setattr(main_mod.time, "sleep", lambda *a, **kw: (_ for _ in ()).throw(_StopTestLoop()))
def test_tick_failure_records_row_and_reraises(monkeypatch, main_mod):
"""A raising run_plugin_scripts('schedule') must still insert resTickFailed=1
and re-raise the original exception unchanged."""
conn = make_db()
rh_upgrade_conn = conn.cursor()
from server.db.db_upgrade import ensure_Resource_History
ensure_Resource_History(rh_upgrade_conn)
conn.commit()
db = _FakeDB(conn)
fake_pm = _FakePM(raise_on_schedule=True)
_common_patches(monkeypatch, main_mod, db, fake_pm)
with pytest.raises(RuntimeError, match="boom-in-schedule"):
main_mod.main()
row = conn.execute(
"SELECT resTickFailed FROM Resource_History"
).fetchone()
assert row is not None, "Resource_History row must be written even when the tick raises"
assert row[0] == 1
def test_normal_tick_records_row_with_tick_failed_zero(monkeypatch, main_mod):
"""A normal, non-raising tick must insert a row with resTickFailed=0."""
conn = make_db()
rh_upgrade_conn = conn.cursor()
from server.db.db_upgrade import ensure_Resource_History
ensure_Resource_History(rh_upgrade_conn)
conn.commit()
db = _FakeDB(conn)
fake_pm = _FakePM(raise_on_schedule=False)
_common_patches(monkeypatch, main_mod, db, fake_pm)
monkeypatch.setattr(main_mod, "get_notifications", lambda db: {})
monkeypatch.setattr(main_mod, "NotificationInstance", lambda db: _FakeNotificationInstance())
monkeypatch.setattr(main_mod, "update_devices_names", lambda pm: None)
monkeypatch.setattr(main_mod, "WorkflowManager", _FakeWorkflowManager)
monkeypatch.setattr(main_mod, "UserEventsQueueInstance", lambda: SimpleNamespace(
has_update_devices=lambda: False
))
with pytest.raises(_StopTestLoop):
main_mod.main()
row = conn.execute(
"SELECT resTickFailed FROM Resource_History"
).fetchone()
assert row is not None, "Resource_History row must be written on a normal tick"
assert row[0] == 0
assert "schedule" in fake_pm.calls
def test_maint_perf_days_zero_disables_collection(monkeypatch, main_mod):
"""MAINT_PERF_DAYS == 0 must produce zero new Resource_History rows."""
conn = make_db()
rh_upgrade_conn = conn.cursor()
from server.db.db_upgrade import ensure_Resource_History
ensure_Resource_History(rh_upgrade_conn)
conn.commit()
db = _FakeDB(conn)
fake_pm = _FakePM(raise_on_schedule=False)
_common_patches(monkeypatch, main_mod, db, fake_pm)
# Override the default 30-day fixture value with 0 (disabled).
monkeypatch.setattr(main_mod, "get_setting_value", lambda key, default=None: 0 if key == "MAINT_PERF_DAYS" else default)
monkeypatch.setattr(main_mod, "get_notifications", lambda db: {})
monkeypatch.setattr(main_mod, "NotificationInstance", lambda db: _FakeNotificationInstance())
monkeypatch.setattr(main_mod, "update_devices_names", lambda pm: None)
monkeypatch.setattr(main_mod, "WorkflowManager", _FakeWorkflowManager)
monkeypatch.setattr(main_mod, "UserEventsQueueInstance", lambda: SimpleNamespace(
has_update_devices=lambda: False
))
with pytest.raises(_StopTestLoop):
main_mod.main()
row = conn.execute("SELECT COUNT(*) FROM Resource_History").fetchone()
assert row[0] == 0, "MAINT_PERF_DAYS=0 must produce zero Resource_History rows"
def test_maint_perf_days_non_numeric_disables_instead_of_crashing(monkeypatch, main_mod):
"""
Regression: an empty/corrupted MAINT_PERF_DAYS setting value must disable
the optional feature, not raise ValueError/TypeError and kill the whole
main loop - this int() call sits outside the tick's own try/except/finally,
so it has no other safety net.
"""
conn = make_db()
rh_upgrade_conn = conn.cursor()
from server.db.db_upgrade import ensure_Resource_History
ensure_Resource_History(rh_upgrade_conn)
conn.commit()
db = _FakeDB(conn)
fake_pm = _FakePM(raise_on_schedule=False)
_common_patches(monkeypatch, main_mod, db, fake_pm)
# Simulate a corrupted/empty setting value instead of a real integer.
monkeypatch.setattr(main_mod, "get_setting_value", lambda key, default=None: "" if key == "MAINT_PERF_DAYS" else default)
monkeypatch.setattr(main_mod, "get_notifications", lambda db: {})
monkeypatch.setattr(main_mod, "NotificationInstance", lambda db: _FakeNotificationInstance())
monkeypatch.setattr(main_mod, "update_devices_names", lambda pm: None)
monkeypatch.setattr(main_mod, "WorkflowManager", _FakeWorkflowManager)
monkeypatch.setattr(main_mod, "UserEventsQueueInstance", lambda: SimpleNamespace(
has_update_devices=lambda: False
))
# Must reach the sentinel (i.e. complete the tick normally), not raise ValueError.
with pytest.raises(_StopTestLoop):
main_mod.main()
row = conn.execute("SELECT COUNT(*) FROM Resource_History").fetchone()
assert row[0] == 0, "A non-numeric MAINT_PERF_DAYS must disable collection, not crash"
def test_after_sample_precedes_insert_and_final_commit(monkeypatch, main_mod):
"""Self-measurement boundary: the 'after' sample and insert_resource_history()
must both run before the tick's own later db.commitDB() calls, so the
feature's own write never inflates what it reports."""
conn = make_db()
rh_upgrade_conn = conn.cursor()
from server.db.db_upgrade import ensure_Resource_History
ensure_Resource_History(rh_upgrade_conn)
conn.commit()
db = _FakeDB(conn)
call_order = []
real_commit = db.commitDB
real_get_times = rh.get_process_cpu_times_and_io
real_insert = rh.insert_resource_history
def spy_commit():
call_order.append("commitDB")
return real_commit()
def spy_get_times():
call_order.append("sample")
return real_get_times()
def spy_insert(*args, **kwargs):
call_order.append("insert")
return real_insert(*args, **kwargs)
db.commitDB = spy_commit
fake_pm = _FakePM(raise_on_schedule=False)
_common_patches(monkeypatch, main_mod, db, fake_pm)
monkeypatch.setattr(main_mod, "get_process_cpu_times_and_io", spy_get_times)
monkeypatch.setattr(main_mod, "insert_resource_history", spy_insert)
monkeypatch.setattr(main_mod, "get_notifications", lambda db: {})
monkeypatch.setattr(main_mod, "NotificationInstance", lambda db: _FakeNotificationInstance())
monkeypatch.setattr(main_mod, "update_devices_names", lambda pm: None)
monkeypatch.setattr(main_mod, "WorkflowManager", _FakeWorkflowManager)
monkeypatch.setattr(main_mod, "UserEventsQueueInstance", lambda: SimpleNamespace(
has_update_devices=lambda: False
))
with pytest.raises(_StopTestLoop):
main_mod.main()
insert_index = call_order.index("insert")
# Both samples (pre and post) happen before the insert...
assert call_order[:insert_index].count("sample") == 2
# ...and every commitDB() recorded happens after the insert, not before it.
commits_before_insert = call_order[:insert_index].count("commitDB")
assert commits_before_insert == 0, (
f"insert_resource_history() must run before the tick's own commitDB() calls, "
f"call order was: {call_order}"
)
# ---------------------------------------------------------------------------
# Non-interference regression test (Design §1's correction): the /health
# live gauge (cpu_percent-based) must not affect the tick-scoped sampler's
# own computation, since they use independent Process() objects.
# ---------------------------------------------------------------------------
def test_health_gauge_does_not_affect_tick_scoped_sampling(monkeypatch):
from server.api_server import health_endpoint
pre = rh.get_process_cpu_times_and_io()
# Simulate /health being polled mid-tick, between the two tick-scoped samples.
health_endpoint.get_process_cpu_percent()
post = rh.get_process_cpu_times_and_io()
db = _make_resource_history_db()
rh.insert_resource_history(db, pre, post, duration_ms=1000)
row = db.sql.execute("SELECT resCpuPercent FROM Resource_History").fetchone()
# Purely a non-crash / non-interference check - the /health call must not
# raise or corrupt the tick-scoped computation's independent state.
assert row[0] is not None
@@ -0,0 +1,51 @@
"""
Tests for health_endpoint.py's process-scoped metrics: get_process_cpu_percent()/
get_process_rss_mb() (the /health live gauge, Design §2 of the resource-usage
history feature) and their inclusion in get_health_status().
"""
import psutil
from unittest.mock import MagicMock
from server.api_server import health_endpoint as he
def test_get_process_cpu_percent_returns_value(monkeypatch):
monkeypatch.setattr(he._health_process, "cpu_percent", lambda interval=None: 12.5)
assert he.get_process_cpu_percent() == 12.5
def test_get_process_cpu_percent_falls_back_to_zero_on_error(monkeypatch):
def raising(interval=None):
raise psutil.NoSuchProcess(pid=1)
monkeypatch.setattr(he._health_process, "cpu_percent", raising)
assert he.get_process_cpu_percent() == 0.0
def test_get_process_rss_mb_returns_rounded_mb(monkeypatch):
fake_process = MagicMock()
fake_process.memory_info.return_value = MagicMock(rss=150 * 1024 * 1024)
monkeypatch.setattr(he.psutil, "Process", lambda: fake_process)
assert he.get_process_rss_mb() == 150.0
def test_get_process_rss_mb_falls_back_to_zero_on_error(monkeypatch):
def raising():
raise psutil.AccessDenied()
monkeypatch.setattr(he.psutil, "Process", raising)
assert he.get_process_rss_mb() == 0.0
def test_get_health_status_includes_process_fields(monkeypatch):
"""get_health_status() must expose the two new process metrics alongside
the seven existing host-level ones."""
monkeypatch.setattr(he, "get_process_cpu_percent", lambda: 3.2)
monkeypatch.setattr(he, "get_process_rss_mb", lambda: 142.75)
status = he.get_health_status()
assert status["process_cpu_pct"] == 3.2
assert status["process_rss_mb"] == 142.75
for key in ("db_size_mb", "mem_usage_pct", "load_1m", "storage_pct",
"cpu_temp", "storage_gb", "mem_mb"):
assert key in status