Compare commits

...

9 Commits

Author SHA1 Message Date
Jokob @NetAlertX
b28cd243f0 Merge pull request #1134 from jokob-sk/main
sync
2025-08-10 20:28:10 +10:00
jokob-sk
dce8c34064 docs, rewrite docker image 2025-08-10 20:22:43 +10:00
jokob-sk
9502ee0cd0 UNIFIAPI v0.2, not ofund mac handling #1132 2025-08-10 20:08:09 +10:00
jokob-sk
8eb4ffe3ed logging
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-08 07:34:23 +10:00
jokob-sk
4be59807e5 docs, UNIFIAPI v0.1 2025-08-07 16:41:40 +10:00
jokob-sk
4712a2ff29 css fixes, nav menu update, searchable devParentNodeMac
Some checks failed
Deploy MkDocs / deploy (push) Has been cancelled
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
2025-08-06 09:15:45 +10:00
jokob-sk
f9179a1e89 safe device name if number #1131 2025-08-06 07:20:04 +10:00
jokob-sk
a6df204721 github timeout #1124, css fixes, change button on LOADED_PLUGINS
Some checks failed
Code checks / check-url-paths (push) Has been cancelled
docker / docker_dev (push) Has been cancelled
Deploy MkDocs / deploy (push) Has been cancelled
2025-08-05 21:32:35 +10:00
jokob-sk
101189ae7c devParentNodeMac chips in devices list 2025-08-05 20:54:28 +10:00
40 changed files with 1394 additions and 138 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,3 +1,3 @@
github: jokob-sk github: jokob-sk
patreon: 84385063 patreon: netalertx
buy_me_a_coffee: jokobsk buy_me_a_coffee: jokobsk

81
.github/workflows/docker_rewrite.yml vendored Executable file
View File

@@ -0,0 +1,81 @@
name: docker
on:
push:
branches:
- rewrite
tags:
- '*.*.*'
pull_request:
branches:
- rewrite
jobs:
docker_dev:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
if: >
contains(github.event.head_commit.message, 'PUSHPROD') != 'True' &&
github.repository == 'jokob-sk/NetAlertX'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up dynamic build ARGs
id: getargs
run: echo "version=$(cat ./stable/VERSION)" >> $GITHUB_OUTPUT
- name: Get release version
id: get_version
run: echo "version=Dev" >> $GITHUB_OUTPUT
- name: Create .VERSION file
run: echo "${{ steps.get_version.outputs.version }}" >> .VERSION
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/jokob-sk/netalertx-dev-rewrite
jokobsk/netalertx-dev-rewrite
tags: |
type=raw,value=latest
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Log in to Github Container Registry (GHCR)
uses: docker/login-action@v3
with:
registry: ghcr.io
username: jokob-sk
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -13,7 +13,7 @@ ENV PATH="/opt/venv/bin:$PATH"
COPY . ${INSTALL_DIR}/ COPY . ${INSTALL_DIR}/
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \ RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
&& bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \ && bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \
&& bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \ && bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \
&& bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;" && bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;"

View File

@@ -43,7 +43,7 @@ RUN phpenmod -v 8.2 sqlite3
RUN apt-get install -y python3-venv RUN apt-get install -y python3-venv
RUN python3 -m venv myenv RUN python3 -m venv myenv
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag " RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
# Create a buildtimestamp.txt to later check if a new version was released # Create a buildtimestamp.txt to later check if a new version was released
RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt

34
docs/DEBUG_PHP.md Executable file
View File

@@ -0,0 +1,34 @@
# Debugging backend PHP issues
## Logs in UI
![Logs UI](./img/DEBUG/maintenance_debug_php.png)
You can view recent backend PHP errors directly in the **Maintenance > Logs** section of the UI. This provides quick access to logs without needing terminal access.
## Accessing logs directly
Sometimes, the UI might not be accessible. In that case, you can access the logs directly inside the container.
### Step-by-step:
1. **Open a shell into the container:**
```bash
docker exec -it netalertx /bin/sh
```
2. **Check the NGINX error log:**
```bash
cat /var/log/nginx/error.log
```
3. **Check the PHP application error log:**
```bash
cat /app/log/app.php_errors.log
```
These logs will help identify syntax issues, fatal errors, or startup problems when the UI fails to load properly.

View File

@@ -53,6 +53,9 @@ The function `guess_device_attributes(...)` runs a series of matching functions
4. IP pattern → `match_ip()` 4. IP pattern → `match_ip()`
5. Final fallback → defaults defined in the `NEWDEV_devIcon` and `NEWDEV_devType` settings. 5. Final fallback → defaults defined in the `NEWDEV_devIcon` and `NEWDEV_devType` settings.
> [!NOTE]
> The app will try guessing the device type or icon if `devType` or `devIcon` are `""` or `"null"`.
### Use of default values ### Use of default values
The guessing process runs for every device **as long as the current type or icon still matches the default values**. Even if earlier heuristics return a match, the system continues evaluating additional clues — like name or IP — to try and replace placeholders. The guessing process runs for every device **as long as the current type or icon still matches the default values**. Even if earlier heuristics return a match, the system continues evaluating additional clues — like name or IP — to try and replace placeholders.

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -1,7 +1,7 @@
<?php <?php
require 'php/templates/header.php'; require 'php/templates/header.php';
require 'php/templates/notification.php'; require 'php/templates/modals.php';
?> ?>
<!-- ----------------------------------------------------------------------- --> <!-- ----------------------------------------------------------------------- -->

View File

