Files
microdao-daarion/site/tasks/TASK_PHASE_NODE2_ROUTER_SWAPPER_FIX/index.html
Apple ef3473db21 snapshot: NODE1 production state 2026-02-09
Complete snapshot of /opt/microdao-daarion/ from NODE1 (144.76.224.179).
This represents the actual running production code that has diverged
significantly from the previous main branch.

Key changes from old main:
- Gateway (http_api.py): expanded from ~40KB to 164KB with full agent support
- Router: new /v1/agents/{id}/infer endpoint with vision + DeepSeek routing
- Behavior Policy: SOWA v2.2 (3-level: FULL/ACK/SILENT)
- Agent Registry: config/agent_registry.yml as single source of truth
- 13 agents configured (was 3)
- Memory service integration
- CrewAI teams and roles

Excluded from snapshot: venv/, .env, data/, backups, .tgz archives

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 08:46:46 -08:00

961 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="canonical" href="https://IvanTytar.github.io/microdao-daarion/tasks/TASK_PHASE_NODE2_ROUTER_SWAPPER_FIX/">
<link rel="icon" href="../../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.5.3, mkdocs-material-9.5.18">
<title>TASK_PHASE_NODE2_ROUTER_SWAPPER_FIX — Router / Swapper / Node Guardian - DAARION Documentation</title>
<link rel="stylesheet" href="../../assets/stylesheets/main.66ac8b77.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<script>__md_scope=new URL("../..",location),__md_hash=e=>[...e].reduce((e,_)=>(e<<5)-e+_.charCodeAt(0),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#task_phase_node2_router_swapper_fix-router-swapper-node-guardian" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../.." title="DAARION Documentation" class="md-header__button md-logo" aria-label="DAARION Documentation" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54Z"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3V6m0 5h18v2H3v-2m0 5h18v2H3v-2Z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
DAARION Documentation
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
TASK_PHASE_NODE2_ROUTER_SWAPPER_FIX — Router / Swapper / Node Guardian
</span>
</div>
</div>
</div>
<script>var media,input,key,value,palette=__md_get("__palette");if(palette&&palette.color){"(prefers-color-scheme)"===palette.color.media&&(media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']"),palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent"));for([key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.516 6.516 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5Z"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.516 6.516 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12Z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"/></svg>
</button>
</nav>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../.." title="DAARION Documentation" class="md-nav__button md-logo" aria-label="DAARION Documentation" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54Z"/></svg>
</a>
DAARION Documentation
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../public/" class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../public/getting-started/" class="md-nav__link">
<span class="md-ellipsis">
Getting Started
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../public/architecture-overview/" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../public/daiS_daos_overview/" class="md-nav__link">
<span class="md-ellipsis">
DAIS & DAOS
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5" >
<label class="md-nav__link" for="__nav_5" id="__nav_5_label" tabindex="">
<span class="md-ellipsis">
Internal
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5">
<span class="md-nav__icon md-icon"></span>
Internal
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5_1" >
<label class="md-nav__link" for="__nav_5_1" id="__nav_5_1_label" tabindex="0">
<span class="md-ellipsis">
Infra
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_5_1_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5_1">
<span class="md-nav__icon md-icon"></span>
Infra
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../internal/infra/INFRA_AUTOMATION_PACK_V1/" class="md-nav__link">
<span class="md-ellipsis">
Infra Automation Pack v1
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../internal/infra/monitoring_overview/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../internal/infra/nodes_registry_v0/" class="md-nav__link">
<span class="md-ellipsis">
Nodes Registry v0
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_5_2" >
<label class="md-nav__link" for="__nav_5_2" id="__nav_5_2_label" tabindex="0">
<span class="md-ellipsis">
Specs
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_5_2_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_5_2">
<span class="md-nav__icon md-icon"></span>
Specs
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../internal/specs/matrix_presence_aggregator/" class="md-nav__link">
<span class="md-ellipsis">
Matrix Presence Aggregator
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../internal/specs/city_map_spec/" class="md-nav__link">
<span class="md-ellipsis">
City Map Spec
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../internal/specs/node_join_protocol_draft/" class="md-nav__link">
<span class="md-ellipsis">
Node Join Protocol (Draft)
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#_1" class="md-nav__link">
<span class="md-ellipsis">
Контекст
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#_2" class="md-nav__link">
<span class="md-ellipsis">
Ціль
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#_3" class="md-nav__link">
<span class="md-ellipsis">
Архітектурні принципи
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#_4" class="md-nav__link">
<span class="md-ellipsis">
Завдання
</span>
</a>
<nav class="md-nav" aria-label="Завдання">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#1-get_node_endpoints-servicescity-servicerepo_citypy" class="md-nav__link">
<span class="md-ellipsis">
1. Нормалізувати get_node_endpoints у services/city-service/repo_city.py
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-get_node_swapper_detail-servicescity-serviceroutes_citypy" class="md-nav__link">
<span class="md-ellipsis">
2. Виправити get_node_swapper_detail у services/city-service/routes_city.py
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#3-node-guardian-node2" class="md-nav__link">
<span class="md-ellipsis">
3. Перевірити та поправити node-guardian (особливо для NODE2)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#4-node_id" class="md-nav__link">
<span class="md-ellipsis">
4. Сумісність з наявними фільтрами по node_id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#5-swapper-models-ui" class="md-nav__link">
<span class="md-ellipsis">
5. Swapper models → UI
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#acceptance-criteria" class="md-nav__link">
<span class="md-ellipsis">
Acceptance Criteria
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#_5" class="md-nav__link">
<span class="md-ellipsis">
Пріоритет
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="task_phase_node2_router_swapper_fix-router-swapper-node-guardian">TASK_PHASE_NODE2_ROUTER_SWAPPER_FIX — Router / Swapper / Node Guardian<a class="headerlink" href="#task_phase_node2_router_swapper_fix-router-swapper-node-guardian" title="Permanent link">&para;</a></h1>
<h2 id="_1">Контекст<a class="headerlink" href="#_1" title="Permanent link">&para;</a></h2>
<ul>
<li>У DAARION.city є дві ноди:</li>
<li>NODE1 — основний сервер (docker-compose, daji-router, swapper-service тощо).</li>
<li>
<p>NODE2 — MacBook Pro M4 Max (<code>node-2-macbook-m4max</code>, IP: <code>192.168.1.33</code>).</p>
</li>
<li>
<p>UI <code>https://daarion.space/nodes/node/...</code> показує:</p>
</li>
<li>Для NODE1:<ul>
<li>Swapper Service: <code>Degraded</code>, моделей 0/0, <code>No models found</code>.</li>
<li>DAGI Router: <code>Up</code>, 9 агентів.</li>
</ul>
</li>
<li>
<p>Для NODE2:</p>
<ul>
<li>Swapper Service: <code>Degraded</code>, моделей 0/0, <code>No models found</code>.</li>
<li>DAGI Router: <code>Down</code>, <code>Router недоступний</code>.</li>
</ul>
</li>
<li>
<p>Cursor раніше змінював <code>get_node_endpoints</code> так, що:</p>
</li>
<li>Для NODE1 використовувалися docker URLs (<code>http://dagi-router:9102</code>, <code>http://swapper-service:8890</code>).</li>
<li>Для NODE2 — <code>http://localhost:9102</code>, <code>http://localhost:8890</code>, з визначенням по <code>node_id</code>.</li>
<li>
<p>Це працює локально на Mac, але в прод-оточенні <code>city-service</code> крутиться в docker на NODE1, і <code>localhost</code> для нього — це контейнер, а не MacBook або DAGI Router.</p>
</li>
<li>
<p>Стани в UI беруться не напряму з Router/Swapper, а з таблиці <code>node_cache</code> (метрики, які пушить <code>node-guardian</code>).</p>
</li>
</ul>
<h2 id="_2">Ціль<a class="headerlink" href="#_2" title="Permanent link">&para;</a></h2>
<ol>
<li>Стандартизувати визначення endpoint'ів для DAGI Router і Swapper так, щоб:</li>
<li>У PROD усе працювало через один базовий URL (docker-hostи <code>dagi-router</code> / <code>swapper-service</code>).</li>
<li>Не було прив'язки до <code>node_id</code> для вибору URL.</li>
<li>
<p>У DEV/локально на Mac можна було використовувати <code>localhost</code> через ENV.</p>
</li>
<li>
<p>Переконатися, що <code>node-guardian</code> для NODE2 коректно оновлює <code>node_cache</code>:</p>
</li>
<li>Є записи з <code>node_id = 'node-2-macbook-m4max'</code> для <code>router_healthy</code> і <code>swapper_state</code>.</li>
<li>
<p>Помилки логуються явно, а не тихо ковтаються.</p>
</li>
<li>
<p>Виправити відображення моделей у Swapper Service:</p>
</li>
<li>Якщо в Swapper реально є моделі, UI має показувати їх (назву, статус, тип, тощо).</li>
<li>
<p>Якщо моделей немає/немає зв'язку — показувати чесний <code>Degraded</code> з fallback, але не плутати це з "0/0 при наявних моделях".</p>
</li>
<li>
<p>Мінімізувати магію та дублювання логіки: конфіг через ENV, один контракт між <code>city-service</code>, <code>node-guardian</code> і <code>DAGI Router / Swapper</code>.</p>
</li>
</ol>
<hr />
<h2 id="_3">Архітектурні принципи<a class="headerlink" href="#_3" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>Один DAGI Router + один Swapper в проді</strong> (на NODE1) обслуговує всі ноди.</li>
<li><code>city-service</code> завжди ходить до Router/Swapper через <strong>базовий URL з ENV</strong>, без умов по <code>node_id</code>.</li>
<li><code>node-guardian</code>:</li>
<li>Викликає Router/Swapper.</li>
<li>Перетворює результати в метрики <code>node_cache</code> (<code>router_healthy</code>, <code>swapper_state</code>, можливо інші).</li>
<li>Маркує ці записи конкретним <code>node_id</code>, що відповідає ноді, де стоїть guardian.</li>
</ul>
<hr />
<h2 id="_4">Завдання<a class="headerlink" href="#_4" title="Permanent link">&para;</a></h2>
<h3 id="1-get_node_endpoints-servicescity-servicerepo_citypy">1. Нормалізувати <code>get_node_endpoints</code> у <code>services/city-service/repo_city.py</code><a class="headerlink" href="#1-get_node_endpoints-servicescity-servicerepo_citypy" title="Permanent link">&para;</a></h3>
<ol>
<li>Знайти реалізацію <code>get_node_endpoints</code>.</li>
<li>Прибрати логіку, яка підміняє URL на <code>localhost</code> на основі <code>node_id</code> (наприклад, <code>if "node-2" in node_id</code> тощо).</li>
<li>Замість цього:</li>
<li>Винести базові URL у ENV, наприклад:<ul>
<li><code>ROUTER_BASE_URL</code> (наприклад, <code>http://dagi-router:9102</code> у проді).</li>
<li><code>SWAPPER_BASE_URL</code> (наприклад, <code>http://swapper-service:8890</code> у проді).</li>
</ul>
</li>
<li>Для DEV (локальний запуск на Mac без Docker) дозволити дефолт:<ul>
<li><code>ROUTER_BASE_URL=http://localhost:9102</code></li>
<li><code>SWAPPER_BASE_URL=http://localhost:8890</code></li>
</ul>
</li>
<li><code>get_node_endpoints(node)</code> має повертати структуру типу:</li>
</ol>
<p><code>python
return NodeEndpoints(
router_base=f"{ROUTER_BASE_URL}",
swapper_base=f"{SWAPPER_BASE_URL}",
# за потреби — окремі health / metrics / models endpoints
)</code></p>
<ol>
<li>Не прив'язувати URL до <code>node_id</code>. Вся різниця між нодами має відображатись у:</li>
<li><code>node_cache</code> (метрики),</li>
<li>БД агентів (який агент до якої ноди прив'язаний).</li>
</ol>
<h3 id="2-get_node_swapper_detail-servicescity-serviceroutes_citypy">2. Виправити <code>get_node_swapper_detail</code> у <code>services/city-service/routes_city.py</code><a class="headerlink" href="#2-get_node_swapper_detail-servicescity-serviceroutes_citypy" title="Permanent link">&para;</a></h3>
<ol>
<li>Переконатися, що endpoint <code>GET /api/v1/nodes/{node_id}/swapper</code>:</li>
<li>Використовує <code>get_node_endpoints</code> для звернення до Swapper.</li>
<li>Коректно обробляє:<ul>
<li>HTTP 200 з валідною відповіддю <code>/api/v1/models</code>.</li>
<li>HTTP 5xx / timeout / connection error.</li>
</ul>
</li>
<li>Логіка:</li>
<li>Якщо відповідь успішна — парсити список моделей:<ul>
<li>Назва моделі.</li>
<li>Статус (loaded/failed/loading).</li>
<li>Кількість інстансів, GPU/CPU тощо (як дозволяє API Swapper).</li>
</ul>
</li>
<li>Оновлювати / читати кеш (<code>node_cache</code>) так, щоб UI міг показувати:<ul>
<li>Загальну кількість моделей.</li>
<li>Loaded / Failed / Pending.</li>
</ul>
</li>
<li>Якщо помилка або моделі не повертаються:<ul>
<li>Повернути <code>status: "degraded"</code> та <code>models: []</code>, а НЕ 404.</li>
</ul>
</li>
<li>Гарантувати, що UI завжди отримує валідний JSON:</li>
<li>навіть якщо Swapper мертвий,</li>
<li>без сирих трас і HTML помилок.</li>
</ol>
<h3 id="3-node-guardian-node2">3. Перевірити та поправити <code>node-guardian</code> (особливо для NODE2)<a class="headerlink" href="#3-node-guardian-node2" title="Permanent link">&para;</a></h3>
<ol>
<li>Знайти код <code>node-guardian</code> (швидше за все окремий сервіс / скрипт).</li>
<li>Переконатися, що він:</li>
<li>Читає ENV:<ul>
<li><code>NODE_ID</code> (для NODE2: <code>node-2-macbook-m4max</code>).</li>
<li><code>CITY_API_URL</code> (HTTPS URL до city-service).</li>
</ul>
</li>
<li>Периодично:<ul>
<li>Викликає Router health endpoint (через <code>ROUTER_BASE_URL</code> або відповідний URL з ENV).</li>
<li>Викликає Swapper <code>/api/v1/models</code> або health endpoint.</li>
<li>Пушить у <code>node_cache</code> записи:</li>
<li><code>router_healthy</code> з payload (<code>{"ok": true/false, "latency_ms": ...}</code>).</li>
<li><code>swapper_state</code> з payload (<code>{"models_total": X, "models_loaded": Y, "models_failed": Z, "raw": ...}</code>).</li>
</ul>
</li>
<li>Додати нормальні лог-меседжі:</li>
<li>На успішне оновлення.</li>
<li>На помилки (HTTP статус, текст помилки).</li>
<li>Перевірити, що в БД (таблиця <code>node_cache</code>):</li>
<li>Після запуску guardian на NODE2 з'являються рядки з <code>node_id = 'node-2-macbook-m4max'</code> для <code>router_healthy</code> і <code>swapper_state</code>.</li>
</ol>
<h3 id="4-node_id">4. Сумісність з наявними фільтрами по <code>node_id</code><a class="headerlink" href="#4-node_id" title="Permanent link">&para;</a></h3>
<ol>
<li>Знайти всі місця, де читається <code>node_cache</code> для вузла:</li>
<li>Наприклад, <code>get_node_status</code>, <code>get_node_swapper_detail</code>, <code>get_node_router_detail</code> тощо.</li>
<li>Переконатися, що фільтрація відбувається по <code>node_id</code> + <code>kind</code>:</li>
<li><code>WHERE node_id = :node_id AND kind = :kind</code></li>
<li>Не використовувати глобальний <code>swapper_state</code> без <code>node_id</code>, якщо вже перейшли на модель "по нодах".</li>
<li>Якщо історично був один глобальний запис без <code>node_id</code>:</li>
<li>Міграція (якщо потрібно) — або прибрати цей запис, або задовольнитися тим, що UI читає тільки записи з конкретним <code>node_id</code>.</li>
</ol>
<h3 id="5-swapper-models-ui">5. Swapper models → UI<a class="headerlink" href="#5-swapper-models-ui" title="Permanent link">&para;</a></h3>
<ol>
<li>Забезпечити, щоб бекенд повертав у UI-модель (DTO) для Swapper дані:</li>
<li><code>status</code> (<code>"ok" | "degraded" | "down"</code>).</li>
<li><code>models_total</code>.</li>
<li><code>models_loaded</code>.</li>
<li><code>models_failed</code>.</li>
<li><code>models</code> (масив з короткою інформацією по кожній моделі).</li>
<li>Перевірити, що фронт (Node detail page) читає ці поля й не падає, якщо масив <code>models</code> порожній.</li>
<li>Якщо потрібен stub/fallback — він має відрізнятись від реального "0 моделей при піднятому Swapper'і".</li>
</ol>
<hr />
<h2 id="acceptance-criteria">Acceptance Criteria<a class="headerlink" href="#acceptance-criteria" title="Permanent link">&para;</a></h2>
<ol>
<li><strong>Endpoint конфіг</strong>:</li>
<li>У <code>.env</code> / docker-compose є:<ul>
<li><code>ROUTER_BASE_URL</code> (у проді → <code>http://dagi-router:9102</code>).</li>
<li><code>SWAPPER_BASE_URL</code> (у проді → <code>http://swapper-service:8890</code>).</li>
</ul>
</li>
<li><code>get_node_endpoints</code> не використовує <code>node_id</code> для визначення URL.</li>
<li>
<p>У DEV-режимі локальний запуск на Mac використовує <code>localhost:9102/8890</code>.</p>
</li>
<li>
<p><strong>NODE2 в UI</strong>:</p>
</li>
<li>
<p>На сторінці НОДА2:</p>
<ul>
<li>DAGI Router:</li>
<li>Показує реальний статус (<code>Up/Down</code>) на основі <code>router_healthy</code> з <code>node_cache</code>.</li>
<li>При живому Router статус <code>Up</code> без "Router недоступний".</li>
<li>Swapper Service:</li>
<li>Показує реальну кількість моделей (якщо вони є у Swapper).</li>
<li>При проблемах — <code>Degraded</code>, але без 404/порожніх екранiв.</li>
</ul>
</li>
<li>
<p><strong>Node Guardian</strong>:</p>
</li>
<li>Guardian на НОДА2 працює, логі показують регулярні оновлення.</li>
<li>
<p>У Postgres (таблиця <code>node_cache</code>) є останні записи:</p>
<ul>
<li><code>node_id = 'node-2-macbook-m4max'</code>, <code>kind = 'router_healthy'</code>.</li>
<li><code>node_id = 'node-2-macbook-m4max'</code>, <code>kind = 'swapper_state'</code>.</li>
</ul>
</li>
<li>
<p><strong>Swapper models</strong>:</p>
</li>
<li><code>curl &lt;SWAPPER_BASE_URL&gt;/api/v1/models</code> повертає список моделей.</li>
<li>UI Swapper Service для NODE1 показує ці моделі в таблиці (а не лише <code>0/0</code>).</li>
<li>
<p>При зупиненому Swapper:</p>
<ul>
<li>UI показує <code>Degraded</code> або <code>Down</code>, але бекенд повертає валідний JSON з fallback.</li>
</ul>
</li>
<li>
<p><strong>Без регресій</strong>:</p>
</li>
<li>NODE1 продовжує показувати 9 агентів у DAGI Router.</li>
<li>Інші частини <code>nodes</code> UI працюють як раніше (агенти, статуси, Node Guardian &amp; Steward секція).</li>
</ol>
<hr />
<h2 id="_5">Пріоритет<a class="headerlink" href="#_5" title="Permanent link">&para;</a></h2>
<ul>
<li>Високий. Це критична частина UX для нод та діагностики стану DAGI в DAARION.city.</li>
</ul>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"base": "../..", "features": ["navigation.sections", "navigation.instant", "content.code.copy"], "search": "../../assets/javascripts/workers/search.b8dbb3d2.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}}</script>
<script src="../../assets/javascripts/bundle.3220b9d7.min.js"></script>
</body>
</html>