scada:events

Розбіжності

Тут показані розбіжності між вибраною ревізією та поточною версією сторінки.

Посилання на цей список змін

Порівняння попередніх версій Попередня ревізія
Наступна ревізія
Попередня ревізія
scada:events [2026/08/28 10:39] scadascada:events [2026/08/28 10:58] (поточний) scada
Рядок 1: Рядок 1:
 ====== Логування подій керування (MWJ Audit Service) ====== ====== Логування подій керування (MWJ Audit Service) ======
  
-Опис розгортання служби логування дій операторів (вхід/вихід, запис тегів) для проєкту **UA_Energy_Demo_23_04** на базі EcoStruxure Power Operation v2024.+===== Опис =====
  
-===== Загальна схема =====+Служба логування подій керування (вхід/вихід операторів, запис тегів) для проєкту **UA_Energy_Demo_23_04**.
  
-  * **mwj_audit_service.py** — фоновий Python-скрипт, який кожну секунду сканує журнал подій (``*.MWJ``) хард-клієнта AlarmServer, витягує події ``Login``, ``Logout``, ``TagWrite`` і віддає їх у вигляді JSON по HTTP на ``127.0.0.1:5005/events``. +Складається з трьох частин: 
-  * **Task Scheduler** — запускає ``mwj_audit_service.py`` автоматично при старті сервера, незалежно від того, чи залогінений користувач+  * **mwj_audit_service.py** — Python-скрипт, сканує журнал подій AlarmServer і віддає їх у JSON на ``127.0.0.1:5005/events``. Запускається автоматично через Task Scheduler
-  * **mwj_events.aspx** — ASP.NET-сторінка на WebHMI-сервері, яка проксіює запит із браузера клієнта до локального Python-сервісу (обхід CORS/локальних обмежень), віддаючи той самий JSON назовні.+  * **mwj_events.aspx** — сторінка на WebHMI-сервері, яка проксіює цей JSON у браузер клієнта
 +  * **mwj_rawevents_patch.js** — JS-патч, який підтягує ці події в AlarmViewer (Events view) і показує їх поряд зі стандартними подіями.
  
-Разом ці три компоненти дозволяють підтягувати події керування в кастомний JS-віджет AlarmViewer (див. [[mwj_rawevents_patch|патч mwj_rawevents_patch.js]]).+===== 1. Розміщення mwj_audit_service.py =====
  
-===== 1. Встановлення Python-скрипта =====+Скопіювати скрипт у папку проєкту:
  
-  - Скопіювати ``mwj_audit_service.py`` на сервер, наприклад у ``C:\python_portable\mwj_audit_service.py``. +<code> 
-  Переконатися, що на сервері є портативний Python за шляхом ``C:\python_portable\python.exe`` (окрема інсталяція, без залежності від системного PATH — це важливо, бо служба стартує ще до входу користувача). +C:\ProgramData\Schneider Electric\Power Operation\v2024\User\UA_Energy_Demo_23_04\mwj_audit_service.py 
-  - У скрипті перевірити/поправити константи під конкретний проєкт:+</code> 
 + 
 +Перед запуском **замінити** ``JOURNAL_DIR`` під конкретний проєкт а за потреби ``PORT``, якщо 5005 зайнятий):
  
 <code python> <code python>
-JOURNAL_DIR = r"C:\ProgramData\Schneider Electric\Power Operation\v2024\Data\UA_Energy_Demo_23_04\c1.AlarmServer1\Database\Journal\JnI00000"+import re 
 +import os 
 +import struct 
 +import datetime 
 +import json 
 +import threading 
 +import time 
 +from http.server import HTTPServer, BaseHTTPRequestHandler 
 + 
 +JOURNAL_DIR = r"C:\ProgramData\Schneider Electric\Power Operation\v2024\Data\UA_Energy_Demo_23_04\c1.AlarmServer1\Database\Journal\JnI00000" #шлях до журналу подій хард клієнта
 PORT = 5005 PORT = 5005
 DAYS_BACK = 7 DAYS_BACK = 7
 POLL_SEC = 1 POLL_SEC = 1