@@ -387,6 +387,16 @@ body
margin-bottom: 0px; margin-bottom: 0px;
} }
.plugin-content #tabs-location .nav-tabs-custom > .nav-tabs > li
{
display: contents;
}
.plugin-content .left-nav
{
display: contents;
}
.pa-small-box-2 .inner h3 { .pa-small-box-2 .inner h3 {
margin-left: 0em; margin-left: 0em;
margin-bottom: 1.3em; margin-bottom: 1.3em;
@@ -1411,6 +1421,7 @@ input[readonly] {
.iconPreview svg{ .iconPreview svg{
min-width: 20px; min-width: 20px;
max-width: 20px; max-width: 20px;
margin-bottom: -3px;
} }
@@ -1489,7 +1500,7 @@ input[readonly] {
} }
#tableDevicesBox td svg, #tableDevicesBox td i{ #tableDevicesBox td svg, #tableDevicesBox td i{
height: 1.5em !important; height: 1em !important;
} }
#TileCards .tile .inner #TileCards .tile .inner
@@ -1649,6 +1660,21 @@ input[readonly] {
pointer-events: none; pointer-events: none;
} }
.custom-badge a
{
color: #fff !important;
font-size: 14px;
}
.custom-badge
{
border: 1px solid #aaa;
border-radius: 4px;
border-style: solid;
padding: 0 5px;
font-size: 14px;
display: inline-block;
}
#deviceDetailsEdit .form-control #deviceDetailsEdit .form-control
{ {
min-height: 42px; min-height: 42px;
@@ -1700,6 +1726,16 @@ input[readonly] {
width: 92%; width: 92%;
} }
#modal-ok
{
z-index: 1051; /*highest priority*/
}
#modal-form-plc
{
display: grid;
}
/* ----------------------------------------------------------------- */ /* ----------------------------------------------------------------- */
/* NETWORK page */ /* NETWORK page */
/* ----------------------------------------------------------------- */ /* ----------------------------------------------------------------- */

View File

@@ -20,6 +20,7 @@
--color-yellow: #f39c12; --color-yellow: #f39c12;
--color-red: #dd4b39; --color-red: #dd4b39;
--color-gray: #8c8c8c; --color-gray: #8c8c8c;
--color-white: #fff;
} }
:root { :root {
@@ -793,5 +794,5 @@
.btn:hover .btn:hover
{ {
color: var(--color-gray); color: var(--color-white);
} }

View File

@@ -25,7 +25,7 @@
<!-- Content header--------------------------------------------------------- --> <!-- Content header--------------------------------------------------------- -->
<section class="content-header"> <section class="content-header">
<?php require 'php/templates/notification.php'; ?> <?php require 'php/templates/modals.php'; ?>
<h1 id="pageTitle"> <h1 id="pageTitle">
&nbsp<small>Quering device info...</small> &nbsp<small>Quering device info...</small>

View File

@@ -290,18 +290,6 @@
}); });
} }
// ----------------------------------------
// Show the description of a setting
function showDescriptionPopup(e) {
console.log($(e).attr("my-set-key"));
showModalOK("Info", getString($(e).attr("my-set-key") + '_description'))
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Save device data to DB // Save device data to DB
function setDeviceData(direction = '', refreshCallback = '') { function setDeviceData(direction = '', refreshCallback = '') {

View File

@@ -503,36 +503,36 @@ function collectFilters() {
function mapColumnIndexToFieldName(index, tableColumnVisible) { function mapColumnIndexToFieldName(index, tableColumnVisible) {
// the order is important, don't change it! // the order is important, don't change it!
const columnNames = [ const columnNames = [
"devName", "devName", // 0
"devOwner", "devOwner", // 1
"devType", "devType", // 2
"devIcon", "devIcon", // 3
"devFavorite", "devFavorite", // 4
"devGroup", "devGroup", // 5
"devFirstConnection", "devFirstConnection", // 6
"devLastConnection", "devLastConnection", // 7
"devLastIP", "devLastIP", // 8
"devIsRandomMac", // resolved on the fly "devIsRandomMac", // 9 resolved on the fly
"devStatus", // resolved on the fly "devStatus", // 10 resolved on the fly
"devMac", "devMac", // 11
"devIpLong", //formatIPlong(device.devLastIP) || "", // IP orderable "devIpLong", // 12 formatIPlong(device.devLastIP) || "", // IP orderable
"rowid", "rowid", // 13
"devParentMAC", "devParentMAC", // 14
"devParentChildrenCount", // resolved on the fly "devParentChildrenCount", // 15 resolved on the fly
"devLocation", "devLocation", // 16
"devVendor", "devVendor", // 17
"devParentPort", "devParentPort", // 18
"devGUID", "devGUID", // 19
"devSyncHubNode", "devSyncHubNode", // 20
"devSite", "devSite", // 21
"devSSID", "devSSID", // 22
"devSourcePlugin", "devSourcePlugin", // 23
"devPresentLastScan", "devPresentLastScan", // 24
"devAlertDown", "devAlertDown", // 25
"devCustomProps", "devCustomProps", // 26
"devFQDN", "devFQDN", // 27
"devParentRelType", "devParentRelType", // 28
"devReqNicsOnline" "devReqNicsOnline" // 29
]; ];
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]); // console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
@@ -899,6 +899,28 @@ function initializeDatatable (status) {
} }
} }, } },
// Parent Mac
{targets: [mapIndx(14)],
'createdCell': function (td, cellData, rowData, row, col) {
if (!isValidMac(cellData)) {
$(td).html('');
return;
}
const data = {
id: cellData, // MAC address
text: cellData // Optional display text (you could use a name or something else)
};
spanWrap = $(`<span class="custom-badge text-white"></span>`)
$(td).html(spanWrap);
const chipHtml = renderDeviceLink(data, spanWrap, true); // pass the td as container
$(spanWrap).append(chipHtml);
}
},
// Status color // Status color
{targets: [mapIndx(10)], {targets: [mapIndx(10)],
'createdCell': function (td, cellData, rowData, row, col) { 'createdCell': function (td, cellData, rowData, row, col) {
@@ -982,9 +1004,7 @@ function initializeDatatable (status) {
} }
}); });
} }

View File

@@ -4,7 +4,7 @@
"display": "standalone", "display": "standalone",
"icons": [ "icons": [
{ {
"src": "", "src": "/img/NetAlertX_logo.png",
"sizes": "180x180", "sizes": "180x180",
"type": "image/png" "type": "image/png"
} }

View File

@@ -1032,6 +1032,7 @@ function getDevDataByMac(macAddress, dbColumn) {
} }
} }
console.error("⚠ Device with MAC not found:" + macAddress)
return "Unknown"; // Return a default value if MAC address is not found return "Unknown"; // Return a default value if MAC address is not found
} }

View File

