Merge branch 'main' of https://github.com/netalertx/NetAlertX into agentic-workflows

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
This commit is contained in:
Adam Outler
2026-02-02 21:33:25 +01:00
50 changed files with 344 additions and 129 deletions

View File

@@ -157,6 +157,8 @@ Check the [GitHub Issues](https://github.com/netalertx/NetAlertX/issues) for the
## Everything else
<!--- --------------------------------------------------------------------- --->
<a href="https://trendshift.io/repositories/12670" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12670" alt="jokob-sk%2FNetAlertX | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
### 📧 Get notified what's new
Get notified about a new release, what new functionality you can use and about breaking changes.
@@ -219,7 +221,7 @@ Proudly using [Weblate](https://hosted.weblate.org/projects/pialert/). Help out
[sync_hub]: ./docs/img/sync_hub.png "Screen 8"
[notification_center]: ./docs/img/notification_center.png "Screen 8"
[sent_reports_text]: ./docs/img/sent_reports_text.png "Screen 8"
[device_nmap]: ./docs/img/device_nmap.png "Screen 9"
[device_nmap]: ./docs/img/device_tools.png "Screen 9"
[report1]: ./docs/img/report_sample.png "Report sample 1"
[main_dark]: /docs/img/1_devices_dark.jpg "Main screen dark"
[maintain_dark]: /docs/img/5_maintain.jpg "Maintain screen dark"

View File

@@ -59,6 +59,10 @@ http://<server>:<GRAPHQL_PORT>/
## Endpoints
> [!NOTE]
> You can explore the API endpoints by using the interactive API docs at `http://<server>:<GRAPHQL_PORT>/docs`.
> ![API docs](./img/API/API_docs.png)
> [!TIP]
> When retrieving devices or settings try using the GraphQL API endpoint first as it is read-optimized.

View File

@@ -31,11 +31,6 @@ graph TB
D -->|Response Data| C
C -->|JSON Response| B
B -->|Stream Events| A
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#e8f5e8
```
### MCP Tool Integration
@@ -98,15 +93,6 @@ graph LR
F --> I
G --> J
H --> I
style A fill:#e1f5fe
style B fill:#e1f5fe
style C fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#f3e5f5
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
```
---

View File

@@ -6,7 +6,7 @@ NetAlertX is a lightweight, flexible platform for monitoring networks, tracking
## Network Discovery & Device Tracking
[Network Discovery & Device Tracking](./img/FEATURES/Network_Discovery_Device_Tracking.png)
![Network Discovery & Device Tracking](./img/FEATURES/Network_Discovery_Device_Tracking.png)
- **Automatic Device Detection**: Continuously scans your local network to detect all connected devices via ARP, DHCP, SNMP, and compatible controllers.
- **Presence Monitoring**: Track when devices appear, disappear, or reconnect on the network.

BIN
docs/img/API/API_docs.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

BIN
docs/img/device_tools.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 446 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 203 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 328 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 156 KiB

View File

@@ -4,10 +4,21 @@ hide:
- toc
---
# NetAlertX Documentation
Guides and resources to help you set up, configure, and troubleshoot NetAlertX.
<div class="hero-wrapper">
<div class="hero-content">
<h1>NetAlertX</h1>
<p class="hero-subheading">
Centralized network visibility and continuous asset discovery.
</p>
<p class="hero-description">
NetAlertx delivers a scalable and secure solution for comprehensive network monitoring, supporting security awareness and operational efficiency.
</p>
</div>
<div class="hero-image">
<img src="./img/devices_split.png" alt="Hero image for NetAlertx" class="hero-logo-crisp">
</div>
</div>
<div class="promo-card-wrapper">
<div class="promo-card">
@@ -43,48 +54,6 @@ Guides and resources to help you set up, configure, and troubleshoot NetAlertX.
</div>
</div>
![Preview](./img/devices_split.png)
## In-App Help
NetAlertX provides contextual help within the application:
- **Hover over settings, fields, or labels** to see additional tooltips and guidance.
- **Click ? (question-mark) icons** next to various elements to view detailed information.
---
## Installation Guides
The app can be installed different ways, with the best support of the docker-based deployments. This includes the Home Assistant and Unraid installation approaches. See details below.
### Docker (Fully Supported)
NetAlertX is fully supported in Docker environments, allowing for easy setup and configuration. Follow the official guide to get started:
- [Docker Installation Guide](./DOCKER_INSTALLATION.md)
This guide will take you through the process of setting up NetAlertX using Docker Compose or standalone Docker commands.
### Home Assistant (Fully Supported)
You can install NetAlertX also as a Home Assistant addon [![Home Assistant](https://img.shields.io/badge/Repo-blue?logo=home-assistant&style=for-the-badge&color=0aa8d2&logoColor=fff&label=Add)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons) via the [alexbelgium/hassio-addons](https://github.com/alexbelgium/hassio-addons/) repository. This is only possible if you run a supervised instance of Home Assistant. If not, you can still run NetAlertX in a separate Docker container and follow this guide to configure MQTT.
- [[Installation] Home Assistant](https://github.com/alexbelgium/hassio-addons/tree/master/netalertx)
### Unraid (Partial Support)
The Unraid template was created by the community, so it's only partially supported. Alternatively, here is [another version of the Unraid template](https://github.com/jokob-sk/NetAlertX-unraid).
- [[Installation] Unraid App](https://unraid.net/community/apps)
### Bare-Metal Installation (Experimental)
If you prefer to run NetAlertX on your own hardware, you can try the experimental bare-metal installation. Please note that this method is still under development, and are looking for maintainers to help improve it.
- [Bare-Metal Installation Guide](./HW_INSTALL.md)
---
## Help and Support
@@ -93,14 +62,16 @@ If you need help or run into issues, here are some resources to guide you:
**Before opening an issue, please:**
- **Hover over settings, fields, or labels** to see additional tooltips and guidance.
- **Click ? (question-mark) icons** next to various elements to view detailed information.
- [Check common issues](./DEBUG_TIPS.md#common-issues) to see if your problem has already been reported.
- [Look at closed issues](https://github.com/jokob-sk/NetAlertX/issues?q=is%3Aissue+is%3Aclosed) for possible solutions to past problems.
- [Look at closed issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed) for possible solutions to past problems.
- **Enable debugging** to gather more information: [Debug Guide](./DEBUG_TIPS.md).
**Need more help?** Join the community discussions or submit a support request:
- Visit the [GitHub Discussions](https://github.com/jokob-sk/NetAlertX/discussions) for community support.
- If you are experiencing issues that require immediate attention, consider opening an issue on our [GitHub Issues page](https://github.com/jokob-sk/NetAlertX/issues).
- Visit the [GitHub Discussions](https://github.com/netalertx/NetAlertX/discussions) for community support.
- If you are experiencing issues that require immediate attention, consider opening an issue on our [GitHub Issues page](https://github.com/netalertx/NetAlertX/issues).
---
@@ -119,15 +90,15 @@ For more information on contributing, check out our [Dev Guide](./DEV_ENV_SETUP.
To keep up with the latest changes and updates to NetAlertX, please refer to the following resources:
- [Releases](https://github.com/jokob-sk/NetAlertX/releases)
- [Releases](https://github.com/netalertx/NetAlertX/releases)
Make sure to follow the project on GitHub to get notifications for new releases and important updates.
---
## Additional info
- **Documentation Index**: Check out the full [documentation index](https://github.com/jokob-sk/NetAlertX/tree/main/docs) for all the guides available.
- **Documentation Index**: Check out the full [documentation index](https://github.com/netalertx/NetAlertX/tree/main/docs) for all the guides available.
If you have any suggestions or improvements, please dont hesitate to contribute!
NetAlertX is actively maintained. You can find the source code, report bugs, or request new features on our [GitHub page](https://github.com/jokob-sk/NetAlertX).
NetAlertX is actively maintained. You can find the source code, report bugs, or request new features on our [GitHub page](https://github.com/netalertx/NetAlertX).

View File

@@ -14,6 +14,53 @@
* limitations under the License.
*/
/* --- HERO SECTION --- */
.hero-wrapper {
display: flex;
align-items: flex-start;
justify-content: space-between;
max-width: 1200px;
margin: 0 auto;
}
.hero-content {
flex: 1;
max-width: 26.8rem;
}
.hero-content h1 {
font-size: 3rem;
line-height: 1.12;
font-weight: 450;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
p.hero-subheading {
font-size: 1.5rem;
line-height: 1.28;
margin: 0 0 1rem;
}
p.hero-description {
line-height: 1.4;
margin: 0;
}
.hero-image {
display: flex;
justify-content: center;
margin-top: 15px;
max-width: 45%;
}
.hero-logo-crisp {
width: 100%;
height: auto;
object-fit: contain;
}
/* --- PROMO CARDS --- */
.promo-card-wrapper {
display: flex;

View File

@@ -508,6 +508,41 @@ body
color: #a0a0a0;
}
.small-box {
margin-bottom: 15px !important;
float: left;
width: 100%;
}
#TopSmallBoxes .small-box {
height: 3em;
}
.small-box .icon
{
font-size: 2.2em !important;
float: right;
top: 0;
}
.small-box .small-box-text
{
float:left;
font-size: x-large;
margin-top: -0.3em;
}
.small-box .infobox_label
{
font-size: larger;
float: right;
text-align: center;
}
hr
{
margin-top: 5px;
margin-bottom: 10px;
}
/* -----------------------------------------------------------------------------
Customized Box Borders
----------------------------------------------------------------------------- */
@@ -901,10 +936,6 @@ height: 50px;
background-color: #b2b6be !important;
}
.infobox_label {
font-size: 16px !important;
}
.deviceSelector
{
display: block;
@@ -1667,6 +1698,10 @@ textarea[readonly],
min-height: 42px;
}
.form-group {
margin-bottom: 5px !important;
}
/* Remove the default Select2 chevron (the down arrow) */
.select2-container .select2-selection__arrow b {
display: none !important;

View File

@@ -172,6 +172,15 @@ function getDeviceData() {
labelClasses: "col-sm-4 col-xs-12 control-label",
inputClasses: "col-sm-8 col-xs-12 input-group"
},
// Group for Custom properties.
DevDetail_CustomProperties_Title: {
data: ["devCustomProps"],
docs: "https://docs.netalertx.com/CUSTOM_PROPERTIES",
iconClass: "fa fa-list",
inputGroupClasses: "field-group cutprop-group col-lg-6 col-sm-12 col-xs-12",
labelClasses: "col-sm-12 col-xs-12 control-label",
inputClasses: "col-sm-12 col-xs-12 input-group"
},
// Group for Children.
DevDetail_Children_Title: {
data: ["devChildrenDynamic"],
@@ -181,15 +190,6 @@ function getDeviceData() {
labelClasses: "col-sm-12 col-xs-12 control-label",
inputClasses: "col-sm-12 col-xs-12 input-group"
},
// Group for Custom properties.
DevDetail_CustomProperties_Title: {
data: ["devCustomProps"],
docs: "https://docs.netalertx.com/CUSTOM_PROPERTIES",
iconClass: "fa fa-list",
inputGroupClasses: "field-group cutprop-group col-lg-6 col-sm-12 col-xs-12",
labelClasses: "col-sm-12 col-xs-12 control-label",
inputClasses: "col-sm-12 col-xs-12 input-group"
}
};
// Filter settings data to get relevant settings

View File

@@ -450,10 +450,18 @@ function localizeTimestamp(input) {
const date = new Date(str);
if (!isFinite(date)) {
console.error(`ERROR: Couldn't parse date: '${str}' with TIMEZONE ${tz}`);
return 'Failed conversion - Check browser console';
return 'Failed conversion';
}
// CHECK: Does the input string have an offset (e.g., +11:00 or Z)?
// If it does, and we apply a 'tz' again, we double-shift.
const hasOffset = /[Z|[+-]\d{2}:?\d{2}]$/.test(str.trim());
return new Intl.DateTimeFormat(LOCALE, {
timeZone: tz,
// If it has an offset, we display it as-is (UTC mode in Intl
// effectively means "don't add more hours").
// If no offset, apply your variable 'tz'.
timeZone: hasOffset ? 'UTC' : tz,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
hour12: false

View File

@@ -23,10 +23,19 @@ class NetAlertXStateManager {
*/
init() {
if (this.initialized) return;
// waiting until cache ready
const waitForInit = () => {
if (!isAppInitialized()) {
setTimeout(waitForInit, 300);
return;
}
console.log("[NetAlertX State] Initializing state manager...");
this.trySSE();
this.initialized = true;
console.log("[NetAlertX State] App initialized, starting state manager");
this.trySSE();
this.initialized = true;
};
waitForInit();
}
/**

View File

@@ -478,9 +478,14 @@ function deleteEvents30()
function askUnlockFields () {
// Ask
showModalWarning('<?= lang('Maintenance_Tool_UnlockFields_noti');?>', '<?= lang('Maintenance_Tool_UnlockFields_noti_text');?>',
'<?= lang('Gen_Cancel');?>', '<?= lang('Gen_Delete');?>', 'unlockFields');
'<?= lang('Gen_Cancel');?>', '<?= lang('Gen_Delete');?>', () => unlockFields(true));
}
function unlockFields() {
function unlockFields(clearAllFields) {
console.log("clearAllFields");
console.log(clearAllFields);
const apiBase = getApiBase();
const apiToken = getSetting("API_TOKEN");
const url = `${apiBase}/devices/fields/unlock`;
@@ -489,7 +494,7 @@ function unlockFields() {
const payload = {
mac: null, // null = all devices
fields: null, // null = all tracked fields
clearAll: true // clear all source values
clearAll: clearAllFields // clear all source values
};
$.ajax({

View File

@@ -70,6 +70,12 @@
</div>
<div class="col-md-10"><?= lang('Maintenance_Tool_del_unlockFields_selecteddev_text');?></div>
</div>
<div class="col-md-12">
<div class="col-md-2" style="">
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red" id="btnClearSourceFields" onclick="askClearSourceFields()"><?= lang('Maintenance_Tool_clearSourceFields_selected');?></button>
</div>
<div class="col-md-10"><?= lang('Maintenance_Tool_clearSourceFields_selected_text');?></div>
</div>
</div>
</div>
</div>
@@ -436,6 +442,18 @@ function askUnlockFieldsSelected () {
'unlockFieldsSelected');
}
// -----------------------------------------------------------------------------
// Ask to unlock fields of selected devices
function askClearSourceFields () {
// Ask
showModalWarning(
getString('Maintenance_Tool_clearSourceFields_selected_noti'),
getString('Gen_AreYouSure'),
getString('Gen_Cancel'),
getString('Gen_Okay'),
()=>unlockFieldsSelected(null, true));
}
// -----------------------------------------------------------------------------
// Unlock fields for selected devices
function unlockFieldsSelected(fields = null, clearAll = false) {
@@ -443,6 +461,7 @@ function unlockFieldsSelected(fields = null, clearAll = false) {
const macs_tmp = selectorMacs(); // returns array of MACs
console.log(macs_tmp);
console.log(clearAll);
if (!macs_tmp || macs_tmp == "" || macs_tmp.length === 0) {
@@ -461,7 +480,7 @@ function unlockFieldsSelected(fields = null, clearAll = false) {
const payload = {
mac: macsArray, // array of MACs for backend
fields: fields, // null for all tracked fields
clear_all: clearAll // true to clear all sources, false to clear only LOCKED/USER
clearAll: clearAll // true to clear all sources, false to clear only LOCKED/USER
};
$.ajax({

View File

@@ -21,8 +21,10 @@ function renderSmallBox($params) {
<a href="#" onclick="javascript: ' . htmlspecialchars($onclickEvent) . '">
<div class="small-box ' . htmlspecialchars($color) . '">
<div class="inner">
<h3 id="' . htmlspecialchars($headerId) . '" style="' . htmlspecialchars($headerStyle) . '"> ' . htmlspecialchars($dataValue) . ' </h3>
<p class="infobox_label">' . lang(htmlspecialchars($labelLang)) . '</p>
<div class="col-lg-6 col-sm-6 col-xs-6">
<div class="small-box-text col-lg-12 col-sm-12 col-xs-12" id="' . htmlspecialchars($headerId) . '" style="' . htmlspecialchars($headerStyle) . '"> <b>' . htmlspecialchars($dataValue) . '</b> </div>
</div>
<div class="infobox_label col-lg-6 col-sm-6 col-xs-6">' . lang(htmlspecialchars($labelLang)) . '</div>
</div>
<div class="icon">
<i id="' . htmlspecialchars($iconId) . '" class="' . htmlspecialchars($iconClass) . '"></i>

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "جاري إنشاء نسخة احتياطية...",
"Maintenance_Tool_backup_text": "إنشاء نسخة احتياطية من قاعدة البيانات",
"Maintenance_Tool_check_visible": "فحص المرئي",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "الوضع الداكن",
"Maintenance_Tool_darkmode_noti": "الوضع الداكن",
"Maintenance_Tool_darkmode_noti_text": "تم تغيير الوضع الداكن",
@@ -786,4 +789,4 @@
"settings_system_label": "نظام",
"settings_update_item_warning": "قم بتحديث القيمة أدناه. احرص على اتباع التنسيق السابق. <b>لم يتم إجراء التحقق.</b>",
"test_event_tooltip": "احفظ التغييرات أولاً قبل اختبار الإعدادات."
}
}

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Estàs segur que vols executar el Backup DB? Assegura't que no hi ha exploració en funcionament.",
"Maintenance_Tool_backup_text": "Les còpies de seguretat de la base de dades es troben al directori de bases de dades com a arxiu zip, anomenat amb la data de creació. No hi ha un nombre màxim de còpies de seguretat.",
"Maintenance_Tool_check_visible": "Desmarqueu-ho per amagar la columna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Canvia Modes (Fosc/Clar)",
"Maintenance_Tool_darkmode_noti": "Canvia Modes",
"Maintenance_Tool_darkmode_noti_text": "Després del canvi de tema, la pàgina intenta recarregar-se per activar el canvi. Si és necessari, s'ha de netejar la memòria cau.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "",
"Maintenance_Tool_backup_text": "",
"Maintenance_Tool_check_visible": "",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "",
"Maintenance_Tool_darkmode_noti": "",
"Maintenance_Tool_darkmode_noti_text": "",

View File

@@ -84,12 +84,12 @@
"DevDetail_EveandAl_NewDevice": "Neues Gerät",
"DevDetail_EveandAl_NewDevice_Tooltip": "Zeigt den Status „Neu“ für das Gerät an und nimmt es in Listen auf, wenn der Filter „Neue Geräte“ aktiv ist. Hat keine Auswirkungen auf Benachrichtigungen.",
"DevDetail_EveandAl_RandomMAC": "Zufällige MAC",
"DevDetail_EveandAl_ScanCycle": "Scan Abstand",
"DevDetail_EveandAl_ScanCycle": "Gerät scannen",
"DevDetail_EveandAl_ScanCycle_a": "Gerät scannen",
"DevDetail_EveandAl_ScanCycle_z": "Gerät nicht scannen",
"DevDetail_EveandAl_Skip": "pausiere wiederhol. Meldungen für",
"DevDetail_EveandAl_Skip": "Keine wiederholten Benachrichtigungen für",
"DevDetail_EveandAl_Title": "Konfiguration der Benachrichtigungen",
"DevDetail_Events_CheckBox": "Blende Verbindungs-Ereignisse aus",
"DevDetail_Events_CheckBox": "Verbindungsereignisse ausblenden",
"DevDetail_GoToNetworkNode": "Zur Netzwerkseite des angegebenen Knotens navigieren.",
"DevDetail_Icon": "Icon",
"DevDetail_Icon_Descr": "Geben Sie einen Font Awesome Icon-Namen ohne das Präfix „fa-“ ein oder die vollständige Klasse, z. B.: fa fa-brands fa-apple.",
@@ -102,10 +102,10 @@
"DevDetail_MainInfo_Network": "<i class=\"fa fa-server\"></i> Knoten (MAC)",
"DevDetail_MainInfo_Network_Port": "<i class=\"fa fa-ethernet\"></i> Port",
"DevDetail_MainInfo_Network_Site": "Seite",
"DevDetail_MainInfo_Network_Title": "Network",
"DevDetail_MainInfo_Network_Title": "Netzwerkdetails",
"DevDetail_MainInfo_Owner": "Eigen&shy;tümer",
"DevDetail_MainInfo_SSID": "SSID",
"DevDetail_MainInfo_Title": "Hauptinformation",
"DevDetail_MainInfo_Title": "Geräteinformationen",
"DevDetail_MainInfo_Type": "Typ",
"DevDetail_MainInfo_Vendor": "Hersteller",
"DevDetail_MainInfo_mac": "MAC",
@@ -209,7 +209,7 @@
"Device_MultiEdit_Tooltip": "Achtung! Beim Drücken werden alle Werte auf die oben ausgewählten Geräte übertragen.",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Success": "Gerät erfolgreich gespeichert",
"Device_Saved_Unexpected": "",
"Device_Searchbox": "Suche",
"Device_Shortcut_AllDevices": "Meine Geräte",
@@ -255,7 +255,7 @@
"Device_TableHead_SyncHubNodeName": "Synchronisationsknoten",
"Device_TableHead_Type": "Typ",
"Device_TableHead_Vendor": "Hersteller",
"Device_TableHead_Vlan": "",
"Device_TableHead_Vlan": "VLAN",
"Device_Table_Not_Network_Device": "Nicht konfiguriert als Netzwerkgerät",
"Device_Table_info": "Zeige _START_ bis _END_ von _TOTAL_ Einträgen",
"Device_Table_nav_next": "Nächste",
@@ -445,6 +445,9 @@
"Maintenance_Tool_backup_noti_text": "Sind Sie sicher, dass Sie die Datenbank jetzt sichern möchten. Prüfen Sie, dass gerade keine Scans stattfinden.",
"Maintenance_Tool_backup_text": "Die Datenbank-Sicher&shy;ungen befinden sich im Datenbank-Ver&shy;zeich&shy;nis, gepackt als zip-Archive, benannt mit dem Erstellungs&shy;datum. Es gibt keine maximale Anzahl von Backups.",
"Maintenance_Tool_check_visible": "Abwählen um die Spalte auszublenden.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Darstellungswechsel (Dunkel/Hell)",
"Maintenance_Tool_darkmode_noti": "Darstellungswechsel",
"Maintenance_Tool_darkmode_noti_text": "Wechselt zwischen der hellen und der dunklen Darstellung. Wenn die Umschaltung nicht ordentlich funktionieren sollte, versuchen Sie den Browsercache zu löschen.",

View File

@@ -414,8 +414,8 @@
"Maintenance_Tool_ImportPastedConfig": "Settings Import (paste)",
"Maintenance_Tool_ImportPastedConfig_noti_text": "Are you sure you want to import the pasted config settings? This will completely <b>overwrite</b> the <code>app.conf</code> file.",
"Maintenance_Tool_ImportPastedConfig_text": "Imports the <code>app.conf</code> file containing all the application Settings. You might want to download the current <code>app.conf</code> file first with the <b>Settings Export</b>.",
"Maintenance_Tool_UnlockFields": "Clear All Device Sources",
"Maintenance_Tool_UnlockFields_noti": "Clear All Device Sources",
"Maintenance_Tool_UnlockFields": "Unlock Device Fields",
"Maintenance_Tool_UnlockFields_noti": "Unlock Device Fields",
"Maintenance_Tool_UnlockFields_noti_text": "Are you sure you want to clear all source values (LOCKED/USER) for all device fields on all devices? This action cannot be undone.",
"Maintenance_Tool_UnlockFields_text": "This tool will remove all source values from every tracked field for all devices, effectively unlocking all fields for plugins and users. Use this with caution, as it will affect your entire device inventory.",
"Maintenance_Tool_arpscansw": "Toggle arp-Scan (on/off)",
@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Are you sure you want to execute the DB Backup? Be sure that no scan is currently running.",
"Maintenance_Tool_backup_text": "The database backups are located in the database directory as a zip-archive, named with the creation date. There is no maximum number of backups.",
"Maintenance_Tool_check_visible": "Uncheck to hide column.",
"Maintenance_Tool_clearSourceFields_selected": "Clear source fields",
"Maintenance_Tool_clearSourceFields_selected_noti": "Clear sources",
"Maintenance_Tool_clearSourceFields_selected_text": "This will clear all source fields of the selected devices. This action cannot be undone.",
"Maintenance_Tool_darkmode": "Toggle Modes (Dark/Light)",
"Maintenance_Tool_darkmode_noti": "Toggle Modes",
"Maintenance_Tool_darkmode_noti_text": "After the theme switch, the page tries to reload itself to activate the change. If necessary, the cache must be cleared.",

View File

@@ -443,6 +443,9 @@
"Maintenance_Tool_backup_noti_text": "¿Estás seguro de que quieres exactos la copia de seguridad de DB? Asegúrese de que ningún escaneo se esté ejecutando actualmente.",
"Maintenance_Tool_backup_text": "Las copias de seguridad de la base de datos se encuentran en el directorio de la base de datos como una Zip-Archive, nombrada con la fecha de creación. No hay un número máximo de copias de seguridad.",
"Maintenance_Tool_check_visible": "Desactivar para ocultar columna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Cambiar Modo (Dark/Light)",
"Maintenance_Tool_darkmode_noti": "Cambiar Modo",
"Maintenance_Tool_darkmode_noti_text": "Después del cambio de tema, la página intenta volver a cargar para activar el cambio. Si es necesario, el caché debe ser eliminado.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "",
"Maintenance_Tool_backup_text": "",
"Maintenance_Tool_check_visible": "",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "",
"Maintenance_Tool_darkmode_noti": "",
"Maintenance_Tool_darkmode_noti_text": "",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Êtes-vous sûr de vouloir lancer la sauvegarde de la base de données? Assurez-vous de ne pas avoir de scan en cours.",
"Maintenance_Tool_backup_text": "Les sauvegardes de base de données sont situées dans le répertoire de la base de données, soir forme d'archive ZIP, nommé selon la date de création. Il n'y a pas de limite de nombre de sauvegarde.",
"Maintenance_Tool_check_visible": "Décocher pour masquer la colonne.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Basculer de mode (clair/sombre)",
"Maintenance_Tool_darkmode_noti": "Basculer de mode",
"Maintenance_Tool_darkmode_noti_text": "Après le changement de thème, la page tente de se rafraîchir pour activer le changement. Si besoin, le cache doit être supprimé.",
@@ -786,4 +789,4 @@
"settings_system_label": "Système",
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. <b>Il n'y a pas de pas de contrôle.</b>",
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
}
}

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Sei sicuro di voler eseguire il backup del DB? Assicurati che nessuna scansione sia attualmente in esecuzione.",
"Maintenance_Tool_backup_text": "I backup del database si trovano nella directory del database come archivio zip, denominato con la data di creazione. Non esiste un numero massimo di backup.",
"Maintenance_Tool_check_visible": "Deseleziona per nascondere la colonna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Alterna modalità (Scuro/Chiaro)",
"Maintenance_Tool_darkmode_noti": "Alterna modalità",
"Maintenance_Tool_darkmode_noti_text": "Dopo il cambio di tema, la pagina tenta di ricaricarsi per attivare la modifica. Potrebbe essere necessaria la cancellazione della cache.",
@@ -786,4 +789,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. <b>La convalida non viene eseguita.</b>",
"test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni."
}
}

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "データベースのバックアップを実行してもよろしいですか? 現在スキャンが実行されていないことを確認してください。",
"Maintenance_Tool_backup_text": "データベースのバックアップは、作成日をファイル名としたzipアーカイブとしてデータベースディレクトリ内に配置されます。バックアップの最大数は存在しません。",
"Maintenance_Tool_check_visible": "チェックを外すと列を非表示にします。",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "モード切替(ダーク/ライト)",
"Maintenance_Tool_darkmode_noti": "モード切替",
"Maintenance_Tool_darkmode_noti_text": "テーマ変更後、変更を有効化するためにページを再読み込みします。必要に応じて、キャッシュをクリアする必要があります。",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Er du sikker på at du vil kjøre Database Sikkerhetskopiering? Pass på at ingen skanning kjører for øyeblikket.",
"Maintenance_Tool_backup_text": "Databasesikkerhetskopiene er plassert i databasekatalogen som et zip-arkiv, navngitt med opprettelsesdatoen. Det er ikke noe maksimalt antall sikkerhetskopier.",
"Maintenance_Tool_check_visible": "Fjern merket for å skjule kolonne.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Bytt Modus (mørk/lys)",
"Maintenance_Tool_darkmode_noti": "Bytt Modus",
"Maintenance_Tool_darkmode_noti_text": "Etter tema bytte, prøver siden å laste seg inn på nytt for å aktivere endringen. Om nødvendig må hurtigbufferen tømmes.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Czy na pewno chcesz wykonać kopię zapasową bazy danych? Upewnij się, że żadne skanowanie nie jest obecnie uruchomione.",
"Maintenance_Tool_backup_text": "Kopie zapasowe bazy danych są zapisywane w katalogu bazy danych jako archiwum ZIP, nazwane zgodnie z datą utworzenia. Nie ma ustalonego limitu liczby kopii zapasowych.",
"Maintenance_Tool_check_visible": "Usuń zaznaczenie, aby ukryć kolumnę.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Przełącz tryb (Ciemny/Jasny)",
"Maintenance_Tool_darkmode_noti": "Przełącz tryb",
"Maintenance_Tool_darkmode_noti_text": "Po zmianie motywu strona próbuje przeładować się, aby zastosować zmiany. W razie potrzeby należy wyczyścić pamięć podręczną (cache).",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Tem a certeza de que pretende executar a cópia de segurança da BD? Certifique-se de que não está a ser executada nenhuma verificação.",
"Maintenance_Tool_backup_text": "Os backups do banco de dados estão localizados no diretório do banco de dados como um zip-archive, nomeado com a data de criação. Não há nenhum número máximo de backups.",
"Maintenance_Tool_check_visible": "Desmarque para esconder a coluna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Modos de alternância (escuro/claro)",
"Maintenance_Tool_darkmode_noti": "Modos de alternância",
"Maintenance_Tool_darkmode_noti_text": "Após a mudança de tema, a página tenta recarregar-se para ativar a alteração. Se necessário, a cache deve ser limpa.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Tem a certeza de que pretende executar a cópia de segurança da BD? Certifique-se de que não está a ser executada nenhuma verificação.",
"Maintenance_Tool_backup_text": "Os backups da base de dados estão localizadas no diretório da base de dados como um arquivo zip, nomeado com a data de criação. Não há nenhum número máximo de backups.",
"Maintenance_Tool_check_visible": "Desmarque para esconder a coluna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Modos de alternância (escuro/claro)",
"Maintenance_Tool_darkmode_noti": "Modos de alternância",
"Maintenance_Tool_darkmode_noti_text": "Após a mudança de tema, a página tenta recarregar-se para ativar a alteração. Se necessário, a cache deve ser limpa.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Вы уверены, что хотите выполнить резервное копирование БД? Убедитесь, что в данный момент сканирование не выполняется.",
"Maintenance_Tool_backup_text": "Резервные копии базы данных располагаются в каталоге базы данных в виде zip-архива, имя которого соответствует дате создания. Максимального количества резервных копий не существует.",
"Maintenance_Tool_check_visible": "Снимите флажок, чтобы скрыть столбец.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Тема (Темная/Светлая)",
"Maintenance_Tool_darkmode_noti": "Переключение режимов",
"Maintenance_Tool_darkmode_noti_text": "После переключения темы страница пытается перезагрузиться, чтобы активировать изменение. При необходимости кэш необходимо очистить.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "",
"Maintenance_Tool_backup_text": "",
"Maintenance_Tool_check_visible": "",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "",
"Maintenance_Tool_darkmode_noti": "",
"Maintenance_Tool_darkmode_noti_text": "",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "DB Yedeklemesini yürütmek istediğinizden emin misiniz? Şu anda hiçbir taramanın çalışmadığından emin olun.",
"Maintenance_Tool_backup_text": "Veritabanı yedekleri, veritabanı dizininde, oluşturulma tarihiyle adlandırılan bir zip arşivi olarak bulunur. Maksimum yedekleme sayısı yoktur.",
"Maintenance_Tool_check_visible": "Sütunu gizlemek için işareti kaldırın.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Modları Değiştir (Koyu/Açık)",
"Maintenance_Tool_darkmode_noti": "Modları Aç/Kapat",
"Maintenance_Tool_darkmode_noti_text": "Tema geçişinden sonra sayfa, değişikliği etkinleştirmek için kendini yeniden yüklemeye çalışır. Gerekirse, önbellek temizlenmelidir.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "Ви впевнені, що хочете виконати резервне копіювання БД? Переконайтеся, що сканування наразі не виконується.",
"Maintenance_Tool_backup_text": "Резервні копії бази даних зберігаються в каталозі бази даних у вигляді zip-архіву з датою створення. Немає максимальної кількості резервних копій.",
"Maintenance_Tool_check_visible": "Зніміть прапорець, щоб приховати стовпець.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "Перемикати режими (темний/світлий)",
"Maintenance_Tool_darkmode_noti": "Перемикати режими",
"Maintenance_Tool_darkmode_noti_text": "Після перемикання теми сторінка намагається перезавантажитися, щоб активувати зміну. При необхідності необхідно очистити кеш.",

View File

@@ -427,6 +427,9 @@
"Maintenance_Tool_backup_noti_text": "您确定要执行数据库备份吗?请确保当前没有正在运行的扫描。",
"Maintenance_Tool_backup_text": "数据库备份以 zip 存档形式位于数据库目录中,以创建日期命名。备份数量没有上限。",
"Maintenance_Tool_check_visible": "取消选择隐藏列。",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "切换模式(暗/亮)",
"Maintenance_Tool_darkmode_noti": "切换模式",
"Maintenance_Tool_darkmode_noti_text": "主题切换后,页面会尝试重新加载以激活更改。如有必要,必须清除缓存。",
@@ -760,7 +763,7 @@
"run_event_tooltip": "在运行之前,请先启用设置并保存更改。",
"select_icon_event_tooltip": "选择图标",
"settings_core_icon": "fa-solid fa-gem",
"settings_core_label": "核心",
"settings_core_label": "主要",
"settings_device_scanners": "设备扫描器用于发现写入当前扫描数据库表的设备。",
"settings_device_scanners_icon": "fa-solid fa-magnifying-glass-plus",
"settings_device_scanners_info": "使用 <a href=\"/settings.php#LOADED_PLUGINS\">LOADED_PLUGINS</a> 设置加载更多设备扫描仪",
@@ -786,4 +789,4 @@
"settings_system_label": "系统",
"settings_update_item_warning": "更新下面的值。请注意遵循先前的格式。<b>未执行验证。</b>",
"test_event_tooltip": "在测试设置之前,请先保存更改。"
}
}

View File

@@ -418,7 +418,6 @@
},
{
"column": "Watched_Value1",
"mapped_to_column": "scanName",
"css_classes": "col-sm-2",
"show": true,
"type": "label",

View File

@@ -1,6 +1,6 @@
site_name: NetAlertX Documentation
site_name: Documentation
site_url: https://docs.netalertx.com
repo_url: https://github.com/jokob-sk/NetAlertX/
repo_url: https://github.com/netalertx/NetAlertX/
edit_uri: blob/main/docs/
docs_dir: docs
use_directory_urls: true
@@ -141,7 +141,7 @@ theme:
custom_dir: docs/overrides
metadata:
description: "NetAlertX Documentation - The go-to resource for all things related to NetAlertX."
image: "https://raw.githubusercontent.com/jokob-sk/NetAlertX/main/front/img/netalertx_docs.png"
image: "https://raw.githubusercontent.com/netalertx/NetAlertX/main/front/img/netalertx_docs.png"
extra:
home_hide_sidebar: true
analytics:

View File

@@ -4,7 +4,7 @@ import os
# flake8: noqa: E402
from flask import Flask, request, jsonify, Response
from flask import Flask, redirect, request, jsonify, url_for, Response
from models.device_instance import DeviceInstance # noqa: E402
from flask_cors import CORS
from werkzeug.exceptions import HTTPException
@@ -1165,6 +1165,11 @@ def api_docs():
return send_from_directory(openapi_dir, 'swagger.html')
@app.route('/')
def index_redirect():
"""Redirect root to API documentation."""
return redirect(url_for('api_docs'))
# --------------------------
# DB query
# --------------------------
@@ -1401,7 +1406,7 @@ def api_create_event(mac, payload=None):
def api_events_by_mac(mac, payload=None):
"""Delete events for a specific device MAC; string converter keeps this distinct from /events/<int:days>."""
device_handler = DeviceInstance()
result = device_handler.deleteDeviceEvents(mac)
return jsonify(result)
@@ -1740,7 +1745,8 @@ def api_write_notification(payload=None):
auth_callable=is_authorized
)
def api_get_unread_notifications(payload=None):
return get_unread_notifications()
notifications = get_unread_notifications()
return jsonify(notifications)
@app.route("/messaging/in-app/read/all", methods=["POST"])

View File

@@ -231,7 +231,7 @@ def get_unread_notifications():
notifications = json.load(f)
unread = [n for n in notifications if n.get("read", 0) == 0]
return jsonify(unread)
return unread
def mark_notification_as_read(guid=None, max_attempts=3):
@@ -283,6 +283,13 @@ def mark_notification_as_read(guid=None, max_attempts=3):
return {"success": False, "error": error_msg}
def update_unread_notifications_count():
"""
Re-broadcast unread notifications for the frontend .
"""
broadcast_unread_notifications_count(len(get_unread_notifications()))
def delete_notification(guid):
"""
Delete a notification from the notifications file based on its GUID.

View File

@@ -18,7 +18,7 @@ from db.authoritative_handler import (
unlock_fields
)
from helper import is_random_mac, get_setting_value
from utils.datetime_utils import timeNowDB, format_date
from utils.datetime_utils import timeNowDB
class DeviceInstance:
@@ -489,8 +489,8 @@ class DeviceInstance:
return None
device_data = row_to_json(list(row.keys()), row)
device_data["devFirstConnection"] = format_date(device_data["devFirstConnection"])
device_data["devLastConnection"] = format_date(device_data["devLastConnection"])
device_data["devFirstConnection"] = device_data["devFirstConnection"]
device_data["devLastConnection"] = device_data["devLastConnection"]
device_data["devIsRandomMAC"] = is_random_mac(device_data["devMac"])
# Fetch children

View File

@@ -212,8 +212,8 @@ class plugin_manager:
# Run test
self.handle_run(runType)
# Remove sample notification
notificationObj.remove(notificationObj.GUID)
# Save notification
notificationObj.upsert()
mylog("minimal", ["[Test] END Test: ", runType])

View File

@@ -17,6 +17,7 @@ from db.db_helper import print_table_schema
from utils.datetime_utils import timeNowDB
from logger import mylog, Logger
from messaging.reporting import skip_repeated_notifications
from messaging.in_app import update_unread_notifications_count
# Make sure log level is initialized correctly
@@ -104,6 +105,9 @@ def process_scan(db):
# 🐛 CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes
db.sql.execute("DELETE FROM CurrentScan")
# re-broadcast unread notifiation count to update FE
update_unread_notifications_count()
# Commit changes
db.commitDB()

View File

@@ -112,12 +112,29 @@ def normalizeTimeStamp(inputTimeStamp):
# -------------------------------------------------------------------------------------------
def format_date_iso(date1: str) -> Optional[str]:
"""Return ISO 8601 string for a date or None if empty"""
if not date1:
def format_date_iso(date_val: str) -> Optional[str]:
"""Ensures a date string from DB is returned as a proper ISO string with TZ."""
if not date_val:
return None
dt = datetime.datetime.fromisoformat(date1) if isinstance(date1, str) else date1
return dt.isoformat()
try:
# 1. Parse the string from DB (e.g., "2026-01-20 07:58:18")
if isinstance(date_val, str):
# Use a more flexible parser if it's not strict ISO
dt = datetime.datetime.fromisoformat(date_val.replace(" ", "T"))
else:
dt = date_val
# 2. If it has no timezone, ATTACH (don't convert) your config TZ
if dt.tzinfo is None:
target_tz = conf.tz if isinstance(conf.tz, datetime.tzinfo) else ZoneInfo(conf.tz)
dt = dt.replace(tzinfo=target_tz)
# 3. Return the string. .isoformat() will now include the +11:00 or +10:00
return dt.isoformat()
except Exception as e:
print(f"Error formatting date: {e}")
return str(date_val)
# -------------------------------------------------------------------------------------------
@@ -156,21 +173,35 @@ def parse_datetime(dt_str):
def format_date(date_str: str) -> str:
try:
if isinstance(date_str, str):
# collapse all whitespace into single spaces
date_str = re.sub(r"\s+", " ", date_str.strip())
if not date_str:
return ""
date_str = re.sub(r"\s+", " ", str(date_str).strip())
dt = parse_datetime(date_str)
if dt.tzinfo is None:
if isinstance(conf.tz, str):
dt = dt.replace(tzinfo=ZoneInfo(conf.tz))
else:
dt = dt.replace(tzinfo=conf.tz)
if not dt:
return f"invalid:{repr(date_str)}"
# If the DB has no timezone, we tell Python what it IS,
# we don't CONVERT it.
if dt.tzinfo is None:
# Option A: If the DB time is already AEDT, use AEDT.
# Option B: Use conf.tz if that is your 'source of truth'
dt = dt.replace(tzinfo=conf.tz)
return dt.astimezone().isoformat()
# IMPORTANT: Return the ISO format of the object AS IS.
# Calling .astimezone() here triggers a conversion to the
# System Local Time , which is causing your shift.
return dt.isoformat()
except Exception:
return f"invalid:{repr(date_str)}"
except Exception as e:
return f"invalid:{repr(date_str)} e: {e}"
def format_date_diff(date1, date2, tz_name):

View File

@@ -0,0 +1,23 @@
# test/api_endpoints/test_docs.py - Tests for root redirect and API docs endpoints
from api_server import api_server_start
def test_index_redirect_logic():
"""Test the redirect function logic directly."""
with api_server_start.app.test_client() as client:
response = client.get("/", follow_redirects=False)
assert response.status_code == 302
assert response.location == '/docs'
def test_api_docs_logic():
"""Test that api_docs attempts to serve the correct file."""
with api_server_start.app.test_client() as client:
response = client.get("/docs")
assert response.status_code == 200
assert response.mimetype == "text/html"
response.direct_passthrough = False
data = response.get_data()
assert b"<!DOCTYPE html>" in data or b"<html>" in data