-</code>+CORS_ORIGIN = "*"
  
-  * ``UA_Energy_Demo_23_04`` — назва проєктузмінюється під кожне нове розгортання+def filetime_to_dt(raw8: bytes): 
-  * ``PORT`` — локальний портна якому скрипт віддає ``/events`` та ``/health``+    ft = struct.unpack('<Q'raw8)[0] 
-  * ``DAYS_BACK`` — глибина історіїяку скрипт підвантажує при старті+    if ft == 0: 
-  * ``POLL_SEC`` — інтервал повторного сканування журналу.+        return None 
 +    microseconds = ft // 10 
 +    return datetime.datetime(1601, 1, 1, tzinfo=datetime.timezone.utc) + \ 
 +           datetime.timedelta(microseconds=microseconds) 
 +def parse_mwj(path: strsince: datetime.datetime) -> list: 
 +    events = [] 
 +    try: 
 +        data = open(path, 'rb').read() 
 +    except Exception: 
 +        return events 
 +    ext_pat = 'ExternalEvent'.encode('utf-16-le'
 +    kernel_pat = 'Kernel,367,S"'.encode('utf-16-le') 
 +    pos = 0 
 +    while True: 
 +        pos = data.find(ext_pat, pos) 
 +        if pos == -1: 
 +            break 
 +        ts_bytes = data[pos - 8 : pos] 
 +        dt = filetime_to_dt(ts_bytes) if len(ts_bytes) == 8 else None
  
-Скрипт піднімає локальний HTTP-сервер (``127.0.0.1``без зовнішнього доступуз двома ендпоінтами:+        if dt and dt < since: 
 +            pos += len(ext_pat) 
 +            continue 
 +        k_pos = data.find(kernel_pat, pos, pos + 512) 
 +        msg  = '' 
 +        user = '' 
 +        if k_pos != -1: 
 +            str_start = k_pos + len(kernel_pat) 
 +            str_end = data.find(b'\x22\x00', str_start) 
 +            if str_end != -1: 
 +                try: 
 +                    msg = data[str_start:str_end].decode('utf-16-le'
 +                except UnicodeDecodeError: 
 +                    pass 
 +                after = str_end + 4 
 +                user_match = re.search(b'(?:[\x21-\x7e]\x00){2,}', data[after : after + 128]) 
 +                if user_match: 
 +                    candidate = user_match.group().decode('utf-16-le'
 +                    if not any(candidate.startswith(x) for x in ('Kernel', 'DBServer', 'System', 'Digital', 'External')): 
 +                        user = candidate 
 +        if msg: 
 +            event_type = classify(msg) 
 +            if event_type: 
 +                events.append({ 
 +                    'id':        f"{path}:{pos}", 
 +                    'timestamp': dt.isoformat() if dt else '', 
 +                    'type':      event_type, 
 +                    'message':   msg, 
 +                    'user':      user, 
 +                })
  
-  ``GET /events`` — поточний кеш подій у JSON. +        pos += len(ext_pat) 
-  * ``GET /health`` — перевірка живості (``OK``).+    return events 
 +def classify(msg: str) -> str | None: 
 +    if 'Tag Write Operation' in msg: 
 +        return 'TagWrite' 
 +    if 'logged in' in msg or 'Logged on' in msg: 
 +        return 'Login' 
 +    if 'logged out' in msg or 'Logged off' in msg: 
 +        return 'Logout' 
 +    return None 
 +_cache_lock  = threading.Lock() 
 +_events_cache: list = [] 
 +def scan_all(): 
 +    since = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=DAYS_BACK) 
 +    result = [] 
 +    if not os.path.isdir(JOURNAL_DIR): 
 +        print(f"[WARN] Journal dir not found: {JOURNAL_DIR}"
 +        return 
 +    for fname in sorted(os.listdir(JOURNAL_DIR)): 
 +        if not fname.upper().endswith('.MWJ'): 
 +            continue 
 +        full = os.path.join(JOURNAL_DIR, fname) 
 +        try: 
 +            mtime = datetime.datetime.fromtimestamp( 
 +                os.path.getmtime(full), tz=datetime.timezone.utc) 
 +        except OSError: 
 +            continue 
 +        if mtime < since: 
 +            continue 
 +        result.extend(parse_mwj(full, since)) 
 +    seen = set() 
 +    unique = [] 
 +    for e in sorted(result, key=lambda x: x['timestamp'], reverse=True): 
 +        if e['id'] not in seen: 
 +            seen.add(e['id']) 
 +            unique.append(e) 
 +    with _cache_lock: 
 +        global _events_cache 
 +        _events_cache = unique 
 +    print(f"[{datetime.datetime.now():%H:%M:%S}] Scanned {JOURNAL_DIR}: {len(unique)} events"
 +def scanner_thread(): 
 +    while True: 
 +        try: 
 +            scan_all() 
 +        except Exception as e: 
 +            print(f"[ERROR] Scanner: {e}"
 +        time.sleep(POLL_SEC) 
 +class Handler(BaseHTTPRequestHandler): 
 +    def log_message(self, fmt, *args): 
 +        pass  # suppress default access log 
 +    def send_cors(self): 
 +        self.send_header('Access-Control-Allow-Origin', CORS_ORIGIN) 
 +        self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS'
 +        self.send_header('Access-Control-Allow-Headers', 'Content-Type'
 +    def do_OPTIONS(self): 
 +        self.send_response(204) 
 +        self.send_cors() 
 +        self.end_headers() 
 +    def do_GET(self): 
 +        path = self.path.split('?')[0] 
 +        if path == '/events': 
 +            with _cache_lock: 
 +                data = json.dumps(_events_cache, ensure_ascii=False).encode('utf-8'
 +            self.send_response(200) 
 +            self.send_header('Content-Type', 'application/json; charset=utf-8'
 +            self.send_header('Content-Length', str(len(data))) 
 +            self.send_cors() 
 +            self.end_headers() 
 +            self.wfile.write(data) 
 +        elif path == '/health': 
 +            body = b'OK' 
 +            self.send_response(200) 
 +            self.send_cors() 
 +            self.send_header('Content-Length', '2'
 +            self.end_headers() 
 +            self.wfile.write(body) 
 +        else: 
 +            self.send_response(404) 
 +            self.end_headers() 
 +if __name__ == '__main__': 
 +    print(f"Starting MWJ Audit Service on port {PORT}"
 +    scan_all() 
 +    t = threading.Thread(target=scanner_thread, daemon=True) 
 +    t.start() 
 +    server = HTTPServer(('127.0.0.1', PORT), Handler) 
 +    print(f"Listening on http://127.0.0.1:{PORT}/events"
 +    try: 
 +        server.serve_forever() 
 +    except KeyboardInterrupt: 
 +        print("Stopped."
 +</code> 
 + 
 +**Що замінити:** тільки рядок ``JOURNAL_DIR = r"...UA_Energy_Demo_23_04..."`` — підставити фактичну назву проєкту у шлях до Journal.
  
 ===== 2. Реєстрація завдання в Task Scheduler ===== ===== 2. Реєстрація завдання в Task Scheduler =====
  
-Створюється завдання з іменем **logging**, яке стартує скрипт при завантаженні сервера.+Створити нове завдання (Create Task, не Basic Task) з іменем **logging**.
  
-==== Вкладка General ====+== General == 
 +  * Security options: **Run whether user is logged on or not** 
 +  * Увімкнути **Run with highest privileges** 
 +  * Configure for: відповідно до ОС сервера
  
-  * **Name:** ``logging`` +== Triggers == 
-  * **Security options:** обрати ``Run whether user is logged on or not`` +  * New → Begin the task: **At startup** 
-  * Обов'язково увімкнути прапорець **Run with highest privileges** +  * StatusEnabled
-  * **Configure for:** відповідно до ОС сервера (у демо — Windows Server 2019)+
  
-{{ :mwj_task_general.png?400 |Вкладка General}}+== Actions == 
 +  * New → Action**Start a program** 
 +  * Program/script: 
 +<code> 
 +C:\python_portable\python.exe 
 +</code> 
 +  * Add arguments: 
 +<code> 
 +mwj_audit_service.py 
 +</code> 
 +  * Start in (обов'язково!): 
 +<code> 
 +C:\ProgramData\Schneider Electric\Power Operation\v2024\User\UA_Energy_Demo_23_04 
 +</code> 
 +(поле "Start in" вказує на папку, куди поклали скрипт у п.1 — без цього скрипт може не знайти сам себе при запуску не з тієї директорії)
  
-==== Вкладка Triggers ====+== Conditions == 
 +  * Power: увімкнути **Start the task only if the computer is on AC power** та **Stop if the computer switches to battery power** 
 +  * Idle: не використовувати 
 +  * Network: не обмежувати
  
-  * Додати тригер **At startup** (запуск при старті системи), статус — Enabled.+Зберегти завдання (OK), при потребі ввести пароль облікового запису Administrator.
  
-{{ :mwj_task_triggers.png?400 |Вкладка Triggers}}+**Перевірка:** запустити завдання вручну (Run), відкрити на сервері ``http://127.0.0.1:5005/health`` — має повернути ``OK``, і ``http://127.0.0.1:5005/events`` — має повернути масив JSON.
  
-==== Вкладка Actions ====+===== 3. Розміщення mwj_events.aspx =====
  
-  * Тип дії: **Start a program** +Файл вставити за шляхом:
-  * Program/script: ``"C:\python_portable\python.exe"`` +
-  * Add arguments: ``mwj_audit_service.py`` +
-  * Start in (обов'язково!): каталог, де лежить сам скрипт, наприклад ``C:\python_portable`` +
- +
-{{ :mwj_task_actions.png?400 |Вкладка Actions}} +
- +
-==== Вкладка Conditions ==== +
- +
-  * **Power:** увімкнути ``Start the task only if the computer is on AC power`` та ``Stop if the computer switches to battery power`` +
-  * **Idle:** не використовувати (сервер не має idle-стану) +
-  * **Network:** не обмежувати за типом з'єднання +
- +
-{{ :mwj_task_conditions.png?400 |Вкладка Conditions}} +
- +
-==== Перевірка завдання ==== +
- +
-  - Запустити завдання вручну (``Run``) або перезавантажити сервер. +
-  - Переконатися, що процес ``python.exe`` з'явився у диспетчері завдань. +
-  - Відкрити ``http://127.0.0.1:5005/health`` на сервері — має повернути ``OK``. +
-  - Відкрити ``http://127.0.0.1:5005/events`` — має повернути масив JSON з подіями за останні ``DAYS_BACK`` днів. +
- +
-===== 3. Розгортання ASPX-моста для WebHMI ===== +
- +
-Файл ``mwj_events.aspx`` розміщується на WebHMI-сервері за шляхом:+
  
 <code> <code>
Рядок 85: Рядок 230:
 </code> </code>
  
-Він проксіює звернення браузера клієнта до локального Python-сервісу (``127.0.0.1:5005/events``) і повертає той самий JSON, додаючи заголовок ``Access-Control-Allow-Origin: *``:+Код без змін (порт 5005 має співпадати з портом у Python-скрипті):
  
 <code aspx> <code aspx>
Рядок 123: Рядок 268:
 </code> </code>
  
-**Важливо:**+**Перевірка:** ``https://<адреса_webhmi>/mwj_events.aspx`` має віддавати той самий JSON, що й ``http://127.0.0.1:5005/events`` на сервері. 
 + 
 +===== 4. Розміщення mwj_rawevents_patch.js ===== 
 + 
 +Скопіювати скрипт у папку: 
 + 
 +<code> 
 +C:\Program Files (x86)\Schneider Electric\Power Operation\v2024\Applications\Web\SystemDataService\Content\Applications\Alarms\Scripts\Scripts\mwj_rawevents_patch.js 
 +</code> 
 + 
 +Далі відкрити файл (без змін розширення/типу, редагувати як текстовий): 
 + 
 +<code> 
 +C:\Program Files (x86)\Schneider Electric\Power Operation\v2024\Applications\Web\SystemDataService\Content\Applications\Alarms\Scripts\Alarms.Script.Shared 
 +</code> 
 + 
 +і додати в кінець списку рядок: 
 + 
 +<code> 
 +Scripts\mwj_rawevents_patch.js 
 +</code> 
 + 
 +Це підключає патч до AlarmViewer, щоб він підвантажив ``mwj_rawevents_patch.js`` разом з іншими скриптами модуля Alarms. 
 + 
 +Повний код патча: 
 + 
 +<code javascript> 
 +(function(){ 
 +    'use strict'; 
 +    var AUDIT_API_URL = '/WebHmi/mwj_events.aspx'; 
 +    var POLLING_INTERVAL_MS = 5000; 
 +    var MWJ_SOURCE_ID = '<System>.Система'; 
 +    var MWJ_PRIORITY = 0; 
 +    var _mwjCache = []; 
 +    var _mwjIds = {}; 
 +    var _lastRawStr = ''; 
 +    var _lastNativeData = []; 
 + 
 +    function fetchMwj(){ 
 +        fetch(AUDIT_API_URL) 
 +            .then(function(r){return r.json();}) 
 +            .then(function(events){ 
 +                if(!Array.isArray(events)) return; 
 +                var str = JSON.stringify(events); 
 +                if(str === _lastRawStr) return; 
 +                _lastRawStr = str; 
 +                var prevIds = _mwjIds; 
 +                _mwjIds = {}; 
 +                _mwjCache = events.map(toRawEvent); 
 +                _mwjCache.forEach(function(e){_mwjIds[e.Id] = true;}); 
 +                var newEvents = _mwjCache.filter(function(e){return !prevIds[e.Id];}); 
 +                if(newEvents.length > 0) pushNewMwjToView(newEvents); 
 +            }) 
 +            .catch(function(e){ console.warn('[MWJ] fetch error:', e); }); 
 +    } 
 +    function extractUser(msg){ 
 +        var m = msg.match(/(?:^User|by User)\s+([^\s\.]+)/i); 
 +        return m ? m[1] : ''; 
 +    } 
 +    function extractTag(msg){ 
 +        var m = msg.match(/wrote to tags:\s*([^@\n]+)/i); 
 +        if(!m) return msg; 
 +        return m[1].replace(/\.\s*$/, '').trim(); 
 +    } 
 +    function toRawEvent(evt){ 
 +        var ts  = evt.timestamp ? new Date(evt.timestamp).getTime() : Date.now(); 
 +        var msg = evt.message || ''; 
 +        var user = extractUser(msg); 
 +        var tagName = ''; 
 +        var effect = ''; 
 +        var cause = ''; 
 + 
 +        if(evt.type === 'Login'){ 
 +            tagName = user; 
 +            effect = 'Вхід у систему'; 
 +            cause = 'Вхід користувача ' + user; 
 +        } 
 +        else if(evt.type === 'Logout'){ 
 +            tagName = user; 
 +            effect = 'Вихід із системи'; 
 +            cause = 'Вихід користувача ' + user; 
 + 
 +        } 
 +        else if(evt.type === 'TagWrite'){ 
 +            var tag = extractTag(msg); 
 +            tagName = 'Користувач ' + user + ' успішно записав тег'; 
 +            effect = 'Запис тегу'; 
 +            cause = tag; 
 + 
 +        } 
 +        else{ 
 +            tagName   = msg; 
 +            effect  = 'Системна подія'; 
 +            cause = msg; 
 +        } 
 +        return{ 
 +            Id: 'MWJ_' + evt.id, 
 +            TimestampUtc: '/Date(' + ts + ')/', 
 +            StartTimeUtc: '/Date(' + ts + ')/', 
 +            Source:{ 
 +                SourceId: MWJ_SOURCE_ID, 
 +                DisplayName: MWJ_SOURCE_ID, 
 +                IanaTimeZoneName: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC', 
 +            }, 
 +            Cause: cause, 
 +            CauseValue: user, 
 +            Effect: effect, 
 +            EffectValue: effect, 
 +            Name: tagName, 
 +            DisplayEffectValue: tagName, 
 +            DisplayType: 'Подія', 
 +            EventType: 4, 
 +            Priority: MWJ_PRIORITY, 
 +            TimeQuality: 0, TimeQualityMicroseconds: 0, 
 +            Custom1:'', Custom3:'', Custom4:'', Custom5:'', Custom6:'', Custom7:'', Custom8:'', 
 +            IsActive: false, IsAcknowledged: true, 
 +            IsDisabled: false, IsShelved: false, IsDeleted: false, 
 +            _isMwj: true 
 +        }; 
 +    } 
 +    function pushNewMwjToView(newEvents){ 
 +        var App = window.AlarmViewer && 
 +                  window.AlarmViewer.Models && 
 +                  window.AlarmViewer.Models.Application; 
 +        if(!App) return; 
 +        var AlarmViews = IonDataApi.AlarmViews; 
 +        if(App.ViewModeCurrent !== AlarmViews.ViewModeEvents) return; 
 +        var view = App.ActiveView; 
 +        if(!view || typeof view.UpdateViewWithChanges !== 'function') return; 
 +        view.UpdateViewWithChanges(newEvents, []); 
 +    } 
 +    function mergeWithMwj(nativeData){ 
 +        var nativeIds = {}; 
 +        nativeData.forEach(function(e){if(e && e.Id) nativeIds[e.Id] = true;}); 
 +        var result = nativeData.slice(); 
 +        _mwjCache.forEach(function(mwj){ 
 +            if(!nativeIds[mwj.Id]) result.push(mwj); 
 +        }); 
 +        return result; 
 +    } 
 +    function waitForApp(cb){ 
 +        var n = 0; 
 +        var t = setInterval(function(){ 
 +            n++; 
 +            var App = window.AlarmViewer && 
 +                      window.AlarmViewer.Models && 
 +                      window.AlarmViewer.Models.Application; 
 +            if(App && 
 +               typeof App.RenderFreshData             === 'function' && 
 +               typeof App.RenderIncrementalUpdateData === 'function'){ 
 +                clearInterval(t); 
 +                cb(App); 
 +            } 
 +            if(n > 300){ clearInterval(t); console.warn('[MWJ] Application не знайдено за 30с');
 +        }, 100); 
 +    } 
 +    function applyPatch(App){ 
 +        var AlarmViews = IonDataApi.AlarmViews; 
 + 
 +        var _origFresh = App.RenderFreshData; 
 +        App.RenderFreshData = function(isValid, viewMode, data){ 
 +            if(viewMode === AlarmViews.ViewModeEvents && Array.isArray(data)){ 
 +                _lastNativeData = data.filter(function(e){return !e._isMwj;}); 
 +                data = mergeWithMwj(_lastNativeData); 
 +            } 
 +            return _origFresh.call(this, isValid, viewMode, data); 
 +        }; 
 +        var _origIncr = App.RenderIncrementalUpdateData; 
 +        App.RenderIncrementalUpdateData = function(viewMode, added, removed){ 
 +            if(viewMode === AlarmViews.ViewModeEvents){ 
 +                if(Array.isArray(removed)){ 
 +                    removed = removed.filter(function(e){return !_mwjIds[e.Id];}); 
 +                } 
 +                if(Array.isArray(added)){ 
 +                    added.forEach(function(item){ 
 +                        if(!item._isMwj){ 
 +                            var found = false; 
 +                            for(var i = 0; i < _lastNativeData.length; i++){ 
 +                                if(_lastNativeData[i].Id === item.Id){ 
 +                                    _lastNativeData[i] = item; found = true; break; 
 +                                } 
 +                            } 
 +                            if(!found) _lastNativeData.push(item); 
 +                        } 
 +                    }); 
 +                } 
 +            } 
 +            return _origIncr.call(this, viewMode, added, removed); 
 +        }; 
 +    } 
 +    fetchMwj(); 
 +    setInterval(fetchMwj, POLLING_INTERVAL_MS); 
 +    if(document.readyState === 'loading'){ 
 +        document.addEventListener('DOMContentLoaded', function(){ waitForApp(applyPatch); }); 
 +    } 
 +    else{ 
 +        waitForApp(applyPatch); 
 +    } 
 +})(); 
 +</code>
  
-  ASPX-файл виконується на самому WebHMI-сервері, тому ``127.0.0.1:5005`` у ньому — це той самий сервер, де стоїть Task Scheduler-завдання з п.2 (тобто Python-сервіс і IIS/WebHMI мають бути на одній машині). +**Перевірка:** відкрити AlarmViewer → вкладку Events, після входу/виходу оператора або запису тегу — подія має з'явитись у списку протягом ~5 секунд (``POLLING_INTERVAL_MS``).
-  * Після копіювання файлу перевірити доступність зовні: ``https://<адреса_webhmi>/mwj_events.aspx`` має віддавати той самий JSON, що й локальний ``/events``. +
-  * Якщо повертається ``{"error": "..."}`` — значить Python-сервіс не піднявся (перевірити крок 2.4або порт 5005 заблокований локальним фаєрволом.+
  
-===== 4. Типові проблеми =====+===== 5. Типові проблеми =====
  
 ^ Симптом ^ Ймовірна причина ^ Рішення ^ ^ Симптом ^ Ймовірна причина ^ Рішення ^
Рядок 135: Рядок 477:
 | ``/events`` повертає порожній масив | Невірний ``JOURNAL_DIR`` для проєкту, або ``DAYS_BACK`` замалий | Перевірити фактичний шлях журналу проєкту, звірити назву проєкту в шляху | | ``/events`` повертає порожній масив | Невірний ``JOURNAL_DIR`` для проєкту, або ``DAYS_BACK`` замалий | Перевірити фактичний шлях журналу проєкту, звірити назву проєкту в шляху |
 | ASPX повертає ``{"error": ...}`` | Python-сервіс не на тому ж сервері, або порт закритий фаєрволом | Перевірити, що IIS/WebHMI і скрипт на одній машині; дозволити 127.0.0.1:5005 у фаєрволі | | ASPX повертає ``{"error": ...}`` | Python-сервіс не на тому ж сервері, або порт закритий фаєрволом | Перевірити, що IIS/WebHMI і скрипт на одній машині; дозволити 127.0.0.1:5005 у фаєрволі |
 +| Події не з'являються в AlarmViewer | ``mwj_rawevents_patch.js`` не підключено в ``Alarms.Script.Shared`` | Перевірити, що рядок ``Scripts\mwj_rawevents_patch.js`` доданий у файл і немає помилок у шляху |
 | Завдання не стартує при перезавантаженні | Не увімкнено ``Run whether user is logged on or not`` або немає прав | Перевірити Security options і ``Run with highest privileges`` | | Завдання не стартує при перезавантаженні | Не увімкнено ``Run whether user is logged on or not`` або немає прав | Перевірити Security options і ``Run with highest privileges`` |
  • scada/events.1787902772.txt.gz
  • Востаннє змінено: 2026/08/28 10:39
  • повз scada