@@ -161,6 +161,91 @@ function showModalFieldInput(
$(`#${prefix}`).modal("show"); $(`#${prefix}`).modal("show");
} }
// -----------------------------------------------------------------------------
function showModalPopupForm(
title,
message,
btnCancel = getString("Gen_Cancel"),
btnOK = getString("Gen_Okay"),
curValue = "",
callbackFunction = null,
triggeredBy = null,
popupFormJson = null,
parentSettingKey = null
) {
// set captions
prefix = "modal-form";
console.log(popupFormJson);
$(`#${prefix}-title`).html(title);
$(`#${prefix}-message`).html(message);
$(`#${prefix}-cancel`).html(btnCancel);
$(`#${prefix}-OK`).html(btnOK);
if (callbackFunction != null) {
modalCallbackFunction = callbackFunction;
}
if (triggeredBy != null) {
$('#'+prefix).attr("data-myparam-triggered-by", triggeredBy)
}
outputHtml = "";
if (Array.isArray(popupFormJson)) {
popupFormJson.forEach((field, index) => {
// You'll need to define these or map them from `field`
const setName = field.name?.find(n => n.language_code === "en_us")?.string || setKey;
const labelClasses = "col-sm-2"; // example, or from your obj.labelClasses
const inputClasses = "col-sm-10"; // example, or from your obj.inputClasses
const fieldData = field.default_value ?? "";
const fieldOptionsOverride = field.type?.elements[0]?.elementOptions || [];
const setKey = field.function || `field_${index}`;
const setValue = field.default_value ?? "";
const setType = JSON.stringify(field.type);
const setEvents = field.events || []; // default to empty array if missing
const setObj = { setKey, setValue, setType, setEvents };
// Generate the input field HTML
const inputFormHtml = `
<div class="form-group col-xs-12">
<label id="${setKey}_label" class="${labelClasses}"> ${setName}
<i my-set-key="${parentSettingKey}_popupform_${setKey}"
title="${getString("Settings_Show_Description")}"
class="fa fa-circle-info pointer helpIconSmallTopRight"
onclick="showDescriptionPopup(this)">
</i>
</label>
<div class="${inputClasses}">
${generateFormHtml(
null, // settingsData only required for datatables
setObj,
fieldData.toString(),
fieldOptionsOverride,
null
)}
</div>
</div>
`;
// Append to result
outputHtml += inputFormHtml;
});
}
$(`#modal-form-plc`).html(outputHtml);
// $(`#${prefix}-field`).val(curValue);
// setTimeout(function () {
// $(`#${prefix}-field`).focus();
// }, 500);
// Show modal
$(`#${prefix}`).modal("show");
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function modalDefaultOK() { function modalDefaultOK() {
// Hide modal // Hide modal

View File

@@ -67,6 +67,15 @@ function getPluginConfig(pluginsData, prefix) {
return result; return result;
} }
// ----------------------------------------
// Show the description of a setting
function showDescriptionPopup(e) {
console.log($(e).attr("my-set-key"));
showModalOK("Info", getString($(e).attr("my-set-key") + '_description'))
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Generate plugin HTML card based on prefixes in an array // Generate plugin HTML card based on prefixes in an array
function pluginCards(prefixesOfEnabledPlugins, includeSettings) { function pluginCards(prefixesOfEnabledPlugins, includeSettings) {
@@ -299,6 +308,45 @@ function removeDataTableRow(el) {
} }
} }
// ---------------------------------------------------------
// Add item via pop up form dialog
function addViaPopupForm(element) {
console.log(element)
const fromId = $(element).attr("my-input-from");
const toId = $(element).attr("my-input-to");
const curValue = $(`#${fromId}`).val();
const triggeredBy = $(element).attr("id");
const parsed = JSON.parse(atob($(element).data("elementoptionsbase64")));
const popupFormJson = parsed.find(obj => "popupForm" in obj)?.popupForm ?? null;
console.log(`fromId | toId | triggeredBy | curValue: ${fromId} | ${toId} | ${triggeredBy} | ${curValue}`);
showModalPopupForm(
`<i class="fa fa-pen-to-square"></i> ${getString(
"Gen_Update_Value"
)}`, // title
getString("settings_update_item_warning"), // message
getString("Gen_Cancel"), // btnCancel
getString("Gen_Add"), // btnOK
curValue, // curValue
null, // callbackFunction
triggeredBy, // triggeredBy
popupFormJson, // popupform
toId // parentSettingKey
);
}
// ---------------------------------------------------------
// Add item to list via popup form
function addViaPopupFormToList(element, clearInput = true) {
// flag something changes to prevent navigating from page
settingsChanged();
}
// --------------------------------------------------------- // ---------------------------------------------------------
// Add item to list // Add item to list
function addList(element, clearInput = true) { function addList(element, clearInput = true) {
@@ -622,8 +670,6 @@ function generateOptionsOrSetOptions(
// obj.push({ id: item, name: item }) // obj.push({ id: item, name: item })
options = arrayToObject(createArray(overrideOptions ? overrideOptions : getSettingOptions(setKey))) options = arrayToObject(createArray(overrideOptions ? overrideOptions : getSettingOptions(setKey)))
// Call to render lists // Call to render lists
renderList( renderList(
options, options,
@@ -633,8 +679,6 @@ function generateOptionsOrSetOptions(
targetField, targetField,
transformers transformers
); );
} }
@@ -655,6 +699,13 @@ function applyTransformers(val, transformers) {
val = btoa(val); val = btoa(val);
} }
break; break;
case "name|base64":
// // Implement base64 logic
// if (!isBase64(val)) {
// val = btoa(val);
// }
val = val; // probably TODO ⚠
break;
case "getString": case "getString":
// no change // no change
val = val; val = val;
@@ -681,6 +732,13 @@ function reverseTransformers(val, transformers) {
val = atob(val); val = atob(val);
} }
break; break;
case "name|base64":
// // Implement base64 decoding logic
// if (isBase64(val)) {
// val = atob(val);
// }
val = val; // probably TODO ⚠
break;
case "getString": case "getString":
// retrieve string // retrieve string
val = getString(val); val = getString(val);
@@ -720,8 +778,8 @@ const handleElementOptions = (setKey, elementOptions, transformers, val) => {
let customParams = ""; let customParams = "";
let customId = ""; let customId = "";
let columns = []; let columns = [];
let base64Regex = ""; let base64Regex = "";
let elementOptionsBase64 = btoa(JSON.stringify(elementOptions));
elementOptions.forEach((option) => { elementOptions.forEach((option) => {
if (option.prefillValue) { if (option.prefillValue) {
@@ -804,7 +862,8 @@ const handleElementOptions = (setKey, elementOptions, transformers, val) => {
customParams, customParams,
customId, customId,
columns, columns,
base64Regex base64Regex,
elementOptionsBase64
}; };
}; };
@@ -955,6 +1014,8 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
// } // }
// Parse the setType JSON string // Parse the setType JSON string
console.log(processQuotes(setType));
const setTypeObject = JSON.parse(processQuotes(setType)) const setTypeObject = JSON.parse(processQuotes(setType))
const dataType = setTypeObject.dataType; const dataType = setTypeObject.dataType;
const elements = setTypeObject.elements || []; const elements = setTypeObject.elements || [];
@@ -982,7 +1043,8 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
customParams, customParams,
customId, customId,
columns, columns,
base64Regex base64Regex,
elementOptionsBase64
} = handleElementOptions(setKey, elementOptions, transformers, inVal); } = handleElementOptions(setKey, elementOptions, transformers, inVal);
// Override value // Override value
@@ -1051,6 +1113,7 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
my-originalSetKey="${originalSetKey}" my-originalSetKey="${originalSetKey}"
my-input-from="${sourceIds}" my-input-from="${sourceIds}"
my-input-to="${setKey}" my-input-to="${setKey}"
data-elementoptionsbase64="${elementOptionsBase64}"
onclick="${onClick}"> onclick="${onClick}">
${getString(getStringKey)} ${getString(getStringKey)}
</button>`; </button>`;

View File

@@ -715,45 +715,7 @@ function initSelect2() {
{ {
var selectEl = $(this).select2({ var selectEl = $(this).select2({
templateSelection: function (data, container) { templateSelection: function (data, container) {
if (!data.id) return data.text; // default for placeholder etc. return $(renderDeviceLink(data, container));
const device = getDevDataByMac(data.id);
const badge = getStatusBadgeParts(
device.devPresentLastScan,
device.devAlertDown,
device.devMac
)
$(container).addClass(badge.cssClass);
// Custom HTML
const html = $(`
<a href="${badge.url}" target="_blank">
<span class="custom-chip hover-node-info"
data-name="${device.devName}"
data-ip="${device.devLastIP}"
data-mac="${device.devMac}"
data-vendor="${device.devVendor}"
data-type="${device.devType}"
data-lastseen="${device.devLastConnection}"
data-firstseen="${device.devFirstConnection}"
data-relationship="${device.devParentRelType}"
data-status="${device.devStatus}"
data-present="${device.devPresentLastScan}"
data-alert="${device.devAlertDown}"
data-icon="${device.devIcon}"
>
<span class="iconPreview">${atob(device.devIcon)}</span>
${data.text}
<span>
(${badge.iconHtml})
</span
</span>
</a>
`);
return html;
}, },
escapeMarkup: function (m) { escapeMarkup: function (m) {
return m; // Allow HTML return m; // Allow HTML
@@ -817,6 +779,50 @@ function initSelect2() {
} }
} }
// ------------------------------------------
// Render a device link with hover-over functionality
function renderDeviceLink(data, container, useName = false) {
if (!data.id || !isValidMac(data.id)) return data.text; // default placeholder etc.
const device = getDevDataByMac(data.id);
const badge = getStatusBadgeParts(
device.devPresentLastScan,
device.devAlertDown,
device.devMac
);
// badge class and hover-info class to container
$(container)
.addClass(`${badge.cssClass} hover-node-info`)
.attr({
'data-name': device.devName,
'data-ip': device.devLastIP,
'data-mac': device.devMac,
'data-vendor': device.devVendor,
'data-type': device.devType,
'data-lastseen': device.devLastConnection,
'data-firstseen': device.devFirstConnection,
'data-relationship': device.devParentRelType,
'data-status': device.devStatus,
'data-present': device.devPresentLastScan,
'data-alert': device.devAlertDown,
'data-icon': device.devIcon
});
return `
<a href="${badge.url}" target="_blank">
<span class="custom-chip">
<span class="iconPreview">${atob(device.devIcon)}</span>
${useName ? device.devName : data.text}
<span>
(${badge.iconHtml})
</span>
</span>
</a>
`;
}
// ------------------------------------------ // ------------------------------------------
// Display device info on hover (attach only once) // Display device info on hover (attach only once)
function initHoverNodeInfo() { function initHoverNodeInfo() {

View File

@@ -1,6 +1,6 @@
<?php <?php
require 'php/templates/header.php'; require 'php/templates/header.php';
require 'php/templates/notification.php'; require 'php/templates/modals.php';
?> ?>
<!-- Page ------------------------------------------------------------------ --> <!-- Page ------------------------------------------------------------------ -->
@@ -185,6 +185,12 @@ $db->close();
</div> </div>
<div class="db_tools_table_cell_b"><?= lang('Maintenance_Tool_del_ActHistory_text');?></div> <div class="db_tools_table_cell_b"><?= lang('Maintenance_Tool_del_ActHistory_text');?></div>
</div> </div>
<div class="db_info_table_row">
<div class="db_tools_table_cell_a" >
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red dbtools-button" id="btnRestartServer" onclick="askRestartBackend()"><?= lang('Maint_RestartServer');?></button>
</div>
<div class="db_tools_table_cell_b"><?= lang('Maint_Restart_Server_noti_text');?></div>
</div>
</div> </div>
</div> </div>

View File

@@ -136,7 +136,8 @@
customParams, customParams,
customId, customId,
columns, columns,
base64Regex base64Regex,
elementOptionsBase64
} = handleElementOptions('none', elementOptions, transformers, val = ""); } = handleElementOptions('none', elementOptions, transformers, val = "");
// render based on element type // render based on element type

View File

@@ -1,6 +1,6 @@
<?php <?php
require 'php/templates/header.php'; require 'php/templates/header.php';
require 'php/templates/notification.php'; require 'php/templates/modals.php';
?> ?>
<script> <script>

View File

@@ -436,8 +436,24 @@
</li> </li>
<!-- system info menu item --> <!-- system info menu item -->
<li class=" <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'active'; } ?>"> <li class=" treeview <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'active menu-open'; } ?>">
<a href="systeminfo.php"><i class="fa fa-fw fa-info-circle"></i> <span><?= lang('Navigation_SystemInfo');?></span></a> <a href="#">
<i class="fa fa-fw fa-info-circle"></i> <span><?= lang('Navigation_SystemInfo');?></span>
<span class="pull-right-container">
<i class="fa fa-angle-left pull-right"></i>
</span>
</a>
<ul class="treeview-menu " style="display: <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'block'; } else {echo 'none';} ?>;">
<li>
<a href="systeminfo.php#panServer" onclick="setCache('activeSysinfoTab','tabServer');initializeTabs()"><?= lang('Systeminfo_System');?></a>
</li>
<li>
<a href="systeminfo.php#panNetwork" onclick="setCache('activeSysinfoTab','tabNetwork');initializeTabs()"><?= lang('Systeminfo_Network');?></a>
</li>
<li>
<a href="systeminfo.php#panStorage" onclick="setCache('activeSysinfoTab','tabStorage');initializeTabs()"><?= lang('Systeminfo_Storage');?></a>
</li>
</ul>
</li> </li>
</ul> </ul>
@@ -450,24 +466,6 @@
<script defer> <script defer>
// Generate work-in-progress icons
function workInProgress() {
if($(".work-in-progress").length > 0 && $(".work-in-progress").html().trim() == "")
{
$(".work-in-progress").append(`
<a href="https://github.com/jokob-sk/NetAlertX/issues" target="_blank">
<b class="pointer" title="${getString("Gen_Work_In_Progress")}">🦺</b>
</a>
`)
}
}
//--------------------------------------------------------------
//--------------------------------------------------------------
function toggleFullscreen() { function toggleFullscreen() {
if (document.fullscreenElement) { if (document.fullscreenElement) {
@@ -485,6 +483,5 @@ function workInProgress() {
// Update server state in the header // Update server state in the header
updateState() updateState()
workInProgress()
</script> </script>

View File

@@ -1,6 +1,6 @@
<?php <?php
require 'php/templates/notification.php'; require 'php/templates/modals.php';
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// check if authenticated // check if authenticated
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/php/templates/security.php';

View File

@@ -117,6 +117,29 @@
</div> </div>
</div> </div>
<!-- Modal form input -->
<div class="modal fade" id="modal-form" data-myparam-triggered-by="" style="display: none;">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 id="modal-form-title" class="modal-title"> Modal Title </h4>
</div>
<div id="modal-form-message" class="modal-body"> Modal message </div>
<div id="modal-form-plc"></div>
<div class="modal-footer">
<button id="modal-form-cancel" type="button" class="btn btn-outline pull-left" style="min-width: 80px;" data-dismiss="modal"> Cancel </button>
<button id="modal-form-OK" type="button" class="btn btn-outline btn-modal-submit" style="min-width: 80px;" onclick="modalDefaultForm()"> OK </button>
</div>
</div>
</div>
</div>
<!-- Modal field input --> <!-- Modal field input -->
<div class="modal modal-warning fade" id="modal-field-input" data-myparam-triggered-by="" style="display: none;"> <div class="modal modal-warning fade" id="modal-field-input" data-myparam-triggered-by="" style="display: none;">

View File

@@ -1,7 +1,7 @@
<?php <?php
require 'php/templates/header.php'; require 'php/templates/header.php';
require 'php/templates/notification.php'; require 'php/templates/modals.php';
?> ?>
<!-- Page ------------------------------------------------------------------ --> <!-- Page ------------------------------------------------------------------ -->

View File

@@ -0,0 +1,26 @@
## Overview
Unifi import plugin using the Site Manager API.
> [!TIP]
> The Site Manager API doesn't seems to have feature parity with the old API yet, so certain limitations apply.
### Quick setup guide
Navigate to your UniFi Site Manager _⚙ Settings -> Control Plane -> Integrations_.
- `api_key` : You can generate your API key under the _Your API Keys_ section.
- `base_url` : You can find your base url in the _API Request Format_ section, e.g. `https://192.168.100.1/proxy/network/integration/`
- `version` : You can find your version as part of the url in the _API Request Format_ section, e.g. `v1`
- `skip_ssl` : To skip SSL with you don't have an SSL certificate
### Usage
- Head to **Settings** > **Plugin name** to adjust the default values.
### Notes
- Version: 1.0.0
- Author: `jokob-sk`
- Release Date: `Aug 2025`

View File

@@ -0,0 +1,743 @@
{
"code_name": "unifi_api_import",
"unique_prefix": "UNIFIAPI",
"plugin_type": "device_scanner",
"execution_order": "Layer_0",
"enabled": true,
"data_source": "script",
"mapped_to_table": "CurrentScan",
"data_filters": [
{
"compare_column": "Object_PrimaryID",
"compare_operator": "==",
"compare_field_id": "txtMacFilter",
"compare_js_template": "'{value}'.toString()",
"compare_use_quotes": true
}
],
"show_ui": true,
"localized": [
"display_name",
"description",
"icon"
],
"display_name": [
{
"language_code": "en_us",
"string": "UniFi import (API)"
}
],
"description": [
{
"language_code": "en_us",
"string": "This plugin is used to import devices from an UNIFI controller via the Site Manager API."
}
],
"icon": [
{
"language_code": "en_us",
"string": "<i class=\"fa-solid fa-shield-halved\"></i>"
}
],
"params": [],
"settings": [
{
"function": "RUN",
"events": [
"run"
],
"type": {
"dataType": "string",
"elements": [
{
"elementType": "select",
"elementOptions": [],
"transformers": []
}
]
},
"default_value": "disabled",
"options": [
"disabled",
"once",
"schedule"
],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "When to run"
}
],
"description": [
{
"language_code": "en_us",
"string": "When the plugin should run. Good options are <code>schedule</code>, <code>once</code>."
}
]
},
{
"function": "RUN_SCHD",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "span",
"elementOptions": [
{
"cssClasses": "input-group-addon validityCheck"
},
{
"getStringKey": "Gen_ValidIcon"
}
],
"transformers": []
},
{
"elementType": "input",
"elementOptions": [
{
"onChange": "validateRegex(this)"
},
{
"base64Regex": "Xig/OlwqfCg/OlswLTldfFsxLTVdWzAtOV18WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlswLTldfDFbMC05XXwyWzAtM118WzAtOV0rLVswLTldK3xcKi9bMC05XSspKVxzKyg/OlwqfCg/OlsxLTldfFsxMl1bMC05XXwzWzAxXXxbMC05XSstWzAtOV0rfFwqL1swLTldKykpXHMrKD86XCp8KD86WzEtOV18MVswLTJdfFswLTldKy1bMC05XSt8XCovWzAtOV0rKSlccysoPzpcKnwoPzpbMC02XXxbMC02XS1bMC02XXxcKi9bMC05XSspKSQ="
}
],
"transformers": []
}
]
},
"default_value": "*/5 * * * *",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Schedule"
}
],
"description": [
{
"language_code": "en_us",
"string": "Only enabled if you select <code>schedule</code> in the <a href=\"#UNIFIAPI_RUN\"><code>UNIFIAPI_RUN</code> setting</a>. Make sure you enter the schedule in the correct cron-like format (e.g. validate at <a href=\"https://crontab.guru/\" target=\"_blank\">crontab.guru</a>). For example entering <code>0 4 * * *</code> will run the scan after 4 am in the <a onclick=\"toggleAllSettings()\" href=\"#TIMEZONE\"><code>TIMEZONE</code> you set above</a>. Will be run NEXT time the time passes."
}
]
},
{
"function": "CMD",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"readonly": "true"
}
],
"transformers": []
}
]
},
"default_value": "python3 /app/front/plugins/unifi_api_import/unifi_api_import.py",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Command"
}
],
"description": [
{
"language_code": "en_us",
"string": "Command to run. This can not be changed"
}
]
},
{
"function": "RUN_TIMEOUT",
"type": {
"dataType": "integer",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"type": "number"
}
],
"transformers": []
}
]
},
"default_value": 10,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Run timeout"
}
],
"description": [
{
"language_code": "en_us",
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
},
{
"function": "sites",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "button",
"elementOptions": [
{
"sourceSuffixes": [
"_in"
]
},
{
"separator": ""
},
{
"cssClasses": "col-xs-12"
},
{
"onClick": "addViaPopupForm(this)"
},
{
"getStringKey": "Gen_Add"
},
{
"popupForm": [
{
"function": "name",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "Enter value"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Site name"
}
],
"description": [
{
"language_code": "en_us",
"string": "The name of your site. Use a descriptive name."
}
]
},
{
"function": "base_url",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "https://host_ip/proxy/network/integration/"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Base URL"
}
],
"description": [
{
"language_code": "en_us",
"string": "You can find your base url in the UniFi Site Manager in <i>Settings -> Control Plane -> Integrations</i> in the <i>API Request Format</i> section, (e.g. <code>https://host_ip/proxy/network/integration/</code>)."
}
]
},
{
"function": "version",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "v1"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "API version"
}
],
"description": [
{
"language_code": "en_us",
"string": "The version of the API (e.g.: <code>v1</code>)."
}
]
},
{
"function": "api_key",
"type": {
"dataType": "string",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"placeholder": "Enter value"
},
{
"suffix": "_in"
},
{
"cssClasses": "col-sm-10"
},
{
"prefillValue": "null"
}
],
"transformers": []
}
]
},
"default_value": "default",
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "API key"
}
],
"description": [
{
"language_code": "en_us",
"string": "You can get an API key in your UniFi Site Manager in <i>Settings -> Control Plane -> Integrations</i>."
}
]
},
{
"function": "hide.site.verify_ssl",
"type": {
"dataType": "boolean",
"elements": [
{
"elementType": "input",
"elementOptions": [
{
"type": "checkbox"
}
],
"transformers": []
}
]
},
"default_value": 1,
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Verify SSL"
}
],
"description": [
{
"language_code": "en_us",
"string": "Disable if you do not have an SSL certificate set up."
}
]
}
],
"transformers": []
}
],
"transformers": []
},
{
"elementType": "select",
"elementHasInputValue": 1,
"elementOptions": [
{
"multiple": "true"
},
{
"readonly": "true"
},
{
"editable": "true"
}
],
"transformers": [
"name|base64"
]
},
{
"elementType": "button",
"elementOptions": [
{
"sourceSuffixes": []
},
{
"separator": ""
},
{
"cssClasses": "col-xs-6"
},
{
"onClick": "removeFromList(this)"
},
{
"getStringKey": "Gen_Remove_Last"
}
],
"transformers": []
},
{
"elementType": "button",
"elementOptions": [
{
"sourceSuffixes": []
},
{
"separator": ""
},
{
"cssClasses": "col-xs-6"
},
{
"onClick": "removeAllOptions(this)"
},
{
"getStringKey": "Gen_Remove_All"
}
],
"transformers": []
}
]
},
"default_value": [],
"options": [],
"localized": [
"name",
"description"
],
"name": [
{
"language_code": "en_us",
"string": "Sites"
}
],
"description": [
{
"language_code": "en_us",
"string": "UniFi sites"
}
]
}
],
"database_column_definitions": [
{
"column": "Index",
"css_classes": "col-sm-2",
"show": true,
"type": "none",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Index"
}
]
},
{
"column": "Object_PrimaryID",
"mapped_to_column": "cur_MAC",
"css_classes": "col-sm-3",
"show": true,
"type": "device_name_mac",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "MAC (name)"
}
]
},
{
"column": "Object_SecondaryID",
"mapped_to_column": "cur_IP",
"css_classes": "col-sm-2",
"show": true,
"type": "device_ip",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "IP"
}
]
},
{
"column": "Watched_Value1",
"mapped_to_column": "cur_Name",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Name"
}
]
},
{
"column": "Watched_Value2",
"mapped_to_column": "cur_Vendor",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Vendor"
}
]
},
{
"column": "Watched_Value3",
"mapped_to_column": "cur_Type",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Device Type"
}
]
},
{
"column": "Watched_Value4",
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "N/A"
}
]
},
{
"column": "Dummy",
"mapped_to_column": "cur_ScanMethod",
"mapped_to_column_data": {
"value": "Example Plugin"
},
"css_classes": "col-sm-2",
"show": false,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Scan method"
}
]
},
{
"column": "DateTimeCreated",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Created"
}
]
},
{
"column": "DateTimeChanged",
"css_classes": "col-sm-2",
"show": true,
"type": "label",
"default_value": "",
"options": [],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Changed"
}
]
},
{
"column": "Status",
"css_classes": "col-sm-1",
"show": true,
"type": "replace",
"default_value": "",
"options": [
{
"equals": "watched-not-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-square-check'></i><div></div>"
},
{
"equals": "watched-changed",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-triangle-exclamation'></i></div>"
},
{
"equals": "new",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-circle-plus'></i></div>"
},
{
"equals": "missing-in-last-scan",
"replacement": "<div style='text-align:center'><i class='fa-solid fa-question'></i></div>"
}
],
"localized": [
"name"
],
"name": [
{
"language_code": "en_us",
"string": "Status"
}
]
}
]
}

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env python
import os
import pathlib
import sys
import json
import sqlite3
from pytz import timezone
# Define the installation path and extend the system path for plugin imports
INSTALL_PATH = "/app"
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from plugin_helper import Plugin_Object, Plugin_Objects, decodeBase64
from plugin_utils import get_plugins_configs
from logger import mylog, Logger
from const import pluginsPath, fullDbPath, logPath
from helper import timeNowTZ, get_setting_value
from messaging.in_app import write_notification
import conf
# Make sure the TIMEZONE for logging is correct
conf.tz = timezone(get_setting_value('TIMEZONE'))
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
pluginName = '<unique_prefix>'
# Define the current path and log file paths
LOG_PATH = logPath + '/plugins'
LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
def main():
mylog('verbose', [f'[{pluginName}] In script'])
# Retrieve configuration settings
some_setting = get_setting_value('SYNC_plugins')
mylog('verbose', [f'[{pluginName}] some_setting value {some_setting}'])
# retrieve data
device_data = get_device_data(some_setting)
# Process the data into native application tables
if len(device_data) > 0:
# insert devices into the lats_result.log
# make sure the below mapping is mapped in config.json, for example:
#"database_column_definitions": [
# {
# "column": "Object_PrimaryID", <--------- the value I save into primaryId
# "mapped_to_column": "cur_MAC", <--------- gets inserted into the CurrentScan DB table column cur_MAC
#
for device in device_data:
plugin_objects.add_object(
primaryId = device['mac_address'],
secondaryId = device['ip_address'],
watched1 = device['hostname'],
watched2 = device['vendor'],
watched3 = device['device_type'],
watched4 = device['last_seen'],
extra = '',
foreignKey = device['mac_address']
# helpVal1 = "Something1", # Optional Helper values to be passed for mapping into the app
# helpVal2 = "Something1", # If you need to use even only 1, add the remaining ones too
# helpVal3 = "Something1", # and set them to 'null'. Check the the docs for details:
# helpVal4 = "Something1", # https://github.com/jokob-sk/NetAlertX/blob/main/docs/PLUGINS_DEV.md
)
mylog('verbose', [f'[{pluginName}] New entries: "{len(device_data)}"'])
# log result
plugin_objects.write_result_file()
return 0
# retrieve data
def get_device_data(some_setting):
device_data = []
# do some processing, call exteranl APIs, and return a device_data list
# ...
#
# Sample data for testing purposes, you can adjust the processing in main() as needed
# ... before adding it to the plugin_objects.add_object(...)
device_data = [
{
'device_id': 'device1',
'mac_address': '00:11:22:33:44:55',
'ip_address': '192.168.1.2',
'hostname': 'iPhone 12',
'vendor': 'Apple Inc.',
'device_type': 'Smartphone',
'last_seen': '2024-06-27 10:00:00',
'port': '1',
'network_id': 'network1'
},
{
'device_id': 'device2',
'mac_address': '00:11:22:33:44:66',
'ip_address': '192.168.1.3',
'hostname': 'Moto G82',
'vendor': 'Motorola Inc.',
'device_type': 'Laptop',
'last_seen': '2024-06-27 10:05:00',
'port': '',
'network_id': 'network1'
}
]
# Return the data to be detected by the main application
return device_data
if __name__ == '__main__':
main()

