-
-
-
-
-
-
+
+
-
- Stamdata
-
-
-
- Type
- {{ hardware.asset_type|title }}
-
- {% if hardware.internal_asset_id %}
-
- Intern ID
- {{ hardware.internal_asset_id }}
-
- {% endif %}
- {% if hardware.customer_asset_id %}
-
- Kunde ID
- {{ hardware.customer_asset_id }}
-
- {% endif %}
- {% if hardware.warranty_until %}
-
- Garanti Udløb
- {{ hardware.warranty_until }}
-
- {% endif %}
- {% if hardware.end_of_life %}
-
- End of Life
- {{ hardware.end_of_life }}
-
- {% endif %}
- -
+
- + + +
- + + +
- + + +
- + + +
-
-
-
-
-
- AnyDesk
-
-
-
- AnyDesk ID
- {{ hardware.anydesk_id or '-' }}
-
-
- AnyDesk Link
-
- {% if hardware.anydesk_link %}
- Connect
- {% else %}
- -
- {% endif %}
-
-
-
-
-
- Tags
-
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
-
-
-
+
+
-
+
-
+
- Stamdata
-
-
-
-
-
- Nuværende Lokation
-
-
- {% if locations and locations|length > 0 %}
- {% set current_loc = locations[0] %}
- {% if not current_loc.end_date %}
-
-
-
- {% if current_loc.notes %}
-
-
-
-
-
- {{ current_loc.location_name or 'Ukendt' }}
- Siden {{ current_loc.start_date }} -
- {{ current_loc.notes }}
-
- {% endif %}
- {% else %}
-
-
-
- {% endif %}
- {% else %}
- Ingen aktiv lokation
-
-
- {% endif %}
- Hardwaret er ikke tildelt en lokation
- -
-
-
-
-
- Nuværende Ejer
-
- {% if ownership and ownership|length > 0 %}
- {% set current_own = ownership[0] %}
- {% if not current_own.end_date %}
-
-
-
- {% else %}
-
-
-
-
-
- - {{ current_own.customer_name or current_own.owner_type|title }} -
- Siden {{ current_own.start_date }} -Ingen aktiv ejer registreret
- {% endif %} - {% else %} -Ingen ejerhistorik
- {% endif %} -
-
-
-
-
-
-
-
-
-
-
-
- Opret Sag
-
-
-
-
-
- Skift Lokation
-
-
-
-
-
-
-
-
- Ny Lokation
-
-
-
-
-
- Tilføj Bilag
-
-
-
- -
-
- - - -
- - - -
- - - -
- - - -
-
-
-
-
-
Kombineret Historik
- -
-
-
-
-
-
- Placeringer
- {% if locations %}
- {% for loc in locations %}
-
+
+
+ {% set anydesk_url = hardware.anydesk_id and ('anydesk://' ~ hardware.anydesk_id) %}
+
-
-
- {% endfor %}
- {% else %}
-
-
- {{ loc.location_name or 'Ukendt' }} ({{ loc.start_date }} {% if loc.end_date %} - {{ loc.end_date }}{% else %}- nu{% endif %})
- {% if loc.notes %}{{ loc.notes }}
{% endif %}
- Ingen lokations historik
+
+
+
+ Type
+
+ {% if hardware.asset_type == 'pc' %}🖥️ PC
+ {% elif hardware.asset_type == 'laptop' %}💻 Laptop
+ {% elif hardware.asset_type == 'printer' %}🖨️ Printer
+ {% elif hardware.asset_type == 'skærm' %}🖥️ Skærm
+ {% elif hardware.asset_type == 'telefon' %}📱 Telefon
+ {% elif hardware.asset_type == 'server' %}🗄️ Server
+ {% elif hardware.asset_type == 'netværk' %}🌐 Netværk
+ {% else %}📦 {{ hardware.asset_type|title }}
{% endif %}
-
-
-
- Ejerskab
- {% if ownership %}
- {% for own in ownership %}
-
+
-
-
- {% endfor %}
+
+
-
- {{ own.customer_name or own.owner_type }} ({{ own.start_date }} {% if own.end_date %} - {{ own.end_date }}{% else %}- nu{% endif %})
- {% if own.notes %}{{ own.notes }}
{% endif %}
-
+ Mærke/Model
+ {{ hardware.brand or '-' }} / {{ hardware.model or '-' }}
+
+ {% if hardware.internal_asset_id %}
+
+ Internt Asset ID
+ {{ hardware.internal_asset_id }}
+
+ {% endif %}
+ {% if hardware.customer_asset_id %}
+
+ Kunde Asset ID
+ {{ hardware.customer_asset_id }}
+
+ {% endif %}
+ {% if hardware.warranty_until %}
+
+ Garanti til
+ {{ hardware.warranty_until }}
+
+ {% endif %}
+ {% if hardware.end_of_life %}
+
+ End of Life
+ {{ hardware.end_of_life }}
+
+ {% endif %}
+
+
+
+
+
+
+ Remote Access
+ + Ændre + +
+
+
+ AnyDesk ID
+
+ {% if anydesk_url %}
+ {{ hardware.anydesk_id }}
{% else %}
-
+ Ingen ejerskabs historik
+ - + {% endif %} + +
+ Handling
+
+ {% if anydesk_url %}
+
+ Connect AnyDesk
+
+ {% else %}
+ Ingen link
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ Lokation
+ +
+ {% if current_loc and not current_loc.end_date %}
+
+
+ {% else %}
+
+
{{ current_loc.location_name }}
+Siden: {{ current_loc.start_date }}
+ {% if current_loc.notes %} +"{{ current_loc.notes }}"
+ {% endif %}
+
+
{% endif %}
Ingen aktiv lokation
+ +
+
+
+
+
+
+ Ejer
+
+ {% if current_owner and not current_owner.end_date %}
+
+
+
+ {% else %}
+
+
{{ current_owner.customer_name or current_owner.owner_type|title }}
+Siden: {{ current_owner.start_date }}
+
+
+ {% endif %}
+ Ingen aktiv ejer
+
+
+
+
+
+
+
+
-
-
+
+
+
+ Seneste Sager
+
{% if cases and cases|length > 0 %}
-
+
- {% for case in cases %}
-
-
+
-
- {{ case.titel }}
- {{ case.created_at }} -
- Status: {{ case.status }}
- ID: {{ case.case_id }}
+ {% for case in cases[:5] %}
+
+
+ {% if cases|length > 5 %}
+
+ {% endif %}
{% else %}
-
+
+
+
+ {{ case.titel }}
+
+ {{ case.status }}
{{ case.created_at.strftime('%Y-%m-%d') if case.created_at else '' }}
{% endfor %}
-
-
-
+ Ingen sager tilknyttet.
- +
+ Ingen sager tilknyttet
{% endif %}
-
- {% if attachments %}
- {% for att in attachments %}
-
-
-
📎
- {{ att.file_name }}
- {{ att.uploaded_at }}
+
+
+
@@ -693,19 +680,6 @@
}
}
} else {
- // Hide if not a match and doesn't contain matches
- // BUT be careful not to hide if a parent matched?
- // Actually, search usually filters down. If parent matches, should we show all children?
- // Let's stick to showing matches and path to matches.
-
- // Important: logic is tricky with flat recursion vs nested DOM
- // My macro structure is nested: .location-item-container contains children-container which contains .location-item-container
- // So `container.style.display = 'block'` on a parent effectively shows the wrapper.
-
- // If I am not a match, and I have no children that are matches...
- // But wait, if my parent is a match, do I show up?
- // Usually "Search" filters items out.
-
if (isMatch || hasChildMatch) {
container.style.display = 'block';
} else {
@@ -758,4 +732,4 @@
}
});
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/app/modules/hardware/templates/edit.html b/app/modules/hardware/templates/edit.html
index 4173827..0494057 100644
--- a/app/modules/hardware/templates/edit.html
+++ b/app/modules/hardware/templates/edit.html
@@ -210,6 +210,15 @@
+
+
+
+
+
+
-
-
-
+
Lokations Historik
+
+ {% if locations %}
+ {% for loc in locations %}
+
- {% endfor %}
- {% else %}
-
+
+
-
+
{{ loc.location_name or 'Ukendt' }}
+
+ {{ loc.start_date }}
+ {% if loc.end_date %} - {{ loc.end_date }}{% else %} Nuværende{% endif %}
+
+ {% if loc.notes %}"{{ loc.notes }}"
{% endif %}
+
- Ingen filer vedhæftet
-
- {% endif %}
+ {% endfor %}
+ {% else %}
+ Ingen historik.
+ {% endif %}
-
- {% if hardware.notes %}
- {{ hardware.notes }}
+
+
@@ -540,6 +479,54 @@
Ejerskabs Historik
+
+ {% if ownership %}
+ {% for own in ownership %}
+
+
+
+ {% endfor %}
{% else %}
- Ingen noter...
+
+
+ {{ own.customer_name or own.owner_type }}
+
+ {{ own.start_date }}
+ {% if own.end_date %} - {{ own.end_date }}{% else %} Nuværende{% endif %}
+
+ {% if own.notes %}"{{ own.notes }}"
{% endif %}
+ Ingen historik.
{% endif %}
+
+
+
+
+
+
+
+
+
+
+ Noter
+
+ {% if hardware.notes %}
+
+ {{ hardware.notes }}
+ {% else %}
+ Ingen noter tilføjet...
+ {% endif %}
+
+ Rediger Noter
+
+
+ {% if hardware.anydesk_id %}
+
+ {% if hardware.anydesk_link %}
+ Test forbindelse
+ {% else %}
+ Test forbindelse
+ {% endif %}
+
+ {% endif %}
diff --git a/app/modules/hardware/templates/eset_import.html b/app/modules/hardware/templates/eset_import.html
new file mode 100644
index 0000000..2533012
--- /dev/null
+++ b/app/modules/hardware/templates/eset_import.html
@@ -0,0 +1,293 @@
+{% extends "shared/frontend/base.html" %}
+
+{% block title %}ESET Import - Hardware - BMC Hub{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+ ⬇️ ESET Import
+ +
+
+
+
+ Ingen data indlaest
+
+
+
+
+
+
+ | Navn | +Serial | +Gruppe | +Device UUID | +Handling | +
|---|---|---|---|---|
| Klik "Hent devices" for at hente ESET-listen. | +||||
+
+{% endblock %}
+
+{% block extra_js %}
+
+{% endblock %}
diff --git a/app/modules/hardware/templates/eset_overview.html b/app/modules/hardware/templates/eset_overview.html
new file mode 100644
index 0000000..0cc3ad5
--- /dev/null
+++ b/app/modules/hardware/templates/eset_overview.html
@@ -0,0 +1,163 @@
+{% extends "shared/frontend/base.html" %}
+
+{% block title %}ESET Oversigt - Hardware - BMC Hub{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+ Import fra ESET
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/app/modules/hardware/templates/eset_test.html b/app/modules/hardware/templates/eset_test.html
new file mode 100644
index 0000000..d3a4cf5
--- /dev/null
+++ b/app/modules/hardware/templates/eset_test.html
@@ -0,0 +1,121 @@
+{% extends "shared/frontend/base.html" %}
+
+{% block title %}ESET Test - Hardware - BMC Hub{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+
+ 🛡️ ESET Oversigt
+ +
+
+
+ 🔗 Matchede enheder
+ {% if matches %}
+
+
+
+
+
+ {% else %}
+ | Hardware | +Serial | +ESET UUID | +ESET Gruppe | +Kontakt | +Company | +Kunde | +Opdateret | +
|---|---|---|---|---|---|---|---|
| + + {{ row.brand or '' }} {{ row.model or '' }} + + | +{{ row.serial_number or '-' }} | +{{ row.eset_uuid or '-' }} | +{{ row.eset_group or '-' }} | ++ {% if row.contact_id %} + {{ row.first_name or '' }} {{ row.last_name or '' }} + {% else %} + - + {% endif %} + | +{{ row.user_company or '-' }} | +{{ row.customer_name or '-' }} | +{{ row.updated_at or '-' }} | +
Ingen matchede enheder fundet endnu.
+ {% endif %}
+
+
+🚨 Kritiske incidents
+ {% if incidents %}
+
+
+
+
+
+ {% else %}
+ | Severity | +Status | +Device UUID | +Detected | +Last Seen | +
|---|---|---|---|---|
| + {% set sev = (inc.severity or '')|lower %} + + {{ inc.severity or 'critical' }} + + | +{{ inc.status or '-' }} | +{{ inc.device_uuid or '-' }} | +{{ inc.detected_at or '-' }} | +{{ inc.last_seen or '-' }} | +
Ingen kritiske incidents lige nu.
+ {% endif %}
+
+
+{% endblock %}
+
+{% block extra_js %}
+
+{% endblock %}
diff --git a/app/modules/hardware/templates/index.html b/app/modules/hardware/templates/index.html
index 6626e9a..16b7fc6 100644
--- a/app/modules/hardware/templates/index.html
+++ b/app/modules/hardware/templates/index.html
@@ -242,10 +242,16 @@
{% block content %}
+
+
+ 🧪 ESET Test
+ +
+
+
+ Test device list (raw)
+
+
+
+
+ Tryk "Hent devices" for at teste forbindelsen.
+
+
+Test single device
+
+
+
+
+ Indtast en UUID og klik "Hent device".
+
@@ -319,6 +325,18 @@
Type:
{{ item.asset_type|title }}
+ {% if item.anydesk_id or item.anydesk_link %}
+
+ AnyDesk:
+
+ {% if item.anydesk_link %}
+ {{ item.anydesk_id or 'Åbn' }}
+ {% elif item.anydesk_id %}
+ {{ item.anydesk_id }}
+ {% endif %}
+
+
+ {% endif %}
{% if item.customer_name %}
Ejer:
diff --git a/app/modules/sag/templates/create.html b/app/modules/sag/templates/create.html
index f8604c6..063ac53 100644
--- a/app/modules/sag/templates/create.html
+++ b/app/modules/sag/templates/create.html
@@ -220,10 +220,7 @@
@@ -1341,6 +1342,21 @@
checkMaintenanceMode();
}
}, 30000);
+
+ // Global Logout Function
+ function logoutUser(event) {
+ if (event) event.preventDefault();
+
+ // Clear local storage
+ localStorage.removeItem('access_token');
+ localStorage.removeItem('user');
+
+ // Clear cookies
+ document.cookie = "access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
+
+ // Redirect to login
+ window.location.href = '/login';
+ }
{% block scripts %}{% endblock %}
diff --git a/app/timetracking/frontend/service_contract_wizard.html b/app/timetracking/frontend/service_contract_wizard.html
index d29964a..ccd2ad5 100644
--- a/app/timetracking/frontend/service_contract_wizard.html
+++ b/app/timetracking/frontend/service_contract_wizard.html
@@ -848,7 +848,7 @@
}
// vTiger's 'duration' field is in seconds, convert to minutes
- if (fieldName === 'duration' && typeof rawValue !== 'string') {
+ if (fieldName === 'duration') {
const seconds = typeof rawValue === 'number' ? rawValue : parseFloat(String(rawValue));
if (Number.isFinite(seconds)) {
return seconds / 60;
@@ -1002,6 +1002,17 @@
const row = document.createElement('tr');
const hoursData = getTimelogHours(item);
const hours = normalizeTimelogHours(hoursData.value, hoursData.field);
+
+ // DEBUG: Log første entry for at verificere parsing
+ if (timelogs.indexOf(item) === 0) {
+ console.log('🔍 TIMELOG DEBUG:', {
+ id: item.id,
+ hoursData: hoursData,
+ parsedHours: hours,
+ rawItem: item
+ });
+ }
+
const relatedId = getTimelogRelatedId(item) || '-';
const relatedCase = caseMap.get(relatedId);
const caseNumber = relatedCase
diff --git a/main.py b/main.py
index 03c0b9e..df12118 100644
--- a/main.py
+++ b/main.py
@@ -124,6 +124,19 @@ async def lifespan(app: FastAPI):
replace_existing=True
)
logger.info("✅ Reminder job scheduled (every 5 minutes)")
+
+ if settings.ESET_ENABLED and settings.ESET_SYNC_ENABLED:
+ from app.jobs.eset_sync import run_eset_sync
+
+ backup_scheduler.scheduler.add_job(
+ func=run_eset_sync,
+ trigger=IntervalTrigger(minutes=settings.ESET_SYNC_INTERVAL_MINUTES),
+ id='eset_sync',
+ name='ESET Sync',
+ max_instances=1,
+ replace_existing=True
+ )
+ logger.info("✅ ESET sync job scheduled (every %d minutes)", settings.ESET_SYNC_INTERVAL_MINUTES)
logger.info("✅ System initialized successfully")
yield
diff --git a/migrations/117_add_eset_fields.sql b/migrations/117_add_eset_fields.sql
new file mode 100644
index 0000000..7480ed4
--- /dev/null
+++ b/migrations/117_add_eset_fields.sql
@@ -0,0 +1,8 @@
+-- Add ESET integration fields to hardware_assets
+-- Enables storing ESET UUID and raw hardware specs
+
+ALTER TABLE hardware_assets
+ADD COLUMN IF NOT EXISTS eset_uuid VARCHAR(100),
+ADD COLUMN IF NOT EXISTS hardware_specs JSONB;
+
+CREATE INDEX IF NOT EXISTS idx_hardware_eset_uuid ON hardware_assets(eset_uuid);
diff --git a/migrations/118_eset_sync.sql b/migrations/118_eset_sync.sql
new file mode 100644
index 0000000..5fa432f
--- /dev/null
+++ b/migrations/118_eset_sync.sql
@@ -0,0 +1,30 @@
+-- ESET sync support: hardware contacts + incidents cache
+
+CREATE TABLE IF NOT EXISTS hardware_contacts (
+ id SERIAL PRIMARY KEY,
+ hardware_id INT NOT NULL REFERENCES hardware_assets(id) ON DELETE CASCADE,
+ contact_id INT NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
+ role VARCHAR(50) DEFAULT 'primary',
+ source VARCHAR(50) DEFAULT 'eset',
+ created_at TIMESTAMP DEFAULT NOW(),
+ UNIQUE(hardware_id, contact_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_hardware_contacts_hardware ON hardware_contacts(hardware_id);
+CREATE INDEX IF NOT EXISTS idx_hardware_contacts_contact ON hardware_contacts(contact_id);
+
+CREATE TABLE IF NOT EXISTS eset_incidents (
+ id SERIAL PRIMARY KEY,
+ incident_uuid VARCHAR(100) UNIQUE,
+ severity VARCHAR(50),
+ status VARCHAR(50),
+ device_uuid VARCHAR(100),
+ detected_at TIMESTAMP,
+ last_seen TIMESTAMP,
+ payload JSONB,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_eset_incidents_severity ON eset_incidents(severity);
+CREATE INDEX IF NOT EXISTS idx_eset_incidents_device ON eset_incidents(device_uuid);
diff --git a/migrations/119_add_eset_group.sql b/migrations/119_add_eset_group.sql
new file mode 100644
index 0000000..af7af1f
--- /dev/null
+++ b/migrations/119_add_eset_group.sql
@@ -0,0 +1,6 @@
+-- Add ESET group field to hardware_assets
+
+ALTER TABLE hardware_assets
+ADD COLUMN IF NOT EXISTS eset_group TEXT;
+
+CREATE INDEX IF NOT EXISTS idx_hardware_eset_group ON hardware_assets(eset_group);
-
-
-
-
+
Link genereres automatisk.