mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
Compare commits
13 Commits
ba3481759b
...
next_relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfd2cf9e20 | ||
|
|
61824abb9f | ||
|
|
33c5548fe1 | ||
|
|
fd41c395ae | ||
|
|
1a980844f0 | ||
|
|
82e018e284 | ||
|
|
e0e1233b1c | ||
|
|
74677f940e | ||
|
|
21a4d20579 | ||
|
|
9634e4e0f7 | ||
|
|
00a47ab5d3 | ||
|
|
59b417705e | ||
|
|
525d082f3d |
12
.github/workflows/docker_prod.yml
vendored
12
.github/workflows/docker_prod.yml
vendored
@@ -32,6 +32,18 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# --- Previous approach Get release version from tag
|
||||
- name: Set up dynamic build ARGs
|
||||
id: getargs
|
||||
run: echo "version=$(cat ./stable/VERSION)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get release version
|
||||
id: get_version_prev
|
||||
run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}"
|
||||
|
||||
- name: Create .VERSION file
|
||||
run: echo "${{ steps.get_version.outputs.version }}" >> .VERSION_PREV
|
||||
|
||||
# --- Get release version from tag
|
||||
- name: Get release version
|
||||
id: get_version
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ nohup.out
|
||||
config/*
|
||||
.ash_history
|
||||
.VERSION
|
||||
.VERSION_PREV
|
||||
config/pialert.conf
|
||||
config/app.conf
|
||||
db/*
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,16 +1,16 @@
|
||||
# The NetAlertX Dockerfile has 3 stages:
|
||||
#
|
||||
# Stage 1. Builder - NetAlertX Requires special tools and packages to build our virtual environment, but
|
||||
# which are not needed in future stages. We build the builder and extract the venv for runner to use as
|
||||
# which are not needed in future stages. We build the builder and extract the venv for runner to use as
|
||||
# a base.
|
||||
#
|
||||
# Stage 2. Runner builds the bare minimum requirements to create an operational NetAlertX. The primary
|
||||
# reason for breaking at this stage is it leaves the system in a proper state for devcontainer operation
|
||||
# This image also provides a break-out point for uses who wish to execute the anti-pattern of using a
|
||||
# This image also provides a break-out point for uses who wish to execute the anti-pattern of using a
|
||||
# docker container as a VM for experimentation and various development patterns.
|
||||
#
|
||||
# Stage 3. Hardened removes root, sudoers, folders, permissions, and locks the system down into a read-only
|
||||
# compatible image. While NetAlertX does require some read-write operations, this image can guarantee the
|
||||
# compatible image. While NetAlertX does require some read-write operations, this image can guarantee the
|
||||
# code pushed out by the project is the only code which will run on the system after each container restart.
|
||||
# It reduces the chance of system hijacking and operates with all modern security protocols in place as is
|
||||
# expected from a security appliance.
|
||||
@@ -29,7 +29,7 @@ COPY requirements.txt /tmp/requirements.txt
|
||||
RUN apk add --no-cache bash shadow python3 python3-dev gcc musl-dev libffi-dev openssl-dev git \
|
||||
&& python -m venv /opt/venv
|
||||
|
||||
# Create virtual environment owned by root, but readable by everyone else. This makes it easy to copy
|
||||
# Create virtual environment owned by root, but readable by everyone else. This makes it easy to copy
|
||||
# into hardened stage without worrying about permissions and keeps image size small. Keeping the commands
|
||||
# together makes for a slightly smaller image size.
|
||||
RUN pip install --no-cache-dir -r /tmp/requirements.txt && \
|
||||
@@ -95,11 +95,11 @@ ENV READ_WRITE_FOLDERS="${NETALERTX_DATA} ${NETALERTX_CONFIG} ${NETALERTX_DB} ${
|
||||
${SYSTEM_SERVICES_ACTIVE_CONFIG}"
|
||||
|
||||
#Python environment
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV VIRTUAL_ENV=/opt/venv
|
||||
ENV VIRTUAL_ENV_BIN=/opt/venv/bin
|
||||
ENV PYTHONPATH=${NETALERTX_APP}:${NETALERTX_SERVER}:${NETALERTX_PLUGINS}:${VIRTUAL_ENV}/lib/python3.12/site-packages
|
||||
ENV PATH="${SYSTEM_SERVICES}:${VIRTUAL_ENV_BIN}:$PATH"
|
||||
ENV PATH="${SYSTEM_SERVICES}:${VIRTUAL_ENV_BIN}:$PATH"
|
||||
|
||||
# App Environment
|
||||
ENV LISTEN_ADDR=0.0.0.0
|
||||
@@ -110,7 +110,7 @@ ENV VENDORSPATH_NEWEST=${SYSTEM_SERVICES_RUN_TMP}/ieee-oui.txt
|
||||
ENV ENVIRONMENT=alpine
|
||||
ENV READ_ONLY_USER=readonly READ_ONLY_GROUP=readonly
|
||||
ENV NETALERTX_USER=netalertx NETALERTX_GROUP=netalertx
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
|
||||
RUN apk add --no-cache bash mtr libbsd zip lsblk tzdata curl arp-scan iproute2 iproute2-ss nmap \
|
||||
@@ -138,6 +138,7 @@ RUN install -d -o ${NETALERTX_USER} -g ${NETALERTX_GROUP} -m 700 ${READ_WRITE_FO
|
||||
|
||||
# Copy version information into the image
|
||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION
|
||||
COPY --chown=${NETALERTX_USER}:${NETALERTX_GROUP} .[V]ERSION ${NETALERTX_APP}/.VERSION_PREV
|
||||
|
||||
# Copy the virtualenv from the builder stage
|
||||
COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
@@ -147,12 +148,12 @@ COPY --from=builder --chown=20212:20212 ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
# This is done after the copy of the venv to ensure the venv is in place
|
||||
# although it may be quicker to do it before the copy, it keeps the image
|
||||
# layers smaller to do it after.
|
||||
RUN if [ -f '.VERSION' ]; then \
|
||||
cp '.VERSION' "${NETALERTX_APP}/.VERSION"; \
|
||||
else \
|
||||
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/.VERSION"; \
|
||||
fi && \
|
||||
chown 20212:20212 "${NETALERTX_APP}/.VERSION" && \
|
||||
RUN for vfile in .VERSION .VERSION_PREV; do \
|
||||
if [ ! -f "${NETALERTX_APP}/${vfile}" ]; then \
|
||||
echo "DEVELOPMENT 00000000" > "${NETALERTX_APP}/${vfile}"; \
|
||||
fi; \
|
||||
chown 20212:20212 "${NETALERTX_APP}/${vfile}"; \
|
||||
done && \
|
||||
apk add --no-cache libcap && \
|
||||
setcap cap_net_raw+ep /bin/busybox && \
|
||||
setcap cap_net_raw,cap_net_admin+eip /usr/bin/nmap && \
|
||||
@@ -180,7 +181,7 @@ ENV UMASK=0077
|
||||
|
||||
# Create readonly user and group with no shell access.
|
||||
# Readonly user marks folders that are created by NetAlertX, but should not be modified.
|
||||
# AI may claim this is stupid, but it's actually least possible permissions as
|
||||
# AI may claim this is stupid, but it's actually least possible permissions as
|
||||
# read-only user cannot login, cannot sudo, has no write permission, and cannot even
|
||||
# read the files it owns. The read-only user is ownership-as-a-lock hardening pattern.
|
||||
RUN addgroup -g 20212 "${READ_ONLY_GROUP}" && \
|
||||
|
||||
@@ -61,8 +61,7 @@ See alternative [docked-compose examples](https://github.com/jokob-sk/NetAlertX/
|
||||
|
||||
| Required | Path | Description |
|
||||
| :------------- | :------------- | :-------------|
|
||||
| ✅ | `:/data/config` | Folder which will contain the `app.conf` & `devices.csv` ([read about devices.csv](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEVICES_BULK_EDITING.md)) files |
|
||||
| ✅ | `:/data/db` | Folder which will contain the `app.db` database file |
|
||||
| ✅ | `:/data` | Folder which will contain the `/db/app.db`, `/config/app.conf` & `/config/devices.csv` ([read about devices.csv](https://github.com/jokob-sk/NetAlertX/blob/main/docs/DEVICES_BULK_EDITING.md)) files |
|
||||
| ✅ | `/etc/localtime:/etc/localtime:ro` | Ensuring the timezone is teh same as on teh server. |
|
||||
| | `:/tmp/log` | Logs folder useful for debugging if you have issues setting up the container |
|
||||
| | `:/tmp/api` | The [API endpoint](https://github.com/jokob-sk/NetAlertX/blob/main/docs/API.md) containing static (but regularly updated) json and other files. Path configurable via `NETALERTX_API` environment variable. |
|
||||
|
||||
@@ -253,11 +253,11 @@ docker run -it --rm --name netalertx --user "0" \
|
||||
ghcr.io/jokob-sk/netalertx:latest
|
||||
```
|
||||
|
||||
..or alternatively execute:
|
||||
...or alternatively execute:
|
||||
|
||||
```bash
|
||||
sudo chown -R 20211:20211 /local_data_dir
|
||||
sudo chmod -R a+rwx /local_data_dir/
|
||||
sudo chmod -R a+rwx /local_data_dir
|
||||
```
|
||||
|
||||
7. Stop the container
|
||||
|
||||
@@ -13,13 +13,13 @@ There is also an in-app Help / FAQ section that should be answering frequently a
|
||||
|
||||
#### 🐳 Docker (Fully supported)
|
||||
|
||||
- The main installation method is as a [docker container - follow these instructions here](./DOCKER_INSTALLATION.md).
|
||||
- The main installation method is as a [docker container - follow these instructions here](./DOCKER_INSTALLATION.md).
|
||||
|
||||
#### 💻 Bare-metal / On-server (Experimental/community supported 🧪)
|
||||
|
||||
- [(Experimental 🧪) On-hardware instructions](./HW_INSTALL.md)
|
||||
- [(Experimental 🧪) On-hardware instructions](./HW_INSTALL.md)
|
||||
|
||||
- Alternative bare-metal install forks:
|
||||
- Alternative bare-metal install forks:
|
||||
- [leiweibau's fork](https://github.com/leiweibau/Pi.Alert/) (maintained)
|
||||
- [pucherot's original code](https://github.com/pucherot/Pi.Alert/) (un-maintained)
|
||||
|
||||
@@ -63,7 +63,6 @@ There is also an in-app Help / FAQ section that should be answering frequently a
|
||||
|
||||
#### ♻ Misc
|
||||
|
||||
- [Version history (legacy)](./VERSIONS_HISTORY.md)
|
||||
- [Reverse proxy (Nginx, Apache, SWAG)](./REVERSE_PROXY.md)
|
||||
- [Installing Updates](./UPDATES.md)
|
||||
- [Setting up Authelia](./AUTHELIA.md) (DRAFT)
|
||||
@@ -80,27 +79,27 @@ There is also an in-app Help / FAQ section that should be answering frequently a
|
||||
- [Frontend development tips](./FRONTEND_DEVELOPMENT.md)
|
||||
- [Webhook secrets](./WEBHOOK_SECRET.md)
|
||||
|
||||
Feel free to suggest or submit new docs via a PR.
|
||||
Feel free to suggest or submit new docs via a PR.
|
||||
|
||||
## 👨💻 Development priorities
|
||||
|
||||
Priorities from highest to lowest:
|
||||
|
||||
* 🔼 Fixing core functionality bugs not solvable with workarounds
|
||||
* 🔵 New core functionality unlocking other opportunities (e.g.: plugins)
|
||||
* 🔵 Refactoring enabling faster implementation of future functionality
|
||||
* 🔵 New core functionality unlocking other opportunities (e.g.: plugins)
|
||||
* 🔵 Refactoring enabling faster implementation of future functionality
|
||||
* 🔽 (low) UI functionality & improvements (PRs welcome 😉)
|
||||
|
||||
Design philosophy: Focus on core functionality and leverage existing apps and tools to make NetAlertX integrate into other workflows.
|
||||
Design philosophy: Focus on core functionality and leverage existing apps and tools to make NetAlertX integrate into other workflows.
|
||||
|
||||
Examples:
|
||||
Examples:
|
||||
|
||||
1. Supporting apprise makes more sense than implementing multiple individual notification gateways
|
||||
2. Implementing regular expression support across settings for validation makes more sense than validating one setting with a specific expression.
|
||||
2. Implementing regular expression support across settings for validation makes more sense than validating one setting with a specific expression.
|
||||
|
||||
UI-specific requests are a low priority as the framework picked by the original developer is not very extensible (and afaik doesn't support components) and has limited mobile support. Also, I argue the value proposition is smaller than working on something else.
|
||||
|
||||
Feel free to submit PRs if interested. try to **keep the PRs small/on-topic** so they are easier to review and approve.
|
||||
Feel free to submit PRs if interested. try to **keep the PRs small/on-topic** so they are easier to review and approve.
|
||||
|
||||
That being said, I'd reconsider if more people and or recurring sponsors file a request 😉.
|
||||
|
||||
@@ -112,8 +111,8 @@ Please be as detailed as possible with **workarounds** you considered and why a
|
||||
|
||||
If you submit a PR please:
|
||||
|
||||
1. Check that your changes are backward compatible with existing installations and with a blank setup.
|
||||
2. Existing features should always be preserved.
|
||||
1. Check that your changes are backward compatible with existing installations and with a blank setup.
|
||||
2. Existing features should always be preserved.
|
||||
3. Keep the PR small, on-topic and don't change code that is not necessary for the PR to work
|
||||
4. New features code should ideally be re-usable for different purposes, not for a very narrow use case.
|
||||
5. New functionality should ideally be implemented via the Plugins system, if possible.
|
||||
@@ -131,13 +130,13 @@ Suggested test cases:
|
||||
Some additional context:
|
||||
|
||||
* Permanent settings/config is stored in the `app.conf` file
|
||||
* Currently temporary (session?) settings are stored in the `Parameters` DB table as key-value pairs. This table is wiped during a container rebuild/restart and its values are re-initialized from cookies/session data from the browser.
|
||||
* Currently temporary (session?) settings are stored in the `Parameters` DB table as key-value pairs. This table is wiped during a container rebuild/restart and its values are re-initialized from cookies/session data from the browser.
|
||||
|
||||
## 🐛 Submitting an issue or bug
|
||||
|
||||
Before submitting a new issue please spend a couple of minutes on research:
|
||||
|
||||
* Check [🛑 Common issues](./DEBUG_TIPS.md#common-issues)
|
||||
* Check [🛑 Common issues](./DEBUG_TIPS.md#common-issues)
|
||||
* Check [💡 Closed issues](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed) if a similar issue was solved in the past.
|
||||
* When submitting an issue ❗[enable debug](./DEBUG_TIPS.md)❗
|
||||
|
||||
|
||||
@@ -378,7 +378,7 @@ function localizeTimestamp(input) {
|
||||
let tz = getSetting("TIMEZONE") || 'Europe/Berlin';
|
||||
input = String(input || '').trim();
|
||||
|
||||
// ✅ 1. Unix timestamps (10 or 13 digits)
|
||||
// 1. Unix timestamps (10 or 13 digits)
|
||||
if (/^\d+$/.test(input)) {
|
||||
const ms = input.length === 10 ? parseInt(input, 10) * 1000 : parseInt(input, 10);
|
||||
return new Intl.DateTimeFormat('default', {
|
||||
@@ -389,39 +389,53 @@ function localizeTimestamp(input) {
|
||||
}).format(new Date(ms));
|
||||
}
|
||||
|
||||
// ✅ 2. European DD/MM/YYYY
|
||||
// 2. European DD/MM/YYYY
|
||||
let match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?(.*)$/);
|
||||
if (match) {
|
||||
let [ , d, m, y, t = "00:00:00", tzPart = "" ] = match;
|
||||
let [, d, m, y, t = "00:00:00", tzPart = ""] = match;
|
||||
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5?t+":00":t}${tzPart}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// ✅ 3. US MM/DD/YYYY
|
||||
// 3. US MM/DD/YYYY
|
||||
match = input.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:[ ,]+(\d{1,2}:\d{2}(?::\d{2})?))?(.*)$/);
|
||||
if (match) {
|
||||
let [ , m, d, y, t = "00:00:00", tzPart = "" ] = match;
|
||||
let [, m, d, y, t = "00:00:00", tzPart = ""] = match;
|
||||
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${t.length===5?t+":00":t}${tzPart}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// ✅ 4. ISO-style (with T, Z, offsets)
|
||||
match = input.match(/^(\d{4}-\d{1,2}-\d{1,2})[ T](\d{1,2}:\d{2}(?::\d{2})?)(Z|[+-]\d{2}:?\d{2})?$/);
|
||||
// 4. ISO YYYY-MM-DD with optional Z/+offset
|
||||
match = input.match(/^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])[ T](\d{1,2}:\d{2}(?::\d{2})?)(Z|[+-]\d{2}:?\d{2})?$/);
|
||||
if (match) {
|
||||
let [ , ymd, time, offset = "" ] = match;
|
||||
// normalize to YYYY-MM-DD
|
||||
let [y, m, d] = ymd.split('-').map(x => x.padStart(2,'0'));
|
||||
let [, y, m, d, time, offset = ""] = match;
|
||||
const iso = `${y}-${m}-${d}T${time.length===5?time+":00":time}${offset}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// ✅ 5. RFC2822 / "25 Aug 2025 13:45:22 +0200"
|
||||
// 5. RFC2822 / "25 Aug 2025 13:45:22 +0200"
|
||||
match = input.match(/^\d{1,2} [A-Za-z]{3,} \d{4}/);
|
||||
if (match) {
|
||||
return formatSafe(input, tz);
|
||||
}
|
||||
|
||||
// ✅ 6. Fallback (whatever Date() can parse)
|
||||
// 6. DD-MM-YYYY with optional time
|
||||
match = input.match(/^(\d{1,2})-(\d{1,2})-(\d{4})(?:[ T](\d{1,2}:\d{2}(?::\d{2})?))?$/);
|
||||
if (match) {
|
||||
let [, d, m, y, time = "00:00:00"] = match;
|
||||
const iso = `${y}-${m.padStart(2,'0')}-${d.padStart(2,'0')}T${time.length===5?time+":00":time}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// 7. Strict YYYY-DD-MM with optional time
|
||||
match = input.match(/^(\d{4})-(0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])(?:[ T](\d{1,2}:\d{2}(?::\d{2})?))?$/);
|
||||
if (match) {
|
||||
let [, y, d, m, time = "00:00:00"] = match;
|
||||
const iso = `${y}-${m}-${d}T${time.length === 5 ? time + ":00" : time}`;
|
||||
return formatSafe(iso, tz);
|
||||
}
|
||||
|
||||
// 8. Fallback
|
||||
return formatSafe(input, tz);
|
||||
|
||||
function formatSafe(str, tz) {
|
||||
@@ -440,6 +454,7 @@ function localizeTimestamp(input) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
/**
|
||||
* Replaces double quotes within single-quoted strings, then converts all single quotes to double quotes,
|
||||
@@ -1629,7 +1644,7 @@ async function executeOnce() {
|
||||
await cacheSettings();
|
||||
await cacheStrings();
|
||||
|
||||
console.log("✅ All AJAX callbacks have completed");
|
||||
console.log("All AJAX callbacks have completed");
|
||||
onAllCallsComplete();
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<?php
|
||||
require 'php/templates/header.php';
|
||||
require 'php/templates/modals.php';
|
||||
require 'php/templates/modals.php';
|
||||
?>
|
||||
|
||||
|
||||
<script>
|
||||
// show spinning icon
|
||||
showSpinner()
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<!-- Page ------------------------------------------------------------------ -->
|
||||
<div class="content-wrapper">
|
||||
<span class="helpIcon">
|
||||
<span class="helpIcon">
|
||||
<a target="_blank" href="https://github.com/jokob-sk/NetAlertX/blob/main/docs/NETWORK_TREE.md">
|
||||
<i class="fa fa-circle-question"></i>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<div id="toggleFilters" class="">
|
||||
<div id="toggleFilters" class="">
|
||||
<div class="checkbox icheck col-xs-12">
|
||||
<label>
|
||||
<input type="checkbox" name="showOffline" checked>
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<?= lang('Network_ShowOffline');?>
|
||||
<span id="showOfflineNumber">
|
||||
<!-- placeholder -->
|
||||
@@ -31,14 +31,14 @@
|
||||
<div class="checkbox icheck col-xs-12">
|
||||
<label>
|
||||
<input type="checkbox" name="showArchived">
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<?= lang('Network_ShowArchived');?>
|
||||
<div style="margin-left: 10px; display: inline-block; vertical-align: top;">
|
||||
<?= lang('Network_ShowArchived');?>
|
||||
<span id="showArchivedNumber">
|
||||
<!-- placeholder -->
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="networkTree" class="drag">
|
||||
@@ -55,8 +55,8 @@
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<!-- Placeholder -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<section id="unassigned-devices-wrapper">
|
||||
<!-- Placeholder -->
|
||||
</section>
|
||||
@@ -69,7 +69,7 @@
|
||||
require 'php/templates/footer.php';
|
||||
?>
|
||||
|
||||
<script src="lib/treeviz/bundle.js"></script>
|
||||
<script src="lib/treeviz/bundle.js"></script>
|
||||
|
||||
<script defer>
|
||||
|
||||
@@ -78,12 +78,12 @@
|
||||
|
||||
// Create Top level tabs (List of network devices), explanation of the terminology below:
|
||||
//
|
||||
// Switch 1 (node)
|
||||
// Switch 1 (node)
|
||||
// /(p1) \ (p2) <----- port numbers
|
||||
// / \
|
||||
// Smart TV (leaf) Switch 2 (node (for the PC) and leaf (for Switch 1))
|
||||
// \
|
||||
// PC (leaf) <------- leafs are not included in this SQL query
|
||||
// PC (leaf) <------- leafs are not included in this SQL query
|
||||
const rawSql = `
|
||||
SELECT node_name, node_mac, online, node_type, node_ports_count, parent_mac, node_icon, node_alert
|
||||
FROM (
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
const portLabel = node.node_ports_count ? ` (${node.node_ports_count})` : '';
|
||||
const icon = atob(node.node_icon);
|
||||
const id = node.node_mac.replace(/:/g, '_');
|
||||
const id = node.node_mac.replace(/:/g, '_');
|
||||
|
||||
html += `
|
||||
<li class="networkNodeTabHeaders ${i === 0 ? 'active' : ''}">
|
||||
@@ -137,13 +137,13 @@
|
||||
renderNetworkTabContent(nodes);
|
||||
|
||||
// init selected (first) tab
|
||||
initTab();
|
||||
initTab();
|
||||
|
||||
// init selected node highlighting
|
||||
// init selected node highlighting
|
||||
initSelectedNodeHighlighting()
|
||||
|
||||
// Register events on tab change
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
initSelectedNodeHighlighting()
|
||||
});
|
||||
}
|
||||
@@ -205,10 +205,10 @@
|
||||
<hr/>
|
||||
<div class="box box-aqua box-body" id="connected">
|
||||
<h5>
|
||||
<i class="fa fa-sitemap fa-rotate-270"></i>
|
||||
<i class="fa fa-sitemap fa-rotate-270"></i>
|
||||
${getString('Network_Connected')}
|
||||
</h5>
|
||||
|
||||
|
||||
<div id="leafs_${id}" class="table-responsive"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,9 +234,9 @@
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$container.html(wrapperHtml);
|
||||
|
||||
|
||||
const $table = $(`#${tableId}`);
|
||||
|
||||
const columns = [
|
||||
@@ -298,7 +298,7 @@
|
||||
title: getString('Device_TableHead_Vendor'),
|
||||
data: 'devVendor',
|
||||
width: '20%'
|
||||
}
|
||||
}
|
||||
].filter(Boolean);
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@
|
||||
function loadConnectedDevices(node_mac) {
|
||||
const sql = `
|
||||
SELECT devName, devMac, devLastIP, devVendor, devPresentLastScan, devAlertDown, devParentPort,
|
||||
CASE
|
||||
CASE
|
||||
WHEN devIsNew = 1 THEN 'New'
|
||||
WHEN devPresentLastScan = 1 THEN 'On-line'
|
||||
WHEN devPresentLastScan = 0 AND devAlertDown != 0 THEN 'Down'
|
||||
@@ -371,7 +371,7 @@
|
||||
|
||||
const wrapperHtml = `
|
||||
<table class="table table-bordered table-striped node-leafs-table " id="table_leafs_${id}" data-node-mac="${node_mac}">
|
||||
|
||||
|
||||
</table>`;
|
||||
|
||||
loadDeviceTable({
|
||||
@@ -414,12 +414,12 @@
|
||||
$.get(apiUrl, function (data) {
|
||||
|
||||
console.log(data);
|
||||
|
||||
|
||||
const parsed = JSON.parse(data);
|
||||
const allDevices = parsed;
|
||||
|
||||
console.log(allDevices);
|
||||
|
||||
|
||||
|
||||
if (!allDevices || allDevices.length === 0) {
|
||||
showModalOK(getString('Gen_Warning'), getString('Network_NoDevices'));
|
||||
@@ -439,7 +439,7 @@
|
||||
{
|
||||
$('#showArchivedNumber').text(`(${archivedCount})`);
|
||||
}
|
||||
|
||||
|
||||
if(offlineCount > 0)
|
||||
{
|
||||
$('#showOfflineNumber').text(`(${offlineCount})`);
|
||||
@@ -501,7 +501,7 @@ var visibleNodesCount = 0;
|
||||
var parentNodesCount = 0;
|
||||
var hiddenMacs = []; // hidden children
|
||||
var hiddenChildren = [];
|
||||
var deviceListGlobal = null;
|
||||
var deviceListGlobal = null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Recursively get children nodes and build a tree
|
||||
@@ -521,13 +521,17 @@ function getChildren(node, list, path, visited = [])
|
||||
|
||||
// Loop through all items to find children of the current node
|
||||
for (var i in list) {
|
||||
if (list[i].devParentMAC.toLowerCase() == node.devMac.toLowerCase() && !hiddenMacs.includes(list[i].devParentMAC)) {
|
||||
const item = list[i];
|
||||
const parentMac = item.devParentMAC || ""; // null-safe
|
||||
const nodeMac = node.devMac || ""; // null-safe
|
||||
|
||||
visibleNodesCount++;
|
||||
if (parentMac != "" && parentMac.toLowerCase() == nodeMac.toLowerCase() && !hiddenMacs.includes(parentMac)) {
|
||||
|
||||
// Process children recursively, passing a copy of the visited list
|
||||
children.push(getChildren(list[i], list, path + ((path == "") ? "" : '|') + list[i].devParentMAC, visited));
|
||||
}
|
||||
visibleNodesCount++;
|
||||
|
||||
// Process children recursively, passing a copy of the visited list
|
||||
children.push(getChildren(list[i], list, path + ((path == "") ? "" : '|') + parentMac, visited));
|
||||
}
|
||||
}
|
||||
|
||||
// Track leaf and parent node counts
|
||||
@@ -537,7 +541,7 @@ function getChildren(node, list, path, visited = [])
|
||||
parentNodesCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
return {
|
||||
name: node.devName,
|
||||
path: path,
|
||||
mac: node.devMac,
|
||||
@@ -562,19 +566,32 @@ function getChildren(node, list, path, visited = [])
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
function getHierarchy()
|
||||
{
|
||||
{
|
||||
let internetNode = null;
|
||||
|
||||
for(i in deviceListGlobal)
|
||||
{
|
||||
{
|
||||
if(deviceListGlobal[i].devMac == 'Internet')
|
||||
{
|
||||
return (getChildren(deviceListGlobal[i], deviceListGlobal, ''))
|
||||
{
|
||||
internetNode = deviceListGlobal[i];
|
||||
|
||||
return (getChildren(internetNode, deviceListGlobal, ''))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!internetNode) {
|
||||
showModalOk(
|
||||
getString('Network_Configuration_Error'),
|
||||
getString('Network_Root_Not_Configured')
|
||||
);
|
||||
console.error("getHierarchy(): Internet node not found");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
function toggleSubTree(parentMac, treePath)
|
||||
{
|
||||
@@ -593,33 +610,33 @@ function toggleSubTree(parentMac, treePath)
|
||||
myTree.refresh(updatedTree);
|
||||
|
||||
// re-attach any onclick events
|
||||
attachTreeEvents();
|
||||
attachTreeEvents();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
function attachTreeEvents()
|
||||
{
|
||||
// toggle subtree functionality
|
||||
$("div[data-mytreemac]").each(function(){
|
||||
$(this).attr('onclick', 'toggleSubTree("'+$(this).attr('data-mytreemac')+'","'+ $(this).attr('data-mytreepath')+'")')
|
||||
});
|
||||
$(this).attr('onclick', 'toggleSubTree("'+$(this).attr('data-mytreemac')+'","'+ $(this).attr('data-mytreepath')+'")')
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handle network node click - select correct tab in the bottom table
|
||||
function handleNodeClick(el)
|
||||
{
|
||||
{
|
||||
|
||||
isNetworkDevice = $(el).data("devisnetworknodedynamic") == 1;
|
||||
targetTabMAC = ""
|
||||
thisDevMac= $(el).data("mac");
|
||||
|
||||
thisDevMac= $(el).data("mac");
|
||||
|
||||
if (isNetworkDevice == false)
|
||||
{
|
||||
targetTabMAC = $(el).data("parentmac");
|
||||
targetTabMAC = $(el).data("parentmac");
|
||||
} else
|
||||
{
|
||||
targetTabMAC = thisDevMac;
|
||||
targetTabMAC = thisDevMac;
|
||||
}
|
||||
|
||||
var targetTab = $(`a[data-mytabmac="${targetTabMAC}"]`);
|
||||
@@ -628,8 +645,8 @@ function handleNodeClick(el)
|
||||
// Simulate a click event on the target tab
|
||||
targetTab.click();
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (isNetworkDevice) {
|
||||
// Smooth scroll to the tab content
|
||||
@@ -639,7 +656,7 @@ function handleNodeClick(el)
|
||||
} else {
|
||||
$("tr.selected").removeClass("selected");
|
||||
$(`tr[data-mac="${thisDevMac}"]`).addClass("selected");
|
||||
|
||||
|
||||
const tableId = "table_leafs_" + targetTabMAC.replace(/:/g, '_');
|
||||
const $table = $(`#${tableId}`).DataTable();
|
||||
|
||||
@@ -669,10 +686,8 @@ function handleNodeClick(el)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
var myTree;
|
||||
|
||||
|
||||
var emSize;
|
||||
var nodeHeight;
|
||||
// var sizeCoefficient = 1.4
|
||||
@@ -689,140 +704,139 @@ function emToPx(em, element) {
|
||||
|
||||
function initTree(myHierarchy)
|
||||
{
|
||||
// calculate the drawing area based on teh tree width and available screen size
|
||||
|
||||
let baseFontSize = parseFloat($('html').css('font-size'));
|
||||
let treeAreaHeight = ($(window).height() - 155); ;
|
||||
// calculate the font size of the leaf nodes to fit everything into the tree area
|
||||
leafNodesCount == 0 ? 1 : leafNodesCount;
|
||||
|
||||
emSize = pxToEm((treeAreaHeight/(leafNodesCount)).toFixed(2));
|
||||
|
||||
let screenWidthEm = pxToEm($('.networkTable').width()-15);
|
||||
|
||||
// init the drawing area size
|
||||
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${emToPx(screenWidthEm)}px`)
|
||||
|
||||
if(myHierarchy.type == "")
|
||||
if(myHierarchy && myHierarchy.type !== "")
|
||||
{
|
||||
showModalOk(getString('Network_Configuration_Error'), getString('Network_Root_Not_Configured'))
|
||||
|
||||
return;
|
||||
// calculate the drawing area based on the tree width and available screen size
|
||||
let baseFontSize = parseFloat($('html').css('font-size'));
|
||||
let treeAreaHeight = ($(window).height() - 155); ;
|
||||
|
||||
// calculate the font size of the leaf nodes to fit everything into the tree area
|
||||
leafNodesCount == 0 ? 1 : leafNodesCount;
|
||||
|
||||
emSize = pxToEm((treeAreaHeight/(leafNodesCount)).toFixed(2));
|
||||
|
||||
let screenWidthEm = pxToEm($('.networkTable').width()-15);
|
||||
|
||||
// init the drawing area size
|
||||
$("#networkTree").attr('style', `height:${treeAreaHeight}px; width:${emToPx(screenWidthEm)}px`)
|
||||
|
||||
// handle canvas and node size if only a few nodes
|
||||
emSize > 1 ? emSize = 1 : emSize = emSize;
|
||||
|
||||
let nodeHeightPx = emToPx(emSize*1);
|
||||
let nodeWidthPx = emToPx(screenWidthEm / (parentNodesCount));
|
||||
|
||||
// handle if only a few nodes
|
||||
nodeWidthPx > 160 ? nodeWidthPx = 160 : nodeWidthPx = nodeWidthPx;
|
||||
|
||||
console.log(Treeviz);
|
||||
|
||||
myTree = Treeviz.create({
|
||||
htmlId: "networkTree",
|
||||
renderNode: nodeData => {
|
||||
|
||||
(!emptyArr.includes(nodeData.data.port )) ? port = nodeData.data.port : port = "";
|
||||
|
||||
(port == "" || port == 0 || port == 'None' ) ? portBckgIcon = `<i class="fa fa-wifi"></i>` : portBckgIcon = `<i class="fa fa-ethernet"></i>`;
|
||||
|
||||
portHtml = (port == "" || port == 0 || port == 'None' ) ? "   " : port;
|
||||
|
||||
// Build HTML for individual nodes in the network diagram
|
||||
deviceIcon = (!emptyArr.includes(nodeData.data.icon )) ?
|
||||
`<div class="netIcon">
|
||||
${atob(nodeData.data.icon)}
|
||||
</div>` : "";
|
||||
devicePort = `<div class="netPort"
|
||||
style="width:${emSize}em;height:${emSize}em">
|
||||
${portHtml}</div>
|
||||
<div class="portBckgIcon"
|
||||
style="margin-left:-${emSize*0.7}em;">
|
||||
${portBckgIcon}
|
||||
</div>`;
|
||||
collapseExpandIcon = nodeData.data.hiddenChildren ?
|
||||
"square-plus" : "square-minus";
|
||||
|
||||
// generate +/- icon if node has children nodes
|
||||
collapseExpandHtml = nodeData.data.hasChildren ?
|
||||
`<div class="netCollapse"
|
||||
style="font-size:${nodeHeightPx/2}px;top:${Math.floor(nodeHeightPx / 4)}px"
|
||||
data-mytreepath="${nodeData.data.path}"
|
||||
data-mytreemac="${nodeData.data.mac}">
|
||||
<i class="fa fa-${collapseExpandIcon} pointer"></i>
|
||||
</div>` : "";
|
||||
|
||||
selectedNodeMac = $(".nav-tabs-custom .active a").attr('data-mytabmac')
|
||||
|
||||
highlightedCss = nodeData.data.mac == selectedNodeMac ?
|
||||
" highlightedNode " : "";
|
||||
cssNodeType = nodeData.data.devIsNetworkNodeDynamic ?
|
||||
" node-network-device " : " node-standard-device ";
|
||||
|
||||
networkHardwareIcon = nodeData.data.devIsNetworkNodeDynamic ? `<span class="network-hw-icon">
|
||||
<i class="fa-solid fa-hard-drive"></i>
|
||||
</span>` : "";
|
||||
|
||||
const badgeConf = getStatusBadgeParts(nodeData.data.presentLastScan, nodeData.data.alertDown, nodeData.data.mac, statusText = '')
|
||||
|
||||
return result = `<div
|
||||
class="node-inner hover-node-info box pointer ${highlightedCss} ${cssNodeType}"
|
||||
style="height:${nodeHeightPx}px;font-size:${nodeHeightPx-5}px;"
|
||||
onclick="handleNodeClick(this)"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-parentMac="${nodeData.data.parentMac}"
|
||||
data-name="${nodeData.data.name}"
|
||||
data-ip="${nodeData.data.ip}"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-vendor="${nodeData.data.vendor}"
|
||||
data-type="${nodeData.data.type}"
|
||||
data-devIsNetworkNodeDynamic="${nodeData.data.devIsNetworkNodeDynamic}"
|
||||
data-lastseen="${nodeData.data.lastseen}"
|
||||
data-firstseen="${nodeData.data.firstseen}"
|
||||
data-relationship="${nodeData.data.relType}"
|
||||
data-status="${nodeData.data.status}"
|
||||
data-present="${nodeData.data.presentLastScan}"
|
||||
data-alert="${nodeData.data.alertDown}"
|
||||
data-icon="${nodeData.data.icon}"
|
||||
>
|
||||
<div class="netNodeText">
|
||||
<strong><span>${devicePort} <span class="${badgeConf.cssText}">${deviceIcon}</span></span>
|
||||
<span class="spanNetworkTree anonymizeDev" style="width:${nodeWidthPx-50}px">${nodeData.data.name}</span>
|
||||
${networkHardwareIcon}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
${collapseExpandHtml}`;
|
||||
},
|
||||
mainAxisNodeSpacing: 'auto',
|
||||
// secondaryAxisNodeSpacing: 0.3,
|
||||
nodeHeight: nodeHeightPx,
|
||||
nodeWidth: nodeWidthPx,
|
||||
marginTop: '5',
|
||||
isHorizontal : true,
|
||||
hasZoom: true,
|
||||
hasPan: true,
|
||||
marginLeft: '10',
|
||||
marginRight: '10',
|
||||
idKey: "mac",
|
||||
hasFlatData: false,
|
||||
relationnalField: "children",
|
||||
linkWidth: (nodeData) => 2,
|
||||
linkColor: (nodeData) => {
|
||||
relConf = getRelationshipConf(nodeData.data.relType)
|
||||
return relConf.color;
|
||||
}
|
||||
// onNodeClick: (nodeData) => handleNodeClick(nodeData),
|
||||
});
|
||||
|
||||
console.log(deviceListGlobal);
|
||||
myTree.refresh(myHierarchy);
|
||||
|
||||
// hide spinning icon
|
||||
hideSpinner()
|
||||
} else
|
||||
{
|
||||
console.error("getHierarchy() not returning expected result");
|
||||
}
|
||||
|
||||
// handle canvas and node size if only a few nodes
|
||||
emSize > 1 ? emSize = 1 : emSize = emSize;
|
||||
|
||||
let nodeHeightPx = emToPx(emSize*1);
|
||||
let nodeWidthPx = emToPx(screenWidthEm / (parentNodesCount));
|
||||
|
||||
// handle if only a few nodes
|
||||
nodeWidthPx > 160 ? nodeWidthPx = 160 : nodeWidthPx = nodeWidthPx;
|
||||
|
||||
console.log(Treeviz);
|
||||
|
||||
myTree = Treeviz.create({
|
||||
htmlId: "networkTree",
|
||||
renderNode: nodeData => {
|
||||
|
||||
(!emptyArr.includes(nodeData.data.port )) ? port = nodeData.data.port : port = "";
|
||||
|
||||
(port == "" || port == 0 || port == 'None' ) ? portBckgIcon = `<i class="fa fa-wifi"></i>` : portBckgIcon = `<i class="fa fa-ethernet"></i>`;
|
||||
|
||||
portHtml = (port == "" || port == 0 || port == 'None' ) ? "   " : port;
|
||||
|
||||
// Build HTML for individual nodes in the network diagram
|
||||
deviceIcon = (!emptyArr.includes(nodeData.data.icon )) ?
|
||||
`<div class="netIcon">
|
||||
${atob(nodeData.data.icon)}
|
||||
</div>` : "";
|
||||
devicePort = `<div class="netPort"
|
||||
style="width:${emSize}em;height:${emSize}em">
|
||||
${portHtml}</div>
|
||||
<div class="portBckgIcon"
|
||||
style="margin-left:-${emSize*0.7}em;">
|
||||
${portBckgIcon}
|
||||
</div>`;
|
||||
collapseExpandIcon = nodeData.data.hiddenChildren ?
|
||||
"square-plus" : "square-minus";
|
||||
|
||||
// generate +/- icon if node has children nodes
|
||||
collapseExpandHtml = nodeData.data.hasChildren ?
|
||||
`<div class="netCollapse"
|
||||
style="font-size:${nodeHeightPx/2}px;top:${Math.floor(nodeHeightPx / 4)}px"
|
||||
data-mytreepath="${nodeData.data.path}"
|
||||
data-mytreemac="${nodeData.data.mac}">
|
||||
<i class="fa fa-${collapseExpandIcon} pointer"></i>
|
||||
</div>` : "";
|
||||
|
||||
selectedNodeMac = $(".nav-tabs-custom .active a").attr('data-mytabmac')
|
||||
|
||||
highlightedCss = nodeData.data.mac == selectedNodeMac ?
|
||||
" highlightedNode " : "";
|
||||
cssNodeType = nodeData.data.devIsNetworkNodeDynamic ?
|
||||
" node-network-device " : " node-standard-device ";
|
||||
|
||||
networkHardwareIcon = nodeData.data.devIsNetworkNodeDynamic ? `<span class="network-hw-icon">
|
||||
<i class="fa-solid fa-hard-drive"></i>
|
||||
</span>` : "";
|
||||
|
||||
const badgeConf = getStatusBadgeParts(nodeData.data.presentLastScan, nodeData.data.alertDown, nodeData.data.mac, statusText = '')
|
||||
|
||||
return result = `<div
|
||||
class="node-inner hover-node-info box pointer ${highlightedCss} ${cssNodeType}"
|
||||
style="height:${nodeHeightPx}px;font-size:${nodeHeightPx-5}px;"
|
||||
onclick="handleNodeClick(this)"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-parentMac="${nodeData.data.parentMac}"
|
||||
data-name="${nodeData.data.name}"
|
||||
data-ip="${nodeData.data.ip}"
|
||||
data-mac="${nodeData.data.mac}"
|
||||
data-vendor="${nodeData.data.vendor}"
|
||||
data-type="${nodeData.data.type}"
|
||||
data-devIsNetworkNodeDynamic="${nodeData.data.devIsNetworkNodeDynamic}"
|
||||
data-lastseen="${nodeData.data.lastseen}"
|
||||
data-firstseen="${nodeData.data.firstseen}"
|
||||
data-relationship="${nodeData.data.relType}"
|
||||
data-status="${nodeData.data.status}"
|
||||
data-present="${nodeData.data.presentLastScan}"
|
||||
data-alert="${nodeData.data.alertDown}"
|
||||
data-icon="${nodeData.data.icon}"
|
||||
>
|
||||
<div class="netNodeText">
|
||||
<strong><span>${devicePort} <span class="${badgeConf.cssText}">${deviceIcon}</span></span>
|
||||
<span class="spanNetworkTree anonymizeDev" style="width:${nodeWidthPx-50}px">${nodeData.data.name}</span>
|
||||
${networkHardwareIcon}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
${collapseExpandHtml}`;
|
||||
},
|
||||
mainAxisNodeSpacing: 'auto',
|
||||
// secondaryAxisNodeSpacing: 0.3,
|
||||
nodeHeight: nodeHeightPx,
|
||||
nodeWidth: nodeWidthPx,
|
||||
marginTop: '5',
|
||||
isHorizontal : true,
|
||||
hasZoom: true,
|
||||
hasPan: true,
|
||||
marginLeft: '10',
|
||||
marginRight: '10',
|
||||
idKey: "mac",
|
||||
hasFlatData: false,
|
||||
relationnalField: "children",
|
||||
linkWidth: (nodeData) => 2,
|
||||
linkColor: (nodeData) => {
|
||||
relConf = getRelationshipConf(nodeData.data.relType)
|
||||
return relConf.color;
|
||||
}
|
||||
// onNodeClick: (nodeData) => handleNodeClick(nodeData),
|
||||
});
|
||||
|
||||
console.log(deviceListGlobal);
|
||||
myTree.refresh(myHierarchy);
|
||||
|
||||
// hide spinning icon
|
||||
hideSpinner()
|
||||
}
|
||||
|
||||
|
||||
@@ -839,11 +853,11 @@ function initTab()
|
||||
selectedTab = "Internet_id"
|
||||
|
||||
// the #target from the url
|
||||
target = getQueryString('mac')
|
||||
target = getQueryString('mac')
|
||||
|
||||
// update cookie if target specified
|
||||
if(target != "")
|
||||
{
|
||||
{
|
||||
setCache(key, target.replaceAll(":","_")+'_id') // _id is added so it doesn't conflict with AdminLTE tab behavior
|
||||
}
|
||||
|
||||
@@ -860,12 +874,12 @@ function initTab()
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
setCache(key, $(e.target).attr('id'))
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
function initSelectedNodeHighlighting()
|
||||
{
|
||||
{
|
||||
|
||||
var currentNodeMac = $(".networkNodeTabHeaders.active a").data("mytabmac");
|
||||
|
||||
@@ -882,7 +896,7 @@ function initSelectedNodeHighlighting()
|
||||
newSelNode = $("#networkTree div[data-mac='"+currentNodeMac+"']")[0]
|
||||
|
||||
console.log(newSelNode)
|
||||
|
||||
|
||||
$(newSelNode).attr('class', $(newSelNode).attr('class') + ' highlightedNode')
|
||||
}
|
||||
|
||||
@@ -913,7 +927,7 @@ function updateLeaf(leafMac, action) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// showing icons or device names in tabs depending on available screen size
|
||||
// showing icons or device names in tabs depending on available screen size
|
||||
function checkTabsOverflow() {
|
||||
const $ul = $('.nav-tabs');
|
||||
const $lis = $ul.find('li');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
//------------------------------------------------------------------------------
|
||||
// NetAlertX
|
||||
// Open Source Network Guard / WIFI & LAN intrusion detector
|
||||
// Open Source Network Guard / WIFI & LAN intrusion detector
|
||||
//
|
||||
// util.php - Front module. Server side. Common generic functions
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -22,8 +22,8 @@ $ACTION = "";
|
||||
|
||||
// init request params
|
||||
if(array_key_exists('function', $_REQUEST) != FALSE)
|
||||
{
|
||||
$FUNCTION = $_REQUEST['function'];
|
||||
{
|
||||
$FUNCTION = $_REQUEST['function'];
|
||||
}
|
||||
if(array_key_exists('settings', $_REQUEST) != FALSE)
|
||||
{
|
||||
@@ -33,13 +33,13 @@ if(array_key_exists('settings', $_REQUEST) != FALSE)
|
||||
|
||||
// call functions based on requested params
|
||||
switch ($FUNCTION) {
|
||||
|
||||
|
||||
case 'savesettings':
|
||||
|
||||
|
||||
saveSettings();
|
||||
break;
|
||||
|
||||
case 'cleanLog':
|
||||
case 'cleanLog':
|
||||
|
||||
cleanLog($SETTINGS);
|
||||
break;
|
||||
@@ -66,7 +66,7 @@ switch ($FUNCTION) {
|
||||
// Creates a PHP array from a string representing a python array (input format ['...','...'])
|
||||
// Only supports:
|
||||
// - one level arrays, not nested ones
|
||||
// - single quotes
|
||||
// - single quotes
|
||||
function createArray($input){
|
||||
|
||||
// empty array
|
||||
@@ -81,9 +81,9 @@ function createArray($input){
|
||||
$replacement = '';
|
||||
|
||||
// remove brackets
|
||||
$noBrackets = preg_replace($patternBrackets, $replacement, $input);
|
||||
|
||||
$options = array();
|
||||
$noBrackets = preg_replace($patternBrackets, $replacement, $input);
|
||||
|
||||
$options = array();
|
||||
|
||||
// create array
|
||||
$optionsTmp = explode(",", $noBrackets);
|
||||
@@ -99,7 +99,7 @@ function createArray($input){
|
||||
{
|
||||
array_push($options, preg_replace($patternQuotes, $replacement, $item) );
|
||||
}
|
||||
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function printArray ($array) {
|
||||
{
|
||||
echo $val.', ';
|
||||
}
|
||||
}
|
||||
}
|
||||
echo ']<br/>';
|
||||
}
|
||||
|
||||
@@ -171,9 +171,9 @@ function checkPermissions($files)
|
||||
if(file_exists($file) != 1)
|
||||
{
|
||||
$message = "File '".$file."' not found or inaccessible. Correct file permissions, create one yourself or generate a new one in 'Settings' by clicking the 'Save' button.";
|
||||
displayMessage($message, TRUE);
|
||||
displayMessage($message, TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------
|
||||
@@ -189,8 +189,8 @@ function displayMessage($message, $logAlert = FALSE, $logConsole = TRUE, $logFil
|
||||
$message = str_replace(array("\n", "\r", PHP_EOL), '', $message);
|
||||
|
||||
echo "<script>function escape(html, encode) {
|
||||
return html.replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||
.replace(/\t/g, '')
|
||||
return html.replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||
.replace(/\t/g, '')
|
||||
}</script>";
|
||||
|
||||
// Javascript Alert pop-up
|
||||
@@ -210,7 +210,7 @@ function displayMessage($message, $logAlert = FALSE, $logConsole = TRUE, $logFil
|
||||
{
|
||||
|
||||
if (is_writable($logFolderPath.$log_file)) {
|
||||
|
||||
|
||||
|
||||
if(file_exists($logFolderPath.$log_file) != 1) // file doesn't exist, create one
|
||||
{
|
||||
@@ -219,7 +219,7 @@ function displayMessage($message, $logAlert = FALSE, $logConsole = TRUE, $logFil
|
||||
{
|
||||
$log = fopen($logFolderPath.$log_file, "a") or die("Unable to open file - Permissions issue!");
|
||||
}
|
||||
|
||||
|
||||
fwrite($log, "[".$timestamp. "] " . str_replace('<br>',"\n ",str_replace('<br/>',"\n ",$message)).PHP_EOL."" );
|
||||
fclose($log);
|
||||
|
||||
@@ -269,13 +269,13 @@ function addToExecutionQueue($action)
|
||||
// equivalent: /logs DELETE
|
||||
// 🔺----- API ENDPOINTS SUPERSEDED -----🔺
|
||||
function cleanLog($logFile)
|
||||
{
|
||||
{
|
||||
global $logFolderPath, $timestamp;
|
||||
|
||||
$path = "";
|
||||
|
||||
$allowedFiles = ['app.log', 'app_front.log', 'IP_changes.log', 'stdout.log', 'stderr.log', 'app.php_errors.log', 'execution_queue.log', 'db_is_locked.log', 'nginx-error.log', 'cron.log'];
|
||||
|
||||
|
||||
if(in_array($logFile, $allowedFiles))
|
||||
{
|
||||
$path = $logFolderPath.$logFile;
|
||||
@@ -287,11 +287,11 @@ function cleanLog($logFile)
|
||||
$file = fopen($path, "w") or die("Unable to open file!");
|
||||
fwrite($file, "");
|
||||
fclose($file);
|
||||
displayMessage('File <code>'.$logFile.'</code> purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
displayMessage('File <code>'.$logFile.'</code> purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
} else
|
||||
{
|
||||
displayMessage('File <code>'.$logFile.'</code> is not allowed to be purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
displayMessage('File <code>'.$logFile.'</code> is not allowed to be purged.', FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -299,23 +299,23 @@ function cleanLog($logFile)
|
||||
// ----------------------------------------------------------------------------------------
|
||||
function saveSettings()
|
||||
{
|
||||
global $SETTINGS, $FUNCTION, $config_file, $fullConfPath, $configFolderPath, $timestamp;
|
||||
global $SETTINGS, $FUNCTION, $config_file, $fullConfPath, $configFolderPath, $timestamp;
|
||||
|
||||
// save to the file
|
||||
$new_name = $config_file.'_'.$timestamp.'.backup';
|
||||
$new_location = $configFolderPath.$new_name;
|
||||
$new_location = $configFolderPath.'/'.$new_name;
|
||||
|
||||
if(file_exists( $fullConfPath) != 1)
|
||||
{
|
||||
displayMessage('File "'.$fullConfPath.'" not found or missing read permissions. Creating a new <code>'.$config_file.'</code> file.', FALSE, TRUE, TRUE, TRUE);
|
||||
{
|
||||
displayMessage('File "'.$fullConfPath.'" not found or missing read permissions. Creating a new <code>'.$config_file.'</code> file.', FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
// create a backup copy
|
||||
// create a backup copy
|
||||
elseif (!copy($fullConfPath, $new_location))
|
||||
{
|
||||
displayMessage("Failed to copy file ".$fullConfPath." to ".$new_location." <br/> Check your permissions to allow read/write access to the /config folder.", FALSE, TRUE, TRUE, TRUE);
|
||||
{
|
||||
displayMessage("Failed to copy file ".$fullConfPath." to ".$new_location." <br/> Check your permissions to allow read/write access to the /config folder.", FALSE, TRUE, TRUE, TRUE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// generate a clean .conf file
|
||||
$groups = [];
|
||||
|
||||
@@ -339,12 +339,12 @@ function saveSettings()
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($decodedSettings as $setting) {
|
||||
foreach ($decodedSettings as $setting) {
|
||||
if( in_array($setting[0] , $groups) == false) {
|
||||
array_push($groups ,$setting[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// go thru the groups and prepare settings to write to file
|
||||
foreach ($groups as $group) {
|
||||
$txt .= "\n\n# " . $group;
|
||||
@@ -353,20 +353,20 @@ function saveSettings()
|
||||
foreach ($decodedSettings as $setting) {
|
||||
$settingGroup = $setting[0];
|
||||
$setKey = $setting[1];
|
||||
$dataType = $setting[2];
|
||||
$dataType = $setting[2];
|
||||
$settingValue = $setting[3];
|
||||
|
||||
|
||||
// // Parse the settingType JSON
|
||||
// $settingType = json_decode($settingTypeJson, true);
|
||||
|
||||
|
||||
// Sanity check
|
||||
if($setKey == "UI_LANG" && $settingValue == "") {
|
||||
echo "🔴 Error: important settings missing. Refresh the page with 🔃 on the top and try again.";
|
||||
return;
|
||||
}
|
||||
|
||||
if ($group == $settingGroup) {
|
||||
|
||||
|
||||
if ($group == $settingGroup) {
|
||||
|
||||
if ($dataType == 'string' ) {
|
||||
$val = encode_single_quotes($settingValue);
|
||||
$txt .= $setKey . "='" . $val . "'\n";
|
||||
@@ -381,7 +381,7 @@ function saveSettings()
|
||||
$txt .= $setKey . "=" . $val . "\n";
|
||||
} elseif ($dataType == 'array' ) {
|
||||
$temp = '';
|
||||
|
||||
|
||||
if(is_array($settingValue) == FALSE)
|
||||
{
|
||||
$settingValue = json_decode($settingValue);
|
||||
@@ -397,22 +397,22 @@ function saveSettings()
|
||||
|
||||
$temp = '['.$temp.']'; // wrap brackets
|
||||
$txt .= $setKey . "=" . $temp . "\n";
|
||||
|
||||
|
||||
} else {
|
||||
$txt .= $setKey . "='⭕Not handled⭕'\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
$txt = $txt."\n\n";
|
||||
$txt = $txt."#-------------------IMPORTANT INFO-------------------#\n";
|
||||
$txt = $txt."# This file is ingested by a python script, so if #\n";
|
||||
$txt = $txt."# modified it needs to use python syntax #\n";
|
||||
$txt = $txt."# This file is ingested by a python script, so if #\n";
|
||||
$txt = $txt."# modified it needs to use python syntax #\n";
|
||||
$txt = $txt."#-------------------IMPORTANT INFO-------------------#\n";
|
||||
|
||||
// open new file and write the new configuration
|
||||
// open new file and write the new configuration
|
||||
// Create a temporary file
|
||||
$tempConfPath = $fullConfPath . ".tmp";
|
||||
|
||||
@@ -426,8 +426,8 @@ function saveSettings()
|
||||
fwrite($file, $txt);
|
||||
fclose($file);
|
||||
|
||||
// displayMessage(lang('settings_saved'),
|
||||
// FALSE, TRUE, TRUE, TRUE);
|
||||
// displayMessage(lang('settings_saved'),
|
||||
// FALSE, TRUE, TRUE, TRUE);
|
||||
|
||||
echo "OK";
|
||||
|
||||
@@ -445,7 +445,7 @@ function getString ($setKey, $default) {
|
||||
if ($result )
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
@@ -520,14 +520,14 @@ function getDateFromPeriod () {
|
||||
$days = "3650"; //10 years
|
||||
break;
|
||||
default:
|
||||
$days = "1";
|
||||
}
|
||||
$days = "1";
|
||||
}
|
||||
|
||||
$periodDateSQL = "-".$days." day";
|
||||
$periodDateSQL = "-".$days." day";
|
||||
|
||||
return " date('now', '".$periodDateSQL."') ";
|
||||
|
||||
// $period = $_REQUEST['period'];
|
||||
|
||||
// $period = $_REQUEST['period'];
|
||||
// return '"'. date ('Y-m-d', strtotime ('+2 day -'. $period) ) .'"';
|
||||
}
|
||||
|
||||
@@ -537,13 +537,13 @@ function getDateFromPeriod () {
|
||||
function quotes ($text) {
|
||||
return str_replace ('"','""',$text);
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
function logServerConsole ($text) {
|
||||
$x = array();
|
||||
$y = $x['__________'. $text .'__________'];
|
||||
}
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
function handleNull ($text, $default = "") {
|
||||
if($text == NULL || $text == 'NULL')
|
||||
@@ -553,7 +553,7 @@ function handleNull ($text, $default = "") {
|
||||
{
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------
|
||||
@@ -581,14 +581,14 @@ function decodeSpecialChars($str) {
|
||||
// used in Export CSV
|
||||
function getDevicesColumns(){
|
||||
|
||||
$columns = ["devMac",
|
||||
$columns = ["devMac",
|
||||
"devName",
|
||||
"devOwner",
|
||||
"devType",
|
||||
"devVendor",
|
||||
"devFavorite",
|
||||
"devGroup",
|
||||
"devComments",
|
||||
"devComments",
|
||||
"devFirstConnection",
|
||||
"devLastConnection",
|
||||
"devLastIP",
|
||||
@@ -615,8 +615,8 @@ function getDevicesColumns(){
|
||||
"devFQDN",
|
||||
"devParentRelType",
|
||||
"devReqNicsOnline"
|
||||
];
|
||||
|
||||
];
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
@@ -646,7 +646,7 @@ function getCache($key) {
|
||||
}
|
||||
// -------------------------------------------------------------------------------------------
|
||||
function setCache($key, $value, $expireMinutes = 5) {
|
||||
setcookie($key, $value, time()+$expireMinutes*60, "/","", 0);
|
||||
setcookie($key, $value, time()+$expireMinutes*60, "/","", 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ if ! awk '$2 == "/" && $4 ~ /ro/ {found=1} END {exit !found}' /proc/mounts; then
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
⚠️ Warning: Container is running as read-write, not in read-only mode.
|
||||
|
||||
Please mount the root filesystem as --read-only or use read-only: true
|
||||
Please mount the root filesystem as --read-only or use read_only: true
|
||||
https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/read-only-filesystem.md
|
||||
══════════════════════════════════════════════════════════════════════════════
|
||||
EOF
|
||||
|
||||
Reference in New Issue
Block a user