View File

@@ -1,7 +1,7 @@
<?php <?php
require 'php/templates/header.php'; require 'php/templates/header.php';
require 'php/templates/notification.php'; require 'php/templates/modals.php';
?> ?>

View File

@@ -58,6 +58,12 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
<div id="settingsPage" class="content-wrapper"> <div id="settingsPage" class="content-wrapper">
<a style="cursor:pointer">
<span>
<i id='toggleSettings' onclick="toggleAllSettings()" class="settings-expand-icon fa fa-angle-double-down"></i>
</span>
</a>
<!-- Content header--------------------------------------------------------- --> <!-- Content header--------------------------------------------------------- -->
<section class="content-header"> <section class="content-header">
@@ -597,7 +603,8 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
customParams, customParams,
customId, customId,
columns, columns,
base64Regex base64Regex,
elementOptionsBase64
} = handleElementOptions('none', elementOptions, transformers, val = ""); } = handleElementOptions('none', elementOptions, transformers, val = "");
let value; let value;

View File

@@ -13,7 +13,7 @@
require 'php/templates/header.php'; require 'php/templates/header.php';
?> ?>
<?php require 'php/templates/notification.php'; ?> <?php require 'php/templates/modals.php'; ?>
<!-- ----------------------------------------------------------------------- --> <!-- ----------------------------------------------------------------------- -->

