@@ -0,0 +1,164 @@
<!doctype html>
< html lang = "ru" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > SHD MCP< / title >
< style >
: root { color-scheme : light dark ; --bg : #f8fafc ; --surface : #e8edf2 ; --hover : #e1e8ef ; --line : rgba ( 31 , 41 , 55 , .16 ) ; --ink : #1d1f25 ; --muted : #616670 ; --accent : #535963 ; --soft : rgba ( 75 , 85 , 99 , .12 ) ; --bad : #b42318 ; --good : #2f855a ; --button : #fff ; --shadow : 0 10 px 26 px rgba ( 31 , 41 , 55 , .1 ) }
* { box-sizing : border-box } body { margin : 0 ; background : var ( - - bg ) ; color : var ( - - ink ) ; font : 14 px / 1.45 system-ui , - apple-system , BlinkMacSystemFont , "Segoe UI" , sans-serif } . shell { max-width : 980 px ; margin : auto ; padding : 22 px } . card { padding : 20 px ; border : 1 px solid var ( - - line ) ; border-radius : 18 px ; background : var ( - - surface ) ; box-shadow : var ( - - shadow ) }
. eyebrow { color : var ( - - muted ) ; font-size : 11 px ; font-weight : 700 ; letter-spacing : .12 em ; text-transform : uppercase } h1 { margin : 4 px 0 5 px ; font-size : 25 px ; line-height : 1.15 } . lede { margin : 0 ; color : var ( - - muted ) } . summary { display : flex ; gap : 8 px ; flex-wrap : wrap ; margin : 18 px 0 14 px } . summary span { padding : 5 px 9 px ; border : 1 px solid var ( - - line ) ; border-radius : 999 px ; background : var ( - - hover ) ; color : var ( - - muted ) } . summary strong { color : var ( - - ink ) }
. toolbar { display : flex ; gap : 8 px ; margin-bottom : 10 px } . search { min-width : 0 ; flex : 1 ; padding : 10 px 12 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - bg ) ; color : inherit ; outline : 0 } . search : focus { border-color : var ( - - accent ) ; box-shadow : 0 0 0 3 px var ( - - soft ) } button { cursor : pointer } . refresh { padding : 10 px 14 px ; border : 1 px solid var ( - - accent ) ; border-radius : 10 px ; background : var ( - - accent ) ; color : var ( - - button ) ; font-weight : 650 } . refresh : disabled { opacity : .6 ; cursor : wait } . status { min-height : 22 px ; color : var ( - - muted ) } . error { color : var ( - - bad ) }
. table-wrap { max-height : 460 px ; overflow : auto ; border : 1 px solid var ( - - line ) ; border-radius : 12 px ; background : var ( - - bg ) } table { width : 100 % ; border-collapse : collapse ; min-width : 560 px } th , td { padding : 11 px 13 px ; border-bottom : 1 px solid var ( - - line ) ; text-align : left ; vertical-align : top } th { position : sticky ; top : 0 ; background : var ( - - hover ) ; color : var ( - - muted ) ; font-size : 12 px ; font-weight : 650 ; z-index : 1 } tbody tr : last-child td { border-bottom : 0 } . record { cursor : pointer ; outline : 0 } . record : hover , . record : focus { background : var ( - - soft ) } . title { font-weight : 650 } . sub { display : block ; color : var ( - - muted ) ; font-size : 12 px ; margin-top : 2 px } . pill { display : inline-block ; padding : 3 px 8 px ; border-radius : 999 px ; background : var ( - - soft ) ; color : var ( - - accent ) ; font-size : 12 px ; white-space : nowrap } . pill . up , . pill . ok , . pill . active { color : var ( - - good ) ; background : rgba ( 47 , 133 , 90 , .14 ) } . pill . down , . pill . error , . pill . overdue { color : var ( - - bad ) ; background : rgba ( 180 , 35 , 24 , .12 ) } . empty { padding : 24 px ; text-align : center ; color : var ( - - muted ) }
. visual [ hidden ] { display : none } . visual { max-height : 460 px ; overflow : auto ; border : 1 px solid var ( - - line ) ; border-radius : 12 px ; background : var ( - - bg ) ; padding : 12 px } . visual-card { display : block ; width : 100 % ; padding : 11 px 12 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - surface ) ; color : inherit ; text-align : left ; cursor : pointer } . visual-card : hover , . visual-card : focus { background : var ( - - hover ) ; border-color : var ( - - accent ) ; outline : 0 } . visual-title { display : block ; font-weight : 650 } . visual-date , . visual-value { display : block ; color : var ( - - muted ) ; font-size : 12 px ; margin-top : 4 px } . visual-line { display : flex ; gap : 8 px ; align-items : center ; flex-wrap : wrap ; margin-top : 7 px } . kanban { display : grid ; grid-auto-flow : column ; grid-auto-columns : minmax ( 190 px , 1 fr ) ; gap : 10 px ; overflow-x : auto ; align-items : start } . kanban-col { min-width : 190 px ; padding : 10 px ; border : 1 px solid var ( - - line ) ; border-radius : 12 px ; background : var ( - - surface ) } . kanban-head { display : flex ; justify-content : space-between ; gap : 8 px ; margin-bottom : 8 px ; font-weight : 650 } . kanban-list { display : grid ; gap : 8 px } . metric-grid { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 145 px , 1 fr ) ) ; gap : 10 px ; margin-bottom : 12 px } . metric { padding : 12 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - surface ) } . metric-label { display : block ; color : var ( - - muted ) ; font-size : 12 px } . metric-value { display : block ; font-size : 22 px ; font-weight : 700 ; margin-top : 4 px } . timeline { display : grid ; gap : 0 } . timeline-item { display : grid ; grid-template-columns : 18 px minmax ( 0 , 1 fr ) ; gap : 10 px ; position : relative ; padding-bottom : 10 px } . timeline-rail { position : absolute ; left : 8 px ; top : 16 px ; bottom : 0 ; width : 2 px ; background : var ( - - line ) } . timeline-item : last-child . timeline-rail { display : none } . timeline-dot { width : 16 px ; height : 16 px ; border-radius : 50 % ; background : var ( - - accent ) ; border : 3 px solid var ( - - soft ) ; z-index : 1 } . timeline-content { min-width : 0 } . timeline-meta { color : var ( - - muted ) ; font-size : 12 px ; margin-bottom : 5 px } . funnel { display : grid ; gap : 9 px } . funnel-row { display : grid ; grid-template-columns : minmax ( 100 px , 1 fr ) minmax ( 120 px , 2 fr ) auto ; gap : 10 px ; align-items : center } . funnel-fill { width : 100 % ; height : 10 px ; accent-color : var ( - - accent ) } . progress-card { display : grid ; gap : 7 px ; padding : 11 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - surface ) } . progress { width : 100 % ; height : 10 px ; accent-color : var ( - - accent ) } . calendar { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 180 px , 1 fr ) ) ; gap : 10 px } . calendar-day { padding : 10 px ; border : 1 px solid var ( - - line ) ; border-radius : 12 px ; background : var ( - - surface ) ; display : grid ; gap : 8 px ; align-content : start } . calendar-head { font-weight : 650 } . matrix-wrap { overflow : auto } . matrix-table { width : 100 % ; border-collapse : collapse ; min-width : 560 px } . matrix-table th , . matrix-table td { padding : 9 px ; border-bottom : 1 px solid var ( - - line ) ; text-align : left ; vertical-align : top } . matrix-table th { color : var ( - - muted ) ; font-size : 12 px ; background : var ( - - hover ) } . tree { display : grid ; gap : 7 px } . tree-children { display : grid ; gap : 7 px ; padding-left : 18 px ; border-left : 1 px solid var ( - - line ) } . preview-grid { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 220 px , 1 fr ) ) ; gap : 10 px } . preview-card { padding : 12 px ; border : 1 px solid var ( - - line ) ; border-radius : 10 px ; background : var ( - - surface ) } . preview-fields { display : grid ; gap : 5 px ; margin-top : 8 px } . preview-field { font-size : 12 px ; overflow-wrap : anywhere } . preview-field strong { color : var ( - - muted ) ; font-weight : 500 } @ media ( max-width : 640px ) { . funnel-row { grid-template-columns : 1 fr auto } . funnel-fill { grid-column : 1 /- 1 } }
. details { margin-top : 14 px ; padding : 16 px ; border : 1 px solid var ( - - line ) ; border-radius : 12 px ; background : var ( - - bg ) } . details [ hidden ] { display : none } . detail-head { display : flex ; justify-content : space-between ; gap : 12 px ; align-items : start } . details h2 { margin : 0 ; font-size : 18 px } . close { border : 0 ; background : transparent ; color : var ( - - muted ) ; font-size : 20 px } . grid { display : grid ; grid-template-columns : repeat ( auto - fit , minmax ( 150 px , 1 fr ) ) ; gap : 10 px ; margin-top : 14 px } . detail { padding : 9 px ; border-radius : 8 px ; background : var ( - - surface ) } . label { display : block ; color : var ( - - muted ) ; font-size : 11 px ; margin-bottom : 3 px } . value { overflow-wrap : anywhere ; white-space : pre-wrap } @ media ( max-width : 640px ) { . shell { padding : 12 px } . card { padding : 15 px } . toolbar { display : grid ; grid-template-columns : minmax ( 0 , 1 fr ) auto } }
@ media ( prefers-color-scheme : dark ) { : root { --bg : #202020 ; --surface : #292929 ; --hover : #303336 ; --line : #43474c ; --ink : #eaecf0 ; --muted : #96999d ; --accent : #c7b07a ; --soft : rgba ( 199 , 176 , 122 , .18 ) ; --bad : #ff9a93 ; --good : #83c995 ; --button : #241f14 ; --shadow : 0 10 px 26 px rgba ( 0 , 0 , 0 , .34 ) } }
< / style >
< / head >
< body >
< main class = "shell" data-widget-module = "inventory-warnings" > < section class = "card" >
< div class = "eyebrow" > SHD MCP< / div > < h1 id = "title" > SHD< / h1 > < p class = "lede" id = "desc" > < / p > < div class = "summary" id = "summary" > < / div >
< div class = "toolbar" > < input class = "search" id = "search" type = "search" autocomplete = "off" > < button class = "refresh" id = "refresh" type = "button" > < / button > < / div >
< div class = "status" id = "status" role = "status" aria-live = "polite" > < / div >
< div class = "table-wrap" > < table > < thead > < tr > < th id = "head-title" > < / th > < th id = "head-status" > < / th > < th id = "head-date" > < / th > < th id = "head-value" > < / th > < / tr > < / thead > < tbody id = "rows" > < / tbody > < / table > < div class = "empty" id = "empty" hidden > < / div > < / div > < div class = "visual" id = "visual" hidden > < / div >
< section class = "details" id = "details" hidden aria-live = "polite" > < div class = "detail-head" > < div > < h2 id = "detail-title" > < / h2 > < div class = "sub" id = "detail-sub" > < / div > < / div > < button class = "close" id = "close" type = "button" > × < / button > < / div > < div class = "grid" id = "grid" > < / div > < / section >
< / section > < / main >
< script >
( function ( ) {
'use strict' ;
var DEFAULT _MODULE = 'inventory-warnings' ;
var C = { ru : { refresh : 'Обновить' , search : 'Поиск по названию, статусу или ID' , loading : 'Загрузка…' , empty : 'Нет данных для отображения.' , records : 'записей' , status : 'Статус' , date : 'Дата' , value : 'Детали' , id : 'ID' , details : 'Детали' , close : 'Закрыть' , load : 'Не удалось загрузить данные.' , detail : 'Не удалось загрузить детали.' , context : 'Для обновления нужен контекст источника' , total : 'Всего' , amount : 'Сумма' , planned : 'План' , capacity : 'Ёмкость' , utilization : 'Загрузка' , overload : 'Перегрузка' , warnings : 'Предупреждения' , complete : 'Готово' , pending : 'Ожидают' , approved : 'Согласовано' , rejected : 'Отклонено' , progress : 'Прогресс' , score : 'Оценка' , member : 'Участник' , role : 'Роль' , permissions : 'Разрешения' , preview : 'Предпросмотр' , noWarnings : 'Предупреждений нет.' , health : 'Состояние' } , en : { refresh : 'Refresh' , search : 'Search by name, status or ID' , loading : 'Loading…' , empty : 'No data to display.' , records : 'records' , status : 'Status' , date : 'Date' , value : 'Details' , id : 'ID' , details : 'Details' , close : 'Close' , load : 'Could not load data.' , detail : 'Could not load details.' , context : 'A source context is required to refresh.' , total : 'Total' , amount : 'Amount' , planned : 'Planned' , capacity : 'Capacity' , utilization : 'Utilization' , overload : 'Overload' , warnings : 'Warnings' , complete : 'Complete' , pending : 'Pending' , approved : 'Approved' , rejected : 'Rejected' , progress : 'Progress' , score : 'Score' , member : 'Member' , role : 'Role' , permissions : 'Permissions' , preview : 'Preview' , noWarnings : 'No warnings.' , health : 'Health' } } ;
var M = {
discussions : { ru : [ 'Обсуждения' , 'Темы обсуждений с поиском и просмотром сообщений.' , 'Тема' , 'Обновлено' , 'Сообщение' ] , en : [ 'Discussions' , 'Discussion topics with search and message details.' , 'Topic' , 'Updated' , 'Message' ] } ,
notifications : { ru : [ 'Уведомления' , 'Последние уведомления и события модулей.' , 'Уведомление' , 'Время' , 'Источник' ] , en : [ 'Notifications' , 'Recent notifications and module events.' , 'Notification' , 'Time' , 'Source' ] } ,
scheduling : { ru : [ 'Расписание' , 'Ближайшие события и записи из расписания.' , 'Событие' , 'Начало' , 'Участник' ] , en : [ 'Scheduling' , 'Upcoming events and bookings from scheduling.' , 'Event' , 'Starts' , 'Guest' ] } ,
inventory : { ru : [ 'Склад' , 'Складские позиции, остатки и статусы.' , 'Позиция' , 'Обновлено' , 'Количество' ] , en : [ 'Inventory' , 'Stock items, quantities and statuses.' , 'Item' , 'Updated' , 'Quantity' ] } ,
agents : { ru : [ 'Агенты и контроллеры' , 'Доступные агенты, контроллеры и их текущее состояние.' , 'Агент или контроллер' , 'Проверено' , 'Тип' ] , en : [ 'Agents & Controllers' , 'Available agents, controllers and their current state.' , 'Agent or controller' , 'Checked' , 'Type' ] } ,
'status-page' : { ru : [ 'Status Page' , 'Состояние доступности сервисов и мониторов.' , 'Монитор' , 'Проверено' , 'Доступность' ] , en : [ 'Status Page' , 'Service availability and monitor state.' , 'Monitor' , 'Checked' , 'Availability' ] } ,
'project-db' : { ru : [ 'Project DB' , 'Каталог таблиц Project DB с просмотром схемы.' , 'Таблица' , 'Обновлено' , 'Записей' ] , en : [ 'Project DB' , 'Project DB table catalog with schema details.' , 'Table' , 'Updated' , 'Records' ] } ,
notes : { ru : [ 'Заметки' , 'Страницы Notes с поиском и просмотром содержимого.' , 'Страница' , 'Обновлено' , 'Пространство' ] , en : [ 'Notes' , 'Notes pages with search and content details.' , 'Page' , 'Updated' , 'Space' ] } ,
'project-overview' : { ru : [ 'Обзор проекта' , 'Сводные данные по разделам выбранного проекта.' , 'Раздел' , 'Обновлено' , 'Элементы' ] , en : [ 'Project overview' , 'Cross-module records for the selected project.' , 'Section' , 'Updated' , 'Items' ] } ,
files : { ru : [ 'Файлы' , 'Файлы выбранного проекта с поиском и безопасным просмотром ссылок.' , 'Файл' , 'Обновлено' , 'Размер или тип' ] , en : [ 'Files' , 'Files from the selected project with search and safe links.' , 'File' , 'Updated' , 'Size or type' ] } ,
proposals : { ru : [ 'Предложения' , 'Предложения и сметы с поиском, статусами и деталями.' , 'Предложение' , 'Обновлено' , 'Сумма или номер' ] , en : [ 'Proposals' , 'Proposals with search, statuses and details.' , 'Proposal' , 'Updated' , 'Amount or number' ] } ,
terms : { ru : [ 'Terms / ТЗ' , 'Документы технического задания и их ревизии.' , 'Документ' , 'Обновлено' , 'Страницы или ревизия' ] , en : [ 'Terms documents' , 'Terms of Reference documents and revisions.' , 'Document' , 'Updated' , 'Pages or revision' ] } ,
activity : { ru : [ 'Активность' , 'Авторизованные realtime-события SHD по выбранному scope.' , 'Событие' , 'Время' , 'Модуль или тип' ] , en : [ 'Activity' , 'Authenticated SHD realtime events for the selected scope.' , 'Event' , 'Time' , 'Module or type' ] } ,
organizations : { ru : [ 'Организации и доступ' , 'Организации, роли и безопасный просмотр участников.' , 'Организация' , 'Обновлено' , 'Роль или тип' ] , en : [ 'Organizations & access' , 'Organizations, roles and safe membership details.' , 'Organization' , 'Updated' , 'Role or type' ] } ,
gitea : { ru : [ 'Gitea Issues' , 'Открытые задачи разрешённого site или project репозитория.' , 'Issue' , 'Обновлено' , 'Состояние' ] , en : [ 'Gitea Issues' , 'Open issues for an allowed site or project repository.' , 'Issue' , 'Updated' , 'State' ] } ,
schemes : { ru : [ 'Схемы' , 'Документы PDF Scheme Editor и их текущее состояние.' , 'Схема' , 'Обновлено' , 'Ревизия или страницы' ] , en : [ 'Schemes' , 'PDF Scheme Editor documents and their current state.' , 'Scheme' , 'Updated' , 'Revision or pages' ] } ,
'task-kanban' : { ru : [ 'Задачи Kanban' , 'Задачи по колонкам статусов с поиском и деталями.' , 'Задача' , 'Срок' , 'Исполнитель' ] , en : [ 'Task Kanban' , 'Tasks grouped by workflow status with search and details.' , 'Task' , 'Due' , 'Assignee' ] } ,
'team-workload' : { ru : [ 'Загрузка команды' , 'Плановая нагрузка, ёмкость и утилизация команды.' , 'Участник' , 'Период' , 'Часы или процент' ] , en : [ 'Team workload' , 'Planned workload, capacity and team utilization.' , 'Member' , 'Period' , 'Hours or percent' ] } ,
'project-timeline' : { ru : [ 'Timeline проекта' , 'Задачи проекта на временной ленте.' , 'Задача' , 'Срок' , 'Статус' ] , en : [ 'Project timeline' , 'Project tasks on a chronological timeline.' , 'Task' , 'Due' , 'Status' ] } ,
'crm-funnel' : { ru : [ 'CRM-воронка' , 'Сделки CRM, сгруппированные по стадиям.' , 'Стадия' , 'Сделки' , 'Сумма' ] , en : [ 'CRM funnel' , 'CRM deals grouped by pipeline stage.' , 'Stage' , 'Deals' , 'Amount' ] } ,
'finance-dashboard' : { ru : [ 'Финансовый дашборд' , 'Суммы, статусы и документы финансового модуля.' , 'Документ' , 'Обновлено' , 'Сумма' ] , en : [ 'Finance dashboard' , 'Finance amounts, statuses and documents.' , 'Document' , 'Updated' , 'Amount' ] } ,
'proposal-approvals' : { ru : [ 'Согласования предложений' , 'Очередь предложений и текущие approval-статусы.' , 'Предложение' , 'Обновлено' , 'Approval' ] , en : [ 'Proposal approvals' , 'Proposal queue and current approval states.' , 'Proposal' , 'Updated' , 'Approval' ] } ,
'documents-completeness' : { ru : [ 'Полнота документов' , 'Score, ревизии и состояние Terms-документов.' , 'Документ' , 'Обновлено' , 'Полнота' ] , en : [ 'Documents completeness' , 'Scores, revisions and Terms document state.' , 'Document' , 'Updated' , 'Completeness' ] } ,
'agents-health' : { ru : [ 'Здоровье агентов' , 'Состояние агентов и контроллеров.' , 'Агент или контроллер' , 'Проверено' , 'Health' ] , en : [ 'Agents health' , 'Agent and controller health state.' , 'Agent or controller' , 'Checked' , 'Health' ] } ,
'schedule-calendar' : { ru : [ 'Календарь расписания' , 'Ближайшие записи, сгруппированные по дням.' , 'Событие' , 'День' , 'Участник' ] , en : [ 'Schedule calendar' , 'Upcoming bookings grouped by day.' , 'Event' , 'Day' , 'Guest' ] } ,
'acl-matrix' : { ru : [ 'ACL-матрица' , 'Участники организации, роли и permissions.' , 'Участник' , 'Роль' , 'Права' ] , en : [ 'ACL matrix' , 'Organization members, roles and permissions.' , 'Member' , 'Role' , 'Permissions' ] } ,
'inventory-warnings' : { ru : [ 'Предупреждения склада' , 'Низкие остатки и критические складские позиции.' , 'Позиция' , 'Обновлено' , 'Остаток' ] , en : [ 'Inventory warnings' , 'Low-stock and critical inventory items.' , 'Item' , 'Updated' , 'Stock' ] } ,
'gitea-board' : { ru : [ 'Gitea-доска' , 'Issues по состояниям разрешённого репозитория.' , 'Issue' , 'Состояние' , 'Ссылка' ] , en : [ 'Gitea board' , 'Issues grouped by state for an allowed repository.' , 'Issue' , 'State' , 'Link' ] } ,
'schemes-progress' : { ru : [ 'Прогресс схем' , 'Документы схем, ревизии, страницы и completeness.' , 'Схема' , 'Обновлено' , 'Прогресс' ] , en : [ 'Schemes progress' , 'Scheme documents, revisions, pages and completeness.' , 'Scheme' , 'Updated' , 'Progress' ] } ,
'notes-tree' : { ru : [ 'Дерево Notes' , 'Пространства Notes и их иерархия.' , 'Узел' , 'Обновлено' , 'Тип' ] , en : [ 'Notes tree' , 'Notes spaces and their hierarchy.' , 'Node' , 'Updated' , 'Type' ] } ,
'project-db-preview' : { ru : [ 'Project DB preview' , 'Предпросмотр записей выбранной таблицы Project DB.' , 'Запись' , 'Обновлено' , 'Поля' ] , en : [ 'Project DB preview' , 'Preview of records from the selected Project DB table.' , 'Record' , 'Updated' , 'Fields' ] }
} ;
var S = {
discussions : { listTool : 'shd_list_discussion_topics' , listArgs : { limit : 100 } , readTool : 'shd_list_discussion_messages' , detailKey : 'topic_id' , detailArgs : { limit : 50 } , paths : [ 'data' ] , sort : 'desc' } ,
notifications : { listTool : 'shd_list_notifications' , listArgs : { limit : 50 , offset : 0 } , paths : [ 'data' ] , sort : 'desc' } ,
scheduling : { listTool : 'shd_get_scheduling_overview' , listArgs : { limit : 100 } , readTool : 'shd_list_scheduling_bookings' , detailKey : 'booking_id' , detailArgs : { limit : 1 } , paths : [ 'data.bookings' , 'data.upcoming_bookings' , 'data' ] , sort : 'asc' } ,
inventory : { listTool : 'shd_list_inventory_items' , listArgs : { limit : 100 } , readTool : 'shd_get_inventory_item' , detailKey : 'item_id' , paths : [ 'data' ] , sort : 'desc' } ,
agents : { listTool : 'shd_agents_overview' , listArgs : { } , paths : [ 'data.controllers' , 'data.agents' , 'data.user_agents' , 'data' ] , sort : 'asc' } ,
'status-page' : { listTool : 'shd_get_status_page_monitors' , listArgs : { environment : 'dev' } , readTool : 'shd_get_status_page_monitor_history' , detailKey : 'monitor_id' , detailArgs : { limit : 20 } , paths : [ 'data' ] , nested : 'monitors' , sort : 'asc' } ,
'project-db' : { listTool : 'shd_project_db_tables' , listArgs : { } , readTool : 'shd_project_db_schema' , detailKey : 'table' , paths : [ 'data.tables' , 'data' ] , contextRequired : true , sort : 'asc' } ,
notes : { listTool : 'shd_list_note_pages' , listArgs : { limit : 100 } , readTool : 'shd_get_note_page' , detailKey : 'page_id' , paths : [ 'data' ] , sort : 'desc' } ,
'project-overview' : { listTool : 'shd_get_project_context' , listArgs : { include : [ 'summary' , 'tasks' , 'discussions' , 'documents' , 'finance' , 'terms' , 'agents' ] , limit _per _section : 100 } , paths : [ 'data.sections.tasks' , 'data.sections.discussions' , 'data.sections.documents' , 'data.sections.finance' , 'data.sections.terms' ] , combinePaths : true , contextRequired : true , sort : 'desc' } ,
files : { listTool : 'shd_list_files' , readTool : 'shd_get_file_link' , detailKey : 'path' , detailArgs : { download : false } , detailSourceArgs : [ 'project_code' ] , paths : [ 'data' ] , contextRequired : true , sort : 'desc' } ,
proposals : { listTool : 'shd_list_proposals' , listArgs : { limit : 100 } , readTool : 'shd_get_proposal' , detailKey : 'proposal_id' , detailSourceArgs : [ ] , paths : [ 'data' ] , sort : 'desc' } ,
terms : { listTool : 'shd_list_terms_documents' , readTool : 'shd_get_terms_document' , detailKey : 'document_id' , detailSourceArgs : [ 'project_code' ] , paths : [ 'data' ] , contextRequired : true , sort : 'desc' } ,
activity : { listTool : 'shd_get_realtime_events' , listArgs : { module : 'notifications' , limit : 100 } , paths : [ 'data.events' , 'events' , 'data' ] , sort : 'desc' } ,
organizations : { listTool : 'shd_admin_list_organizations' , listArgs : { limit : 100 } , readTool : 'shd_admin_get_organization_members' , detailKey : 'organization_id' , detailSourceArgs : [ ] , paths : [ 'data' ] , sort : 'asc' } ,
gitea : { listTool : 'shd_list_gitea_issues' , listArgs : { target : 'site' , limit : 100 } , paths : [ 'data' ] , sort : 'desc' } ,
schemes : { listTool : 'shd_list_scheme_documents' , readTool : 'shd_get_scheme_document' , detailKey : 'document' , detailSourceArgs : [ 'project' ] , paths : [ 'data' ] , contextRequired : true , sort : 'desc' } ,
'task-kanban' : { listTool : 'shd_list_tasks' , listArgs : { limit : 100 } , readTool : 'shd_get_task' , detailKey : 'task_id' , detailSourceArgs : [ ] , paths : [ 'data' ] , view : 'kanban' , groupBy : 'status' , sort : 'asc' } ,
'team-workload' : { listTool : 'shd_get_task_workload' , listArgs : { period : 'month' } , paths : [ 'data.rows' , 'data.workload' , 'data.assignees' , 'data' ] , view : 'workload' , sort : 'desc' } ,
'project-timeline' : { listTool : 'shd_list_tasks' , listArgs : { limit : 100 } , paths : [ 'data' ] , view : 'timeline' , sort : 'asc' } ,
'crm-funnel' : { listTool : 'shd_list_crm' , listArgs : { section : 'deals' , limit : 100 } , readTool : 'shd_get_crm_entity' , detailKey : 'entity_id' , detailSourceArgs : [ 'section' ] , paths : [ 'data' ] , view : 'funnel' , groupBy : 'stage' , sort : 'desc' } ,
'finance-dashboard' : { listTool : 'shd_list_finance_documents' , listArgs : { limit : 100 } , paths : [ 'data' ] , view : 'dashboard' , sort : 'desc' } ,
'proposal-approvals' : { listTool : 'shd_list_proposals' , listArgs : { limit : 100 } , readTool : 'shd_get_proposal_approval' , detailKey : 'proposal_id' , detailSourceArgs : [ ] , paths : [ 'data' ] , view : 'approval' , sort : 'desc' } ,
'documents-completeness' : { listTool : 'shd_list_terms_documents' , readTool : 'shd_get_terms_document_completeness' , detailKey : 'document_id' , detailSourceArgs : [ 'project_code' ] , paths : [ 'data' ] , view : 'completeness' , contextRequired : true , contextKeys : [ 'project_code' ] , sort : 'desc' } ,
'agents-health' : { listTool : 'shd_agents_overview' , paths : [ 'data.controllers' , 'data.agents' , 'data.user_agents' , 'data' ] , view : 'health' , sort : 'asc' } ,
'schedule-calendar' : { listTool : 'shd_get_scheduling_overview' , listArgs : { limit : 100 } , readTool : 'shd_list_scheduling_bookings' , detailKey : 'booking_id' , detailArgs : { limit : 1 } , paths : [ 'data.bookings' , 'data.upcoming_bookings' , 'data' ] , view : 'calendar' , sort : 'asc' } ,
'acl-matrix' : { listTool : 'shd_list_organization_members' , listArgs : { limit : 100 } , paths : [ 'data' ] , view : 'matrix' , contextRequired : true , contextKeys : [ 'organization_id' ] , contextAll : true , sort : 'asc' } ,
'inventory-warnings' : { listTool : 'shd_list_inventory_items' , listArgs : { limit : 100 } , readTool : 'shd_get_inventory_item' , detailKey : 'item_id' , detailSourceArgs : [ ] , paths : [ 'data' ] , view : 'warnings' , sort : 'asc' } ,
'gitea-board' : { listTool : 'shd_list_gitea_issues' , listArgs : { target : 'site' , limit : 100 } , paths : [ 'data' ] , view : 'kanban' , groupBy : 'state' , sort : 'asc' } ,
'schemes-progress' : { listTool : 'shd_list_scheme_documents' , readTool : 'shd_get_scheme_document' , detailKey : 'document' , detailSourceArgs : [ 'project' ] , paths : [ 'data' ] , view : 'progress' , contextRequired : true , contextKeys : [ 'project' ] , sort : 'desc' } ,
'notes-tree' : { listTool : 'shd_list_note_spaces' , listArgs : { limit : 100 } , readTool : 'shd_get_note_space_tree' , detailKey : 'space_id' , detailSourceArgs : [ ] , paths : [ 'data' ] , view : 'tree' , sort : 'asc' } ,
'project-db-preview' : { listTool : 'shd_project_db_records' , listArgs : { limit : 100 , include _fields : true , include _view _meta : true } , paths : [ 'data.records' , 'data' ] , view : 'preview' , contextRequired : true , contextKeys : [ 'base' , 'project_code' , 'table' ] , contextAll : true , sort : 'asc' }
} ;
var pending = new Map ( ) , nextId = 1 , rows = [ ] , state = { module : DEFAULT _MODULE , config : Object . assign ( { } , S [ DEFAULT _MODULE ] ) , lang : 'ru' , busy : false } ;
var e = { title : document . getElementById ( 'title' ) , desc : document . getElementById ( 'desc' ) , search : document . getElementById ( 'search' ) , refresh : document . getElementById ( 'refresh' ) , status : document . getElementById ( 'status' ) , summary : document . getElementById ( 'summary' ) , tableWrap : document . querySelector ( '.table-wrap' ) , visual : document . getElementById ( 'visual' ) , rows : document . getElementById ( 'rows' ) , empty : document . getElementById ( 'empty' ) , details : document . getElementById ( 'details' ) , detailTitle : document . getElementById ( 'detail-title' ) , detailSub : document . getElementById ( 'detail-sub' ) , grid : document . getElementById ( 'grid' ) , close : document . getElementById ( 'close' ) , ht : document . getElementById ( 'head-title' ) , hs : document . getElementById ( 'head-status' ) , hd : document . getElementById ( 'head-date' ) , hv : document . getElementById ( 'head-value' ) } ;
function lang ( ) { var x = typeof window !== 'undefined' && window . openai && window . openai . locale ; x = String ( x || document . documentElement . lang || 'ru' ) . toLowerCase ( ) ; return x . indexOf ( 'en' ) === 0 ? 'en' : 'ru' }
function t ( k ) { return ( C [ state . lang ] || C . ru ) [ k ] || k }
function obj ( x ) { return x && typeof x === 'object' && ! Array . isArray ( x ) }
function merge ( a , b ) { return Object . assign ( { } , a || { } , b || { } ) }
function payload ( x ) { if ( ! x ) return null ; if ( Object . prototype . hasOwnProperty . call ( x , 'structuredContent' ) ) return x . structuredContent ; if ( Object . prototype . hasOwnProperty . call ( x , 'toolOutput' ) ) return x . toolOutput ; return x }
function unwrap ( x ) { if ( x && x . isError ) throw Error ( t ( 'load' ) ) ; var p = payload ( x ) || { } ; if ( p . error ) throw Error ( String ( p . error . message || p . error . detail || p . error ) ) ; if ( p . ok === false && p . message ) throw Error ( String ( p . message ) ) ; return p }
function applyConfig ( p ) { p = payload ( p ) || { } ; var m = obj ( p . meta ) ? p . meta : { } , w = obj ( m . widget ) ? m . widget : { } , mod = String ( w . module || m . module || state . module || DEFAULT _MODULE ) ; state . module = M [ mod ] ? mod : DEFAULT _MODULE ; state . config = merge ( S [ state . module ] , w ) ; if ( ! obj ( state . config . sourceArgs ) && obj ( m . sourceArgs ) ) state . config . sourceArgs = m . sourceArgs ; state . lang = lang ( ) ; labels ( ) }
function labels ( ) { var m = ( M [ state . module ] || M . discussions ) [ state . lang ] || M . discussions . ru ; document . documentElement . lang = state . lang ; e . title . textContent = m [ 0 ] ; e . desc . textContent = m [ 1 ] ; e . search . placeholder = t ( 'search' ) ; e . refresh . textContent = t ( 'refresh' ) ; e . empty . textContent = t ( 'empty' ) ; e . close . setAttribute ( 'aria-label' , t ( 'close' ) ) ; e . ht . textContent = m [ 2 ] ; e . hs . textContent = t ( 'status' ) ; e . hd . textContent = m [ 3 ] ; e . hv . textContent = m [ 4 ] }
function req ( method , params ) { var o = typeof window !== 'undefined' && window . openai ; if ( method === 'tools/call' && o && typeof o . callTool === 'function' ) return Promise . resolve ( o . callTool ( params . name , params . arguments || { } ) ) ; var id = nextId ++ ; window . parent . postMessage ( { jsonrpc : '2.0' , id : id , method : method , params : params || { } } , '*' ) ; return new Promise ( function ( resolve , reject ) { var timeout = window . setTimeout ( function ( ) { pending . delete ( id ) ; reject ( Error ( t ( 'load' ) ) ) } , 12000 ) ; pending . set ( id , { resolve : resolve , reject : reject , timeout : timeout } ) } ) }
function notify ( method , params ) { window . parent . postMessage ( { jsonrpc : '2.0' , method : method , params : params || { } } , '*' ) }
function path ( x , p ) { return String ( p || '' ) . split ( '.' ) . reduce ( function ( v , k ) { return v && typeof v === 'object' ? v [ k ] : undefined } , x ) }
function findArray ( x , d ) { if ( Array . isArray ( x ) ) return x ; if ( ! obj ( x ) || d > 2 ) return null ; var keys = [ 'items' , 'records' , 'bookings' , 'upcoming_bookings' , 'topics' , 'notifications' , 'pages' , 'spaces' , 'controllers' , 'agents' , 'user_agents' , 'tables' , 'monitors' , 'data' ] ; for ( var i = 0 ; i < keys . length ; i ++ ) if ( Array . isArray ( x [ keys [ i ] ] ) ) return x [ keys [ i ] ] ; for ( var k in x ) if ( Object . prototype . hasOwnProperty . call ( x , k ) ) { var a = findArray ( x [ k ] , d + 1 ) ; if ( a ) return a } return null }
function flatten ( a ) { var n = state . config . nested || state . config . nestedCollection ; if ( ! n ) return a ; var out = [ ] ; a . forEach ( function ( parent ) { if ( parent && Array . isArray ( parent [ n ] ) ) parent [ n ] . forEach ( function ( child ) { var x = Object . assign ( { } , child ) ; if ( parent . name && ! x . group _name ) x . group _name = parent . name ; out . push ( x ) } ) ; else out . push ( parent ) } ) ; return out }
function list ( x ) { var p = payload ( x ) || { } ; if ( Array . isArray ( p . items ) ) return p . items ; var ps = Array . isArray ( state . config . paths ) ? state . config . paths : ( Array . isArray ( state . config . arrayPaths ) ? state . config . arrayPaths : [ ] ) , combined = [ ] ; for ( var i = 0 ; i < ps . length ; i ++ ) { var c = path ( p , ps [ i ] ) , a = Array . isArray ( c ) ? c : findArray ( c , 0 ) ; if ( a ) { if ( state . config . combinePaths ) combined = combined . concat ( a ) ; else return flatten ( a ) } } if ( state . config . combinePaths && combined . length ) return flatten ( combined ) ; var d = p . data === undefined ? p : p . data ; return flatten ( Array . isArray ( d ) ? d : ( findArray ( d , 0 ) || [ ] ) ) }
function str ( x ) { if ( x === null || x === undefined || x === '' ) return '' ; if ( typeof x === 'object' ) { try { return JSON . stringify ( x ) } catch ( err ) { return '' } } return String ( x ) }
function id ( x , i ) { var keys = [ 'id' , 'key' , 'code' , 'uuid' , 'page_id' , 'topic_id' , 'booking_id' , 'item_id' , 'monitor_id' , 'table' ] ; for ( var n = 0 ; n < keys . length ; n ++ ) if ( x && x [ keys [ n ] ] !== undefined && x [ keys [ n ] ] !== null && x [ keys [ n ] ] !== '' ) return str ( x [ keys [ n ] ] ) ; return String ( i + 1 ) }
function title ( x , i ) { return str ( x && ( x . title || x . name || x . subject || x . display _name || x . label || x . table _name || x . table || x . key ) ) || ( t ( 'records' ) + ' ' + ( i + 1 ) ) }
function date ( x ) { return str ( x && ( x . starts _at || x . start _at || x . due _date || x . checked _at || x . updated _at || x . created _at || x . sent _at || x . published _at || x . date ) ) }
function value ( x ) { if ( ! x ) return '' ; if ( x . availability _percent !== undefined && x . availability _percent !== null ) return str ( x . availability _percent ) + '%' ; if ( x . quantity !== undefined && x . quantity !== null ) return str ( x . quantity ) ; if ( x . record _count !== undefined && x . record _count !== null ) return str ( x . record _count ) ; return str ( x . value !== undefined ? x . value : ( x . guest _name || x . space _name || x . group _name || x . controller _type || x . type || x . source || x . module || '' ) ) }
function sub ( x ) { return str ( x && ( x . subtitle || x . project _code || x . space _name || x . channel _name || x . group _name || x . code || x . email ) ) }
function normalize ( x , i ) { var y = obj ( x ) ? Object . assign ( { } , x ) : { value : x } ; y . id = id ( x , i ) ; y . title = title ( x , i ) ; y . subtitle = sub ( x ) ; y . status = status ( x ) ; y . date = date ( x ) ; y . value = value ( x ) ; return y }
function dateText ( x ) { if ( ! x ) return '—' ; var n = Date . parse ( x ) ; if ( Number . isNaN ( n ) ) return String ( x ) ; try { return new Intl . DateTimeFormat ( state . lang , { dateStyle : 'medium' , timeStyle : 'short' } ) . format ( new Date ( n ) ) } catch ( err ) { return String ( x ) } }
function make ( tag , v , cls ) { var n = document . createElement ( tag ) ; if ( cls ) n . className = cls ; if ( v !== undefined ) n . textContent = v ; return n }
function detailText ( x ) { return x === null || x === undefined || x === '' ? '—' : str ( x ) }
function renderSummary ( ) { e . summary . replaceChildren ( ) ; var n = make ( 'span' ) ; n . append ( make ( 'strong' , String ( rows . length ) ) , ' ' + t ( 'records' ) ) ; e . summary . append ( n ) ; var c = { } ; rows . forEach ( function ( x ) { if ( x . status ) c [ x . status ] = ( c [ x . status ] || 0 ) + 1 } ) ; Object . keys ( c ) . slice ( 0 , 3 ) . forEach ( function ( k ) { var n = make ( 'span' ) ; n . append ( make ( 'strong' , String ( c [ k ] ) ) , ' ' + k ) ; e . summary . append ( n ) } ) }
function field ( x , keys ) { for ( var i = 0 ; i < keys . length ; i ++ ) { var v = x && x [ keys [ i ] ] ; if ( v !== undefined && v !== null && v !== '' ) return v } return '' }
function numeric ( x , keys ) { var v = field ( x , keys ) ; if ( v === null || v === undefined || v === '' ) return null ; if ( typeof v === 'number' ) return Number . isFinite ( v ) ? v : null ; var n = Number ( String ( v ) . replace ( /,/g , '.' ) . replace ( /[^0-9.-]/g , '' ) ) ; return Number . isFinite ( n ) ? n : null }
function groupValue ( x , key ) { var v = key ? field ( x , [ key ] ) : status ( x ) ; if ( obj ( v ) ) v = v . name || v . label || v . code || v . id ; return str ( v ) || '—' }
function visualCard ( x , extra ) { var b = make ( 'button' , undefined , 'visual-card' + ( extra ? ' ' + extra : '' ) ) ; b . type = 'button' ; b . append ( make ( 'span' , x . title || 'SHD' , 'visual-title' ) ) ; if ( x . subtitle ) b . append ( make ( 'span' , x . subtitle , 'sub' ) ) ; var line = make ( 'span' , undefined , 'visual-line' ) ; line . append ( make ( 'span' , x . status || '—' , 'pill' ) ) ; if ( x . date ) line . append ( make ( 'span' , dateText ( x . date ) , 'visual-date' ) ) ; b . append ( line ) ; var v = field ( x , [ 'total_amount' , 'amount' , 'planned_hours' , 'utilization_percent' , 'score' , 'quantity' , 'revision' , 'pages' , 'value' ] ) ; if ( v !== '' && v !== null ) b . append ( make ( 'span' , str ( v ) , 'visual-value' ) ) ; b . onclick = function ( ) { details ( x ) } ; return b }
function visualEmpty ( ) { e . visual . replaceChildren ( make ( 'div' , t ( 'empty' ) , 'empty' ) ) }
function renderKanban ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var groups = { } ; a . forEach ( function ( x ) { var g = groupValue ( x , state . config . groupBy || 'status' ) ; ( groups [ g ] || ( groups [ g ] = [ ] ) ) . push ( x ) } ) ; var board = make ( 'div' , undefined , 'kanban' ) ; Object . keys ( groups ) . forEach ( function ( g ) { var col = make ( 'section' , undefined , 'kanban-col' ) ; var head = make ( 'div' , undefined , 'kanban-head' ) ; head . append ( make ( 'span' , g ) , make ( 'span' , String ( groups [ g ] . length ) , 'pill' ) ) ; col . append ( head ) ; var list = make ( 'div' , undefined , 'kanban-list' ) ; groups [ g ] . forEach ( function ( x ) { list . append ( visualCard ( x ) ) } ) ; col . append ( list ) ; board . append ( col ) } ) ; e . visual . append ( board ) }
function renderTimeline ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var wrap = make ( 'div' , undefined , 'timeline' ) ; a . slice ( ) . sort ( function ( x , y ) { return String ( x . date || '' ) . localeCompare ( String ( y . date || '' ) ) } ) . forEach ( function ( x ) { var item = make ( 'article' , undefined , 'timeline-item' ) , rail = make ( 'span' , undefined , 'timeline-rail' ) , dot = make ( 'span' , undefined , 'timeline-dot' ) , content = make ( 'div' , undefined , 'timeline-content' ) ; content . append ( make ( 'div' , dateText ( x . date ) , 'timeline-meta' ) , visualCard ( x , 'timeline-card' ) ) ; item . append ( rail , dot , content ) ; wrap . append ( item ) } ) ; e . visual . append ( wrap ) }
function renderFunnel ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var groups = { } , amounts = { } ; a . forEach ( function ( x ) { var g = groupValue ( x , state . config . groupBy || 'stage' ) ; groups [ g ] = ( groups [ g ] || 0 ) + 1 ; amounts [ g ] = ( amounts [ g ] || 0 ) + ( numeric ( x , [ 'total_amount' , 'amount' , 'value' , 'sum' ] ) || 0 ) } ) ; var max = Math . max . apply ( null , Object . values ( groups ) ) ; var wrap = make ( 'div' , undefined , 'funnel' ) ; Object . keys ( groups ) . forEach ( function ( g ) { var row = make ( 'div' , undefined , 'funnel-row' ) , bar = document . createElement ( 'progress' ) ; bar . className = 'funnel-fill' ; bar . max = max || 1 ; bar . value = groups [ g ] ; row . append ( make ( 'span' , g ) , bar , make ( 'span' , String ( groups [ g ] ) + ( amounts [ g ] ? ' · ' + String ( amounts [ g ] ) : '' ) ) ) ; wrap . append ( row ) } ) ; e . visual . append ( wrap ) }
function metric ( label , value ) { var d = make ( 'div' , undefined , 'metric' ) ; d . append ( make ( 'span' , label , 'metric-label' ) , make ( 'span' , value === null || value === undefined ? '—' : str ( value ) , 'metric-value' ) ) ; return d }
function renderDashboard ( a , mode ) { var grid = make ( 'div' , undefined , 'metric-grid' ) , total = a . length , amount = 0 , planned = 0 , capacity = 0 , util = 0 , overload = 0 ; a . forEach ( function ( x ) { amount += numeric ( x , [ 'total_amount' , 'amount' , 'total' , 'sum' , 'value' ] ) || 0 ; planned += numeric ( x , [ 'planned_hours' , 'planned' , 'hours' ] ) || 0 ; capacity += numeric ( x , [ 'capacity_hours' , 'capacity' ] ) || 0 ; util += numeric ( x , [ 'utilization_percent' , 'utilization' ] ) || 0 ; overload += numeric ( x , [ 'overload_hours' , 'overload' ] ) || 0 } ) ; grid . append ( metric ( t ( 'total' ) , total ) ) ; if ( mode === 'workload' ) { grid . append ( metric ( t ( 'planned' ) , planned || null ) , metric ( t ( 'capacity' ) , capacity || null ) , metric ( t ( 'utilization' ) , a . length ? Math . round ( util / a . length ) + '%' : null ) , metric ( t ( 'overload' ) , overload || 0 ) ) } else { grid . append ( metric ( t ( 'amount' ) , amount || null ) , metric ( t ( 'status' ) , Object . keys ( a . reduce ( function ( m , x ) { m [ groupValue ( x , 'status' ) ] = 1 ; return m } , { } ) ) . length ) ) } e . visual . append ( grid ) ; if ( ! a . length ) { visualEmpty ( ) ; return } var list = make ( 'div' , undefined , 'kanban-list' ) ; a . slice ( 0 , 60 ) . forEach ( function ( x ) { list . append ( visualCard ( x ) ) } ) ; e . visual . append ( list ) }
function approvalValue ( x ) { return groupValue ( x , 'approval_status' ) !== '—' ? groupValue ( x , 'approval_status' ) : groupValue ( x , 'approval_state' ) !== '—' ? groupValue ( x , 'approval_state' ) : groupValue ( x , 'status' ) }
function renderStatusSummary ( a , keys ) { if ( ! a . length ) { visualEmpty ( ) ; return } var counts = { } ; a . forEach ( function ( x ) { var g = groupValue ( x , keys [ 0 ] ) ; if ( g === '—' ) g = groupValue ( x , keys [ 1 ] ) ; if ( g === '—' ) g = groupValue ( x , 'status' ) ; counts [ g ] = ( counts [ g ] || 0 ) + 1 } ) ; var grid = make ( 'div' , undefined , 'metric-grid' ) ; Object . keys ( counts ) . slice ( 0 , 6 ) . forEach ( function ( g ) { grid . append ( metric ( g , counts [ g ] ) ) } ) ; e . visual . append ( grid ) ; var list = make ( 'div' , undefined , 'kanban-list' ) ; a . slice ( 0 , 60 ) . forEach ( function ( x ) { list . append ( visualCard ( x ) ) } ) ; e . visual . append ( list ) }
function progressValue ( x ) { var n = numeric ( x , [ 'score' , 'completeness_score' , 'completion_percent' , 'progress' , 'availability_percent' ] ) ; if ( n !== null ) return Math . max ( 0 , Math . min ( 100 , n ) ) ; if ( x . complete === true || x . is _complete === true || String ( status ( x ) ) . toLowerCase ( ) === 'complete' ) return 100 ; return 0 }
function renderProgress ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var grid = make ( 'div' , undefined , 'preview-grid' ) ; a . forEach ( function ( x ) { var card = make ( 'article' , undefined , 'progress-card' ) ; card . append ( make ( 'strong' , x . title || 'SHD' , 'visual-title' ) ) ; var meta = make ( 'span' , x . subtitle || dateText ( x . date ) , 'visual-date' ) ; card . append ( meta ) ; var p = document . createElement ( 'progress' ) ; p . className = 'progress' ; p . max = 100 ; p . value = progressValue ( x ) ; card . append ( p , make ( 'span' , t ( 'progress' ) + ': ' + String ( p . value ) + '%' , 'visual-value' ) ) ; card . onclick = function ( ) { details ( x ) } ; grid . append ( card ) } ) ; e . visual . append ( grid ) }
function isWarning ( x ) { var s = String ( status ( x ) ) . toLowerCase ( ) ; if ( x . low _stock || x . is _low _stock || x . critical || x . warning ) return true ; if ( /low|critical|out|error|overdue|maintenance/ . test ( s ) ) return true ; var q = numeric ( x , [ 'quantity' , 'available_quantity' , 'stock' ] ) ; var min = numeric ( x , [ 'min_quantity' , 'minimum_quantity' , 'reorder_level' ] ) ; return q !== null && min !== null && q <= min }
function renderWarnings ( a ) { var warnings = a . filter ( isWarning ) , grid = make ( 'div' , undefined , 'metric-grid' ) ; grid . append ( metric ( t ( 'warnings' ) , warnings . length ) , metric ( t ( 'total' ) , a . length ) ) ; e . visual . append ( grid ) ; if ( ! warnings . length ) { e . visual . append ( make ( 'div' , t ( 'noWarnings' ) , 'empty' ) ) ; return } var list = make ( 'div' , undefined , 'kanban-list' ) ; warnings . forEach ( function ( x ) { list . append ( visualCard ( x , 'warning' ) ) } ) ; e . visual . append ( list ) }
function renderCalendar ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var days = { } ; a . forEach ( function ( x ) { var d = x . date ? String ( x . date ) . slice ( 0 , 10 ) : '—' ; ( days [ d ] || ( days [ d ] = [ ] ) ) . push ( x ) } ) ; var cal = make ( 'div' , undefined , 'calendar' ) ; Object . keys ( days ) . forEach ( function ( d ) { var day = make ( 'section' , undefined , 'calendar-day' ) ; day . append ( make ( 'div' , d , 'calendar-head' ) ) ; days [ d ] . forEach ( function ( x ) { day . append ( visualCard ( x ) ) } ) ; cal . append ( day ) } ) ; e . visual . append ( cal ) }
function memberName ( x ) { var v = field ( x , [ 'member_name' , 'username' , 'email' , 'name' , 'display_name' , 'user' ] ) ; if ( obj ( v ) ) return str ( v . name || v . username || v . email || v . id ) ; return str ( v ) || x . id }
function permissionText ( x ) { var p = x . permissions || x . permission || x . scopes || '' ; if ( obj ( p ) ) return Object . keys ( p ) . filter ( function ( k ) { return p [ k ] === true || p [ k ] === 1 || typeof p [ k ] === 'string' } ) . map ( function ( k ) { return k + ( typeof p [ k ] === 'string' ? '=' + p [ k ] : '' ) } ) . join ( ', ' ) || '—' ; if ( Array . isArray ( p ) ) return p . join ( ', ' ) || '—' ; return str ( p ) || '—' }
function renderMatrix ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var wrap = make ( 'div' , undefined , 'matrix-wrap' ) , table = make ( 'table' , undefined , 'matrix-table' ) , head = make ( 'tr' ) ; [ t ( 'member' ) , t ( 'role' ) , t ( 'status' ) , t ( 'permissions' ) ] . forEach ( function ( x ) { head . append ( make ( 'th' , x ) ) } ) ; table . append ( make ( 'thead' ) . appendChild ( head ) . parentNode ) ; var body = make ( 'tbody' ) ; a . forEach ( function ( x ) { var row = make ( 'tr' ) ; row . append ( make ( 'td' , memberName ( x ) ) , make ( 'td' , str ( field ( x , [ 'role' , 'organization_role' , 'type' ] ) ) || '—' ) , make ( 'td' , status ( x ) || '—' ) , make ( 'td' , permissionText ( x ) ) ) ; row . onclick = function ( ) { details ( x ) } ; row . className = 'record' ; body . append ( row ) } ) ; table . append ( body ) ; wrap . append ( table ) ; e . visual . append ( wrap ) }
function treeId ( x , i ) { return str ( field ( x , [ 'id' , 'space_id' , 'page_id' , 'collection_id' , 'key' ] ) ) || String ( i ) }
function treeParent ( x ) { return str ( field ( x , [ 'parent_id' , 'parentId' , 'parent_space_id' , 'parent_collection_id' ] ) ) }
function renderTree ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var by = { } ; a . forEach ( function ( x , i ) { var p = treeParent ( x ) ; ( by [ p ] || ( by [ p ] = [ ] ) ) . push ( { x : x , i : i } ) } ) ; var tree = make ( 'div' , undefined , 'tree' ) ; function branch ( parent , host , seen ) { ( by [ parent ] || [ ] ) . forEach ( function ( entry ) { var id = treeId ( entry . x , entry . i ) ; if ( seen [ id ] ) return ; var wrap = make ( 'div' , undefined , 'tree-node' ) ; wrap . append ( visualCard ( entry . x ) ) ; host . append ( wrap ) ; var child = make ( 'div' , undefined , 'tree-children' ) ; branch ( id , child , Object . assign ( { } , seen , ( function ( ) { var o = { } ; o [ id ] = true ; return o } ) ( ) ) ) ; if ( child . childNodes . length ) wrap . append ( child ) } ) } branch ( '' , tree , { } ) ; if ( ! tree . childNodes . length ) a . forEach ( function ( x ) { tree . append ( visualCard ( x ) ) } ) ; e . visual . append ( tree ) }
function renderPreview ( a ) { if ( ! a . length ) { visualEmpty ( ) ; return } var grid = make ( 'div' , undefined , 'preview-grid' ) ; a . forEach ( function ( x ) { var card = make ( 'article' , undefined , 'preview-card' ) ; card . append ( make ( 'strong' , x . title || 'SHD' , 'visual-title' ) ) ; var fields = make ( 'div' , undefined , 'preview-fields' ) ; Object . keys ( x ) . filter ( function ( k ) { return [ 'id' , 'title' , 'subtitle' , 'status' , 'date' , 'value' , 'meta' ] . indexOf ( k ) === - 1 } ) . slice ( 0 , 10 ) . forEach ( function ( k ) { var cell = make ( 'div' , undefined , 'preview-field' ) ; cell . append ( make ( 'strong' , k + ': ' ) , make ( 'span' , str ( x [ k ] ) ) ) ; fields . append ( cell ) } ) ; card . append ( fields ) ; card . onclick = function ( ) { details ( x ) } ; grid . append ( card ) } ) ; e . visual . append ( grid ) }
function renderVisual ( a ) { e . visual . replaceChildren ( ) ; var v = state . config . view || 'table' ; if ( v === 'kanban' ) renderKanban ( a ) ; else if ( v === 'workload' || v === 'dashboard' ) renderDashboard ( a , v ) ; else if ( v === 'timeline' ) renderTimeline ( a ) ; else if ( v === 'funnel' ) renderFunnel ( a ) ; else if ( v === 'approval' ) renderStatusSummary ( a , [ 'approval_status' , 'approval_state' ] ) ; else if ( v === 'completeness' || v === 'progress' ) renderProgress ( a ) ; else if ( v === 'health' ) renderStatusSummary ( a , [ 'health' , 'status' ] ) ; else if ( v === 'calendar' ) renderCalendar ( a ) ; else if ( v === 'matrix' ) renderMatrix ( a ) ; else if ( v === 'warnings' ) renderWarnings ( a ) ; else if ( v === 'tree' ) renderTree ( a ) ; else if ( v === 'preview' ) renderPreview ( a ) ; else renderDashboard ( a , 'dashboard' ) }
function renderTable ( a ) { e . rows . replaceChildren ( ) ; e . empty . hidden = a . length !== 0 ; a . forEach ( function ( x ) { var r = document . createElement ( 'tr' ) ; r . className = 'record' ; r . tabIndex = 0 ; r . setAttribute ( 'role' , 'button' ) ; r . setAttribute ( 'aria-label' , x . title ) ; var n = make ( 'td' ) ; n . append ( make ( 'span' , x . title , 'title' ) ) ; if ( x . subtitle ) n . append ( make ( 'span' , x . subtitle , 'sub' ) ) ; var s = make ( 'span' , x . status || '—' , 'pill' ) ; if ( x . status ) s . classList . add ( String ( x . status ) . toLowerCase ( ) . replace ( /[^a-z0-9_-]+/g , '-' ) ) ; var sc = make ( 'td' ) ; sc . append ( s ) ; r . append ( n , sc , make ( 'td' , dateText ( x . date ) ) , make ( 'td' , x . value || '—' ) ) ; r . onclick = function ( ) { details ( x ) } ; r . onkeydown = function ( ev ) { if ( ev . key === 'Enter' || ev . key === ' ' ) { ev . preventDefault ( ) ; details ( x ) } } ; e . rows . append ( r ) } ) ; renderSummary ( ) }
function renderRows ( ) { var q = e . search . value . trim ( ) . toLowerCase ( ) , a = rows . filter ( function ( x ) { return ! q || [ x . title , x . subtitle , x . status , x . id , x . value ] . map ( str ) . join ( ' ' ) . toLowerCase ( ) . indexOf ( q ) !== - 1 } ) ; if ( state . config . view && state . config . view !== 'table' ) { e . tableWrap . hidden = true ; e . visual . hidden = false ; e . empty . hidden = true ; renderVisual ( a ) ; renderSummary ( ) ; return } e . tableWrap . hidden = false ; e . visual . hidden = true ; renderTable ( a ) }
function render ( x ) { var p = unwrap ( x ) ; applyConfig ( p ) ; rows = list ( p ) . map ( normalize ) ; var dir = state . config . sort === 'asc' ? 1 : - 1 ; rows . sort ( function ( a , b ) { var x = Date . parse ( a . date ) , y = Date . parse ( b . date ) ; if ( ! Number . isNaN ( x ) && ! Number . isNaN ( y ) && x !== y ) return ( x - y ) * dir ; return a . title . localeCompare ( b . title , state . lang ) * dir } ) ; renderRows ( ) }
function setStatus ( x , bad ) { e . status . textContent = x || '' ; e . status . className = bad ? 'status error' : 'status' }
function add ( k , v ) { var d = make ( 'div' , undefined , 'detail' ) ; d . append ( make ( 'span' , k , 'label' ) , make ( 'span' , detailText ( v ) , 'value' ) ) ; e . grid . append ( d ) }
function show ( x ) { e . detailTitle . textContent = x . title || 'SHD' ; e . detailSub . textContent = x . subtitle || x . id || '' ; e . grid . replaceChildren ( ) ; add ( t ( 'id' ) , x . id ) ; if ( x . status ) add ( t ( 'status' ) , x . status ) ; if ( x . date ) add ( t ( 'date' ) , dateText ( x . date ) ) ; if ( x . value ) add ( t ( 'value' ) , x . value ) ; Object . keys ( x ) . filter ( function ( k ) { return [ 'id' , 'title' , 'subtitle' , 'status' , 'date' , 'value' , 'meta' ] . indexOf ( k ) === - 1 } ) . slice ( 0 , 8 ) . forEach ( function ( k ) { add ( k , x [ k ] ) } ) ; e . details . hidden = false }
function details ( x ) { show ( x ) ; if ( ! state . config . readTool ) return ; var k = state . config . detailKey ; if ( ! k ) return ; var v = x [ k ] === undefined ? x . id : x [ k ] ; if ( v === undefined || v === null || v === '' ) return ; if ( state . module === 'status-page' && ! /^\d+$/ . test ( String ( v ) ) ) return ; var a = { } ; if ( Array . isArray ( state . config . detailSourceArgs ) ) { var ctx = merge ( state . config . listArgs , state . config . sourceArgs ) ; state . config . detailSourceArgs . forEach ( function ( name ) { if ( ctx [ name ] !== undefined ) a [ name ] = ctx [ name ] } ) } else a = merge ( state . config . sourceArgs , state . config . detailArgs ) ; a = merge ( a , state . config . detailArgs ) ; a [ k ] = k === 'table' ? String ( v ) : ( Number ( v ) || v ) ; req ( 'tools/call' , { name : state . config . readTool , arguments : a } ) . then ( function ( result ) { var p = unwrap ( result ) , d = p . data === undefined ? p : p . data ; e . grid . replaceChildren ( ) ; if ( Array . isArray ( d ) ) { add ( t ( 'records' ) , d . length ) ; d . slice ( 0 , 6 ) . forEach ( function ( q , i ) { add ( String ( i + 1 ) , q && ( q . message || q . body || q . content || q . title || q ) ) } ) } else if ( obj ( d ) ) Object . keys ( d ) . slice ( 0 , 12 ) . forEach ( function ( q ) { add ( q , d [ q ] ) } ) ; else add ( t ( 'details' ) , d ) } ) . catch ( function ( err ) { setStatus ( err . message || t ( 'detail' ) , true ) } ) }
function contextReady ( a ) { if ( ! state . config . contextRequired ) return true ; if ( Array . isArray ( state . config . contextSets ) && state . config . contextSets . length ) return state . config . contextSets . every ( function ( set ) { return set . some ( function ( k ) { return a [ k ] !== undefined && a [ k ] !== null && a [ k ] !== '' } ) } ) ; var keys = Array . isArray ( state . config . contextKeys ) && state . config . contextKeys . length ? state . config . contextKeys : [ 'base' , 'project_code' , 'project' ] ; var ready = state . config . contextAll ? keys . every ( function ( k ) { return a [ k ] !== undefined && a [ k ] !== null && a [ k ] !== '' } ) : keys . some ( function ( k ) { return a [ k ] !== undefined && a [ k ] !== null && a [ k ] !== '' } ) ; return ready }
function refresh ( ) { if ( state . busy ) return ; var a = merge ( state . config . listArgs , state . config . sourceArgs ) ; if ( ! contextReady ( a ) ) { setStatus ( t ( 'context' ) , true ) ; return } if ( ! state . config . listTool ) return ; state . busy = true ; e . refresh . disabled = true ; setStatus ( t ( 'loading' ) , false ) ; req ( 'tools/call' , { name : state . config . listTool , arguments : a } ) . then ( function ( x ) { render ( x ) ; setStatus ( '' , false ) } ) . catch ( function ( err ) { setStatus ( err . message || t ( 'load' ) , true ) } ) . finally ( function ( ) { state . busy = false ; e . refresh . disabled = false } ) }
function init ( ) { return req ( 'ui/initialize' , { protocolVersion : '2025-11-25' , appInfo : { name : 'SHD ' + ( M [ state . module ] || M . discussions ) . ru [ 0 ] , title : 'SHD MCP' , version : '1.0.0' , websiteUrl : 'https://shd.xyz.su' } , appCapabilities : { } } ) . then ( function ( ) { notify ( 'ui/notifications/initialized' , { } ) } ) . catch ( function ( ) { } ) }
window . addEventListener ( 'message' , function ( ev ) { if ( ev . source !== window . parent ) return ; var m = ev . data ; if ( ! m || m . jsonrpc !== '2.0' ) return ; if ( m . id !== undefined && pending . has ( m . id ) ) { var q = pending . get ( m . id ) ; pending . delete ( m . id ) ; window . clearTimeout ( q . timeout ) ; if ( m . error ) q . reject ( Error ( m . error . message || t ( 'load' ) ) ) ; else q . resolve ( m . result ) ; return } if ( m . method === 'ui/notifications/tool-input' && ! rows . length ) { try { render ( m . params || { } ) } catch ( err ) { setStatus ( err . message || t ( 'load' ) , true ) } } if ( m . method === 'ui/notifications/tool-result' ) { try { render ( m . params && m . params . structuredContent ) } catch ( err ) { setStatus ( err . message || t ( 'load' ) , true ) } } } , { passive : true } ) ;
e . search . oninput = renderRows ; e . refresh . onclick = refresh ; e . close . onclick = function ( ) { e . details . hidden = true } ; state . lang = lang ( ) ; labels ( ) ; void init ( ) ; var out = typeof window !== 'undefined' && window . openai ? window . openai . toolOutput : null ; if ( out ) { try { render ( out ) } catch ( err ) { setStatus ( err . message || t ( 'load' ) , true ) } } else { var input = typeof window !== 'undefined' && window . openai ? window . openai . toolInput : null ; if ( input ) { try { render ( input ) } catch ( err ) { setStatus ( err . message || t ( 'load' ) , true ) } } else refresh ( ) } }
} ( ) ) ;
< / script >
< / body >
< / html >