View File

@@ -264,8 +264,6 @@ function fetchUsedIps(callback) {
function renderAvailableIpsTable(allIps, usedIps) { function renderAvailableIpsTable(allIps, usedIps) {
const availableIps = allIps.filter(row => !usedIps.includes(row.ip)); const availableIps = allIps.filter(row => !usedIps.includes(row.ip));
console.log(allIps);
console.log(usedIps);
console.log(availableIps); console.log(availableIps);
$('#availableIpsTable').DataTable({ $('#availableIpsTable').DataTable({

View File

@@ -1,7 +1,7 @@
<?php <?php
require 'php/templates/header.php'; require 'php/templates/header.php';
require 'php/templates/notification.php'; require 'php/templates/modals.php';
?> ?>
<!-- ----------------------------------------------------------------------- --> <!-- ----------------------------------------------------------------------- -->

View File

@@ -30,5 +30,5 @@ source myenv/bin/activate
update-alternatives --install /usr/bin/python python /usr/bin/python3 10 update-alternatives --install /usr/bin/python python /usr/bin/python3 10
# install packages thru pip3 # install packages thru pip3
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors unifi-sm-api tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git

View File

@@ -64,6 +64,7 @@ nav:
- Debugging Tips: DEBUG_TIPS.md - Debugging Tips: DEBUG_TIPS.md
- Debugging GraphQL: DEBUG_GRAPHQL.md - Debugging GraphQL: DEBUG_GRAPHQL.md
- Debugging Invalid JSON: DEBUG_INVALID_JSON.md - Debugging Invalid JSON: DEBUG_INVALID_JSON.md
- Debugging PHP: DEBUG_PHP.md
- Debugging Plugins: DEBUG_PLUGINS.md - Debugging Plugins: DEBUG_PLUGINS.md
- Debugging Web UI Port: WEB_UI_PORT_DEBUG.md - Debugging Web UI Port: WEB_UI_PORT_DEBUG.md
- Debugging Workflows: WORKFLOWS_DEBUGGING.md - Debugging Workflows: WORKFLOWS_DEBUGGING.md

View File

@@ -197,7 +197,7 @@ class Query(ObjectType):
searchable_fields = [ searchable_fields = [
"devName", "devMac", "devOwner", "devType", "devVendor", "devLastIP", "devName", "devMac", "devOwner", "devType", "devVendor", "devLastIP",
"devGroup", "devComments", "devLocation", "devStatus", "devSSID", "devGroup", "devComments", "devLocation", "devStatus", "devSSID",
"devSite", "devSourcePlugin", "devSyncHubNode", "devFQDN", "devParentRelType" "devSite", "devSourcePlugin", "devSyncHubNode", "devFQDN", "devParentRelType", "devParentMAC"
] ]
search_term = options.search.lower() search_term = options.search.lower()

View File

@@ -667,7 +667,10 @@ def checkNewVersion():
buildTimestamp = int(f.read().strip()) buildTimestamp = int(f.read().strip())
try: try:
response = requests.get("https://api.github.com/repos/jokob-sk/NetAlertX/releases") response = requests.get(
"https://api.github.com/repos/jokob-sk/NetAlertX/releases",
timeout=5
)
response.raise_for_status() # Raise an exception for HTTP errors response.raise_for_status() # Raise an exception for HTTP errors
text = response.text text = response.text
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:

View File

@@ -157,7 +157,7 @@ def importConfigs (db, all_plugins):
# ---------------------------------------- # ----------------------------------------
# ccd(key, default, config_dir, name, inputtype, options, group, events=[], desc = "", regex = "", setJsonMetadata = {}, overrideTemplate = {}) # ccd(key, default, config_dir, name, inputtype, options, group, events=[], desc = "", regex = "", setJsonMetadata = {}, overrideTemplate = {})
conf.LOADED_PLUGINS = ccd('LOADED_PLUGINS', [] , c_d, 'Loaded plugins', '{"dataType":"array", "elements": [{"elementType" : "select", "elementOptions" : [{"multiple":"true", "ordeable": "true"}] ,"transformers": []}]}', '[]', 'General') conf.LOADED_PLUGINS = ccd('LOADED_PLUGINS', [] , c_d, 'Loaded plugins', '{"dataType":"array","elements":[{"elementType":"select","elementOptions":[{"multiple":"true","ordeable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"selectChange(this)"},{"getStringKey":"Gen_Change"}],"transformers":[]}]}', '[]', 'General')
conf.DISCOVER_PLUGINS = ccd('DISCOVER_PLUGINS', True , c_d, 'Discover plugins', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General') conf.DISCOVER_PLUGINS = ccd('DISCOVER_PLUGINS', True , c_d, 'Discover plugins', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General')
conf.SCAN_SUBNETS = ccd('SCAN_SUBNETS', ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0'] , c_d, 'Subnets to scan', '''{"dataType": "array","elements": [{"elementType": "input","elementOptions": [{"placeholder": "192.168.1.0/24 --interface=eth1"},{"suffix": "_in"},{"cssClasses": "col-sm-10"},{"prefillValue": "null"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": ["_in"]},{"separator": ""},{"cssClasses": "col-xs-12"},{"onClick": "addList(this, false)"},{"getStringKey": "Gen_Add"}],"transformers": []},{"elementType": "select","elementHasInputValue": 1,"elementOptions": [{"multiple": "true"},{"readonly": "true"},{"editable": "true"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeAllOptions(this)"},{"getStringKey": "Gen_Remove_All"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeFromList(this)"},{"getStringKey": "Gen_Remove_Last"}],"transformers": []}]}''', '[]', 'General') conf.SCAN_SUBNETS = ccd('SCAN_SUBNETS', ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0'] , c_d, 'Subnets to scan', '''{"dataType": "array","elements": [{"elementType": "input","elementOptions": [{"placeholder": "192.168.1.0/24 --interface=eth1"},{"suffix": "_in"},{"cssClasses": "col-sm-10"},{"prefillValue": "null"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": ["_in"]},{"separator": ""},{"cssClasses": "col-xs-12"},{"onClick": "addList(this, false)"},{"getStringKey": "Gen_Add"}],"transformers": []},{"elementType": "select","elementHasInputValue": 1,"elementOptions": [{"multiple": "true"},{"readonly": "true"},{"editable": "true"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeAllOptions(this)"},{"getStringKey": "Gen_Remove_All"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeFromList(this)"},{"getStringKey": "Gen_Remove_Last"}],"transformers": []}]}''', '[]', 'General')
conf.LOG_LEVEL = ccd('LOG_LEVEL', 'verbose' , c_d, 'Log verboseness', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['none', 'minimal', 'verbose', 'debug', 'trace']", 'General') conf.LOG_LEVEL = ccd('LOG_LEVEL', 'verbose' , c_d, 'Log verboseness', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['none', 'minimal', 'verbose', 'debug', 'trace']", 'General')
@@ -275,6 +275,15 @@ def importConfigs (db, all_plugins):
# Save the user defined value into the object # Save the user defined value into the object
set["value"] = v set["value"] = v
# Now check for popupForm inside elements → elementOptions
elements = set.get("type", {}).get("elements", [])
for element in elements:
for option in element.get("elementOptions", []):
if "popupForm" in option:
for popup_entry in option["popupForm"]:
popup_pref = key + "_popupform_" + popup_entry.get("function", "")
stringSqlParams = collect_lang_strings(popup_entry, popup_pref, stringSqlParams)
# Collect settings related language strings # Collect settings related language strings
# Creates an entry with key, for example ARPSCAN_CMD_name # Creates an entry with key, for example ARPSCAN_CMD_name
stringSqlParams = collect_lang_strings(set, pref + "_" + set["function"], stringSqlParams) stringSqlParams = collect_lang_strings(set, pref + "_" + set["function"], stringSqlParams)

View File

@@ -447,8 +447,8 @@ def create_new_devices (db):
cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP, cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT, cur_NetworkSite, cur_SSID, cur_Type = row cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP, cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT, cur_NetworkSite, cur_SSID, cur_Type = row
# Handle NoneType # Handle NoneType
cur_Name = cur_Name.strip() if cur_Name else '(unknown)' cur_Name = str(cur_Name).strip() if cur_Name else '(unknown)'
cur_Type = cur_Type.strip() if cur_Type else get_setting_value("NEWDEV_devType") cur_Type = str(cur_Type).strip() if cur_Type else get_setting_value("NEWDEV_devType")
cur_NetworkNodeMAC = cur_NetworkNodeMAC.strip() if cur_NetworkNodeMAC else '' cur_NetworkNodeMAC = cur_NetworkNodeMAC.strip() if cur_NetworkNodeMAC else ''
cur_NetworkNodeMAC = cur_NetworkNodeMAC if cur_NetworkNodeMAC and cur_MAC != "Internet" else (get_setting_value("NEWDEV_devParentMAC") if cur_MAC != "Internet" else "null") cur_NetworkNodeMAC = cur_NetworkNodeMAC if cur_NetworkNodeMAC and cur_MAC != "Internet" else (get_setting_value("NEWDEV_devParentMAC") if cur_MAC != "Internet" else "null")
cur_SyncHubNodeName = cur_SyncHubNodeName if cur_SyncHubNodeName and cur_SyncHubNodeName != "null" else (get_setting_value("SYNC_node_name")) cur_SyncHubNodeName = cur_SyncHubNodeName if cur_SyncHubNodeName and cur_SyncHubNodeName != "null" else (get_setting_value("SYNC_node_name"))