@goodandready/dsh-model-sync 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/README.ru.md +15 -0
- package/lib/client.js +203 -167
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,21 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
+
## 🛠️ Enhancements & Fixes in v0.3.8
|
|
111
|
+
|
|
112
|
+
* ⚙️ **Reactive Settings Card Binding (`settingsScope`)**:
|
|
113
|
+
* Integrated modern DSH kernel `settingsScope` service (`ctx.settingsScope.bind({ namespace: 'dsh-model-sync' })`) with `useSyncExternalStore`.
|
|
114
|
+
* Added direct configuration controls for the background scheduler in the Web UI: enable/disable background sync (`scheduleEnabled`), customize interval (`intervalMinutes`), and toggle automatic application of new models (`autoApply`).
|
|
115
|
+
* Gracefully handles all snapshot lifecycle states (`ready`, `loading`, `unavailable`) and collects field validation errors without dropping form drafts.
|
|
116
|
+
* 🌐 **Full Model Picker Localization**:
|
|
117
|
+
* Translated all previously hardcoded strings in the model selector, including search input placeholder and sorting options (`Name A→Z`, `Price ↑`, `Context ↓`, `Newest ↓`), dynamically reacting to DSH locale switching (`en` / `ru`).
|
|
118
|
+
* 📜 **Synchronization History Localization**:
|
|
119
|
+
* Added the missing `noHistory` key to English and Russian localization dictionaries, ensuring informative feedback when sync history is empty.
|
|
120
|
+
* 📋 **Project Design Contract**:
|
|
121
|
+
* Added authoritative `docs/design/DESIGN.md` defining UI surfaces, styling foundations, token conventions, and state contracts.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
110
125
|
## 🛠️ Enhancements & Fixes in v0.3.6
|
|
111
126
|
|
|
112
127
|
* 🔑 **Universal Credential Resolution (`credentialRef` & `apiKeyRef`)**:
|
package/README.ru.md
CHANGED
|
@@ -84,6 +84,21 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
87
|
+
## 🛠️ Улучшения и исправления в v0.3.8
|
|
88
|
+
|
|
89
|
+
* ⚙️ **Реактивное управление настройками через `settingsScope`**:
|
|
90
|
+
* Интегрирована встроенная служба ядра DSH `settingsScope` (`ctx.settingsScope.bind({ namespace: 'dsh-model-sync' })`) с использованием `useSyncExternalStore`.
|
|
91
|
+
* В карточку настроек плагина добавлены элементы прямого управления планировщиком: включение фонового расписания (`scheduleEnabled`), настройка интервала синхронизации (`intervalMinutes`) и флаг автоприменения изменений (`autoApply`).
|
|
92
|
+
* Корректная обработка всех статусов снимка (`ready`, `loading`, `unavailable`) с валидацией и сохранением полей без прерывания на первой ошибке.
|
|
93
|
+
* 🌐 **Полная локализация пикера моделей**:
|
|
94
|
+
* Заменены захардкоженные русские строки в селекторе моделей: плейсхолдер поиска и варианты сортировки (`Имя A→Z`, `Цена ↑`, `Контекст ↓`, `Новые ↓`) теперь динамически переключаются при смене языка интерфейса (`en` / `ru`).
|
|
95
|
+
* 📜 **Локализация пустой истории синхронизаций**:
|
|
96
|
+
* Добавлен отсутствовавший ключ `noHistory` в словари английского и русского языков («No synchronization history.» / «Истории синхронизаций нет.»).
|
|
97
|
+
* 📋 **Дизайн-контракт проекта**:
|
|
98
|
+
* Создан обязательный документ `docs/design/DESIGN.md`, фиксирующий пользовательские поверхности, токены `--dsw-*` и контракты компонентов.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
87
102
|
## 🛠️ Улучшения и исправления в v0.3.6
|
|
88
103
|
|
|
89
104
|
* 🔑 **Универсальное разрешение учетных данных (`credentialRef` и `apiKeyRef`)**:
|
package/lib/client.js
CHANGED
|
@@ -1,167 +1,203 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({
|
|
2
|
-
id: '@goodandready/dsh-model-sync',
|
|
3
|
-
factory: (require) => {
|
|
4
|
-
var module = { exports: {} }
|
|
5
|
-
var exports = module.exports
|
|
6
|
-
const React = require('react')
|
|
7
|
-
|
|
8
|
-
const CSS = '.dms-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;margin:0;padding:0;overflow:hidden;box-sizing:border-box}' + '.dms-card-open{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-border-l2)}' + '.dms-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' + '.dms-card-header:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.05))}' + '.dms-card-header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}' + '.dms-card-head-text{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' + '.dms-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' + '.dms-card-description{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.45}' + '.dms-card-chevron{color:var(--dsw-alias-label-tertiary);flex:0 0 auto;transition:transform .12s}.dms-card-chevron-open{transform:rotate(180deg)}' + '.dms-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px;display:flex;flex-direction:column;gap:10px;box-sizing:border-box}' + '.dms-wrap{display:flex;flex-direction:column;gap:14px;max-width:760px;box-sizing:border-box;color:var(--dsw-alias-label-primary,inherit)}' +
|
|
9
|
-
'.dms-wrap>p{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}' +
|
|
10
|
-
'.dms-toolbar,.dms-section{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary,rgba(255,255,255,.14)));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box}' +
|
|
11
|
-
'.dms-toolbar>select{flex:1 1 190px;min-width:190px}' +
|
|
12
|
-
'.dms-toolbar>.dms-actions{flex-basis:100%}' +
|
|
13
|
-
'.dms-toolbar>label{display:inline-flex;align-items:flex-start;gap:7px;font-size:12px;line-height:1.35;color:var(--dsw-alias-label-secondary)}' +
|
|
14
|
-
'.dms-toolbar>select,.dms-editor input,.dms-editor textarea{box-sizing:border-box;min-width:0;background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2,transparent));border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2));border-radius:6px;color:var(--dsw-alias-label-primary);font:inherit;padding:7px 9px}' +
|
|
15
|
-
'.dms-toolbar>label input{accent-color:var(--dsw-alias-brand-primary,var(--dsw-alias-button-info-fill))}' +
|
|
16
|
-
'.dms-actions{display:flex;align-items:center;flex-wrap:wrap;gap:7px}' +
|
|
17
|
-
'.dms-wrap>.dms-actions{padding:10px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary,rgba(255,255,255,.14)));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent)}' +
|
|
18
|
-
'.dms-btn{margin:0!important;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:1.35;padding:6px 10px;cursor:pointer;white-space:normal}' +
|
|
19
|
-
'.dms-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.05))}' +
|
|
20
|
-
'.dms-btn:disabled{opacity:.45;cursor:default}' +
|
|
21
|
-
'.dms-section>strong{display:block;font-size:13px;font-weight:650}' +
|
|
22
|
-
'.dms-provider-wrap{margin:0 0 7px}' +
|
|
23
|
-
'.dms-provider{display:grid!important;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:10px 11px;margin:0;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent)}' +
|
|
24
|
-
'.dms-provider>span{min-width:0;overflow-wrap:anywhere;line-height:1.4}' +
|
|
25
|
-
'.dms-section>div[style]{padding:8px 0;border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
26
|
-
'.dms-editor{display:flex;flex-direction:column;align-items:stretch;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box;border-color:var(--dsw-alias-brand-primary,var(--dsw-alias-border-l2))}' +
|
|
27
|
-
'.dms-editor strong{font-size:13px}' +
|
|
28
|
-
'.dms-editor label{display:block;gap:5px;margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px}' +
|
|
29
|
-
'.dms-editor label input,.dms-editor label textarea{width:100%}' +
|
|
30
|
-
'.dms-editor input[type="checkbox"]{width:auto;accent-color:var(--dsw-alias-brand-primary)}' +
|
|
31
|
-
'@media (max-width:540px){.dms-provider{grid-template-columns:1fr!important}.dms-provider .dms-btn{justify-self:start}.dms-toolbar>div:first-child{align-items:stretch;flex-direction:column}.dms-toolbar>div:first-child select{width:100%}.dms-toolbar,.dms-section,.dms-editor{padding:11px 12px}}'
|
|
32
|
-
|
|
33
|
-
function ensureStyles() {
|
|
34
|
-
if (typeof document === 'undefined' || !document.head) return
|
|
35
|
-
const cssId = 'dsh-model-sync/settings.module.css'
|
|
36
|
-
if (document.querySelector('style[data-plugin-css="' + cssId + '"]')) return
|
|
37
|
-
const tag = document.createElement('style')
|
|
38
|
-
tag.textContent = CSS
|
|
39
|
-
tag.setAttribute('data-plugin', 'dsh-model-sync')
|
|
40
|
-
tag.setAttribute('data-plugin-css', cssId)
|
|
41
|
-
document.head.appendChild(tag)
|
|
42
|
-
}
|
|
43
|
-
const en = {
|
|
44
|
-
title: 'Model synchronization', showSettings: 'Show settings', hideSettings: 'Hide settings', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', refreshAll: 'Refresh all', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run', previewTitle: 'Changes preview', previewNone: 'No changes detected.', previewAdded: 'Added', previewRemoved: 'Removed', previewChanged: 'Changed', previewApply: 'Apply preview', previewDismiss: 'Close preview', selectModels: 'Choose models', saveSelection: 'Save and apply selection', selectAtLeastOne: 'Select at least one model or use All models.', cancel: 'Cancel', selected: 'selected', allModels: 'All models', noModels: 'No models in the current catalog.', capabilityFilter: 'Filter by capabilities', capabilityCount: 'shown', noCapabilities: 'No capability metadata in this catalog.', pickerNote: 'The selection is applied to the DSH model picker immediately.', health: 'Check availability', healthOk: 'available', healthError: 'unavailable', healthDormant: 'not configured', policy: 'Manual model selection', savePolicy: 'Save policy', policyInclude: 'Include patterns (one per line)', policyExclude: 'Exclude patterns (one per line)', policyRequire: 'Required capabilities (comma-separated)', policyDeny: 'Denied capabilities (comma-separated)', policyNote: 'Patterns match model id, name, and tags. Apply changes to update the DSH picker.', scheduler: 'Scheduler', nextRun: 'next run', schedulerOff: 'off', schedulerOn: 'on', removeMissing: 'Confirm stale removal', stale: 'stale', deprecated: 'deprecated', history: 'Synchronization history', historyRun: 'Run', diffAdded: 'added', diffRemoved: 'removed', diffRenamed: 'renamed', diffChanged: 'metadata changed', rollback: 'Rollback catalog', rollbackConfirm: 'Rollback this provider catalog to the selected snapshot?', credentials: 'Credential diagnostics', checkCredentials: 'Check credentials', missingCredential: 'not configured', credentialReady: 'configured', report: 'Run report', success: 'success', partial: 'partial success', failure: 'failed', empty: 'no configured providers', notifications: 'Notifications', markRead: 'Mark read', acknowledge: 'Acknowledge', noNotifications: 'No notifications.', selectAllFiltered: 'Select all shown', deselectAllFiltered: 'Deselect all shown', invertFiltered: 'Invert selection', filterContext100k: 'Context ≥ 100k', filterCheap: 'Cheap ≤ $1/1M', showMore: 'Show more (%d)', showingCount: 'Showing %d of %d models', deprecatesIn: 'Deprecates in %dd', cacheHoursAgo: 'cache %dh ago',
|
|
45
|
-
}
|
|
46
|
-
const ru = {
|
|
47
|
-
title: 'Синхронизация моделей', showSettings: 'Показать настройки', hideSettings: 'Скрыть настройки', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', refreshAll: 'Обновить все', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск', previewTitle: 'Предпросмотр изменений', previewNone: 'Изменений не обнаружено.', previewAdded: 'Добавлено', previewRemoved: 'Удалено', previewChanged: 'Изменено', previewApply: 'Применить изменения', previewDismiss: 'Закрыть предпросмотр', selectModels: 'Выбрать модели', saveSelection: 'Сохранить и применить выбор', selectAtLeastOne: 'Выберите хотя бы одну модель или нажмите «Все модели».', cancel: 'Отмена', selected: 'выбрано', allModels: 'Все модели', noModels: 'В текущем каталоге нет моделей.', capabilityFilter: 'Фильтр по возможностям', capabilityCount: 'показано', noCapabilities: 'В этом каталоге нет данных о возможностях.', pickerNote: 'Выбор сразу применяется к пикеру моделей DSH.', health: 'Проверить доступность', healthOk: 'доступен', healthError: 'недоступен', healthDormant: 'не настроен', policy: 'Ручной выбор моделей', savePolicy: 'Сохранить политику', policyInclude: 'Включать (по одному шаблону в строке)', policyExclude: 'Исключать (по одному шаблону в строке)', policyRequire: 'Обязательные возможности (через запятую)', policyDeny: 'Запрещённые возможности (через запятую)', policyNote: 'Шаблоны ищутся в id, названии и тегах модели. Нажмите «Применить», чтобы обновить пикер DSH.', scheduler: 'Планировщик', nextRun: 'следующий запуск', schedulerOff: 'выключен', schedulerOn: 'включён', removeMissing: 'Подтвердить удаление устаревших', stale: 'устарела', deprecated: 'deprecated', history: 'История синхронизаций', historyRun: 'Запуск', diffAdded: 'добавлено', diffRemoved: 'удалено', diffRenamed: 'переименовано', diffChanged: 'изменена метаинформация', rollback: 'Откатить каталог', rollbackConfirm: 'Откатить каталог этого провайдера к выбранному снимку?', credentials: 'Диагностика ключей', checkCredentials: 'Проверить ключи', missingCredential: 'не настроен', credentialReady: 'настроен', report: 'Итог запуска', success: 'успех', partial: 'частичный успех', failure: 'ошибка', empty: 'нет настроенных провайдеров', notifications: 'Уведомления', markRead: 'Прочитано', acknowledge: 'Подтвердить', noNotifications: 'Новых уведомлений нет.', selectAllFiltered: 'Выбрать все показанные', deselectAllFiltered: 'Снять выбор с показанных', invertFiltered: 'Инвертировать выбор', filterContext100k: 'Контекст ≥ 100k', filterCheap: 'Дешевые ≤ $1/1M', showMore: 'Показать ещё (%d)', showingCount: 'Показано %d из %d моделей', deprecatesIn: 'Устареет через %d дн.', cacheHoursAgo: 'кеш %dч назад',
|
|
48
|
-
}
|
|
49
|
-
const API = '/dsh-model-sync'
|
|
50
|
-
const CAPABILITY_LABELS = { vision: { en: 'Vision', ru: 'Зрение' }, tools: { en: 'Tools', ru: 'Инструменты' }, reasoning: { en: 'Reasoning', ru: 'Рассуждение' }, embeddings: { en: 'Embeddings', ru: 'Эмбеддинги' } }
|
|
51
|
-
function capabilityKeys(models) { return [...new Set((models ?? []).flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort() }
|
|
52
|
-
function filterModelsByCapabilities(models, selected) { const required = [...new Set((selected ?? []).filter((key) => typeof key === 'string' && key))]; return (models ?? []).filter((model) => required.every((key) => model?.capabilities?.[key] === true)) }
|
|
53
|
-
const NS = 'dsh-model-sync'
|
|
54
|
-
function useLocale(ctx) {
|
|
55
|
-
return React.useSyncExternalStore(React.useMemo(() => (cb) => ctx.locale?.subscribe?.(cb) ?? (() => {}), [ctx]), React.useCallback(() => ctx.locale?.getSnapshot?.().active ?? 'en', [ctx]))
|
|
56
|
-
}
|
|
57
|
-
function ModelSyncSection({ ctx, includeIntro = true }) {
|
|
58
|
-
const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en
|
|
59
|
-
const [status, setStatus] = React.useState(null); const [provider, setProvider] = React.useState(''); const [dryRun, setDryRun] = React.useState(true); const [removeMissing, setRemoveMissing] = React.useState(false); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(''); const [credentialStatus, setCredentialStatus] = React.useState(null)
|
|
60
|
-
const [selectionProvider, setSelectionProvider] = React.useState(''); const [selectionDraft, setSelectionDraft] = React.useState([]); const [capabilityFilter, setCapabilityFilter] = React.useState([]); const [searchQuery, setSearchQuery] = React.useState(''); const [sortBy, setSortBy] = React.useState('name'); const [preview, setPreview] = React.useState(null); const [policyProvider, setPolicyProvider] = React.useState(''); const [policyDraft, setPolicyDraft] = React.useState({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' })
|
|
61
|
-
const [pageSize, setPageSize] = React.useState(50); const [quickFilter, setQuickFilter] = React.useState({ context100k: false, cheap: false })
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
})
|
|
133
|
-
React.createElement('
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
})
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@goodandready/dsh-model-sync',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} }
|
|
5
|
+
var exports = module.exports
|
|
6
|
+
const React = require('react')
|
|
7
|
+
|
|
8
|
+
const CSS = '.dms-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;margin:0;padding:0;overflow:hidden;box-sizing:border-box}' + '.dms-card-open{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-border-l2)}' + '.dms-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' + '.dms-card-header:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.05))}' + '.dms-card-header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}' + '.dms-card-head-text{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' + '.dms-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' + '.dms-card-description{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.45}' + '.dms-card-chevron{color:var(--dsw-alias-label-tertiary);flex:0 0 auto;transition:transform .12s}.dms-card-chevron-open{transform:rotate(180deg)}' + '.dms-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px;display:flex;flex-direction:column;gap:10px;box-sizing:border-box}' + '.dms-wrap{display:flex;flex-direction:column;gap:14px;max-width:760px;box-sizing:border-box;color:var(--dsw-alias-label-primary,inherit)}' +
|
|
9
|
+
'.dms-wrap>p{margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.55}' +
|
|
10
|
+
'.dms-toolbar,.dms-section{display:flex;flex-direction:row;align-items:center;flex-wrap:wrap;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary,rgba(255,255,255,.14)));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box}' +
|
|
11
|
+
'.dms-toolbar>select{flex:1 1 190px;min-width:190px}' +
|
|
12
|
+
'.dms-toolbar>.dms-actions{flex-basis:100%}' +
|
|
13
|
+
'.dms-toolbar>label{display:inline-flex;align-items:flex-start;gap:7px;font-size:12px;line-height:1.35;color:var(--dsw-alias-label-secondary)}' +
|
|
14
|
+
'.dms-toolbar>select,.dms-editor input,.dms-editor textarea{box-sizing:border-box;min-width:0;background:var(--dsw-specific-input-major,var(--dsw-alias-bg-layer-2,transparent));border:1px solid var(--dsw-alias-border-l1,var(--dsw-alias-border-l2));border-radius:6px;color:var(--dsw-alias-label-primary);font:inherit;padding:7px 9px}' +
|
|
15
|
+
'.dms-toolbar>label input{accent-color:var(--dsw-alias-brand-primary,var(--dsw-alias-button-info-fill))}' +
|
|
16
|
+
'.dms-actions{display:flex;align-items:center;flex-wrap:wrap;gap:7px}' +
|
|
17
|
+
'.dms-wrap>.dms-actions{padding:10px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary,rgba(255,255,255,.14)));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent)}' +
|
|
18
|
+
'.dms-btn{margin:0!important;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:1.35;padding:6px 10px;cursor:pointer;white-space:normal}' +
|
|
19
|
+
'.dms-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2,rgba(255,255,255,.05))}' +
|
|
20
|
+
'.dms-btn:disabled{opacity:.45;cursor:default}' +
|
|
21
|
+
'.dms-section>strong{display:block;font-size:13px;font-weight:650}' +
|
|
22
|
+
'.dms-provider-wrap{margin:0 0 7px}' +
|
|
23
|
+
'.dms-provider{display:grid!important;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:10px 11px;margin:0;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent)}' +
|
|
24
|
+
'.dms-provider>span{min-width:0;overflow-wrap:anywhere;line-height:1.4}' +
|
|
25
|
+
'.dms-section>div[style]{padding:8px 0;border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
26
|
+
'.dms-editor{display:flex;flex-direction:column;align-items:stretch;gap:10px;padding:13px 14px;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-primary));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box;border-color:var(--dsw-alias-brand-primary,var(--dsw-alias-border-l2))}' +
|
|
27
|
+
'.dms-editor strong{font-size:13px}' +
|
|
28
|
+
'.dms-editor label{display:block;gap:5px;margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px}' +
|
|
29
|
+
'.dms-editor label input,.dms-editor label textarea{width:100%}' +
|
|
30
|
+
'.dms-editor input[type="checkbox"]{width:auto;accent-color:var(--dsw-alias-brand-primary)}' +
|
|
31
|
+
'@media (max-width:540px){.dms-provider{grid-template-columns:1fr!important}.dms-provider .dms-btn{justify-self:start}.dms-toolbar>div:first-child{align-items:stretch;flex-direction:column}.dms-toolbar>div:first-child select{width:100%}.dms-toolbar,.dms-section,.dms-editor{padding:11px 12px}}'
|
|
32
|
+
|
|
33
|
+
function ensureStyles() {
|
|
34
|
+
if (typeof document === 'undefined' || !document.head) return
|
|
35
|
+
const cssId = 'dsh-model-sync/settings.module.css'
|
|
36
|
+
if (document.querySelector('style[data-plugin-css="' + cssId + '"]')) return
|
|
37
|
+
const tag = document.createElement('style')
|
|
38
|
+
tag.textContent = CSS
|
|
39
|
+
tag.setAttribute('data-plugin', 'dsh-model-sync')
|
|
40
|
+
tag.setAttribute('data-plugin-css', cssId)
|
|
41
|
+
document.head.appendChild(tag)
|
|
42
|
+
}
|
|
43
|
+
const en = {
|
|
44
|
+
title: 'Model synchronization', showSettings: 'Show settings', hideSettings: 'Hide settings', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', refreshAll: 'Refresh all', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run', previewTitle: 'Changes preview', previewNone: 'No changes detected.', previewAdded: 'Added', previewRemoved: 'Removed', previewChanged: 'Changed', previewApply: 'Apply preview', previewDismiss: 'Close preview', selectModels: 'Choose models', saveSelection: 'Save and apply selection', selectAtLeastOne: 'Select at least one model or use All models.', cancel: 'Cancel', selected: 'selected', allModels: 'All models', noModels: 'No models in the current catalog.', capabilityFilter: 'Filter by capabilities', capabilityCount: 'shown', noCapabilities: 'No capability metadata in this catalog.', pickerNote: 'The selection is applied to the DSH model picker immediately.', health: 'Check availability', healthOk: 'available', healthError: 'unavailable', healthDormant: 'not configured', policy: 'Manual model selection', savePolicy: 'Save policy', policyInclude: 'Include patterns (one per line)', policyExclude: 'Exclude patterns (one per line)', policyRequire: 'Required capabilities (comma-separated)', policyDeny: 'Denied capabilities (comma-separated)', policyNote: 'Patterns match model id, name, and tags. Apply changes to update the DSH picker.', scheduler: 'Scheduler', nextRun: 'next run', schedulerOff: 'off', schedulerOn: 'on', removeMissing: 'Confirm stale removal', stale: 'stale', deprecated: 'deprecated', history: 'Synchronization history', historyRun: 'Run', diffAdded: 'added', diffRemoved: 'removed', diffRenamed: 'renamed', diffChanged: 'metadata changed', rollback: 'Rollback catalog', rollbackConfirm: 'Rollback this provider catalog to the selected snapshot?', credentials: 'Credential diagnostics', checkCredentials: 'Check credentials', missingCredential: 'not configured', credentialReady: 'configured', report: 'Run report', success: 'success', partial: 'partial success', failure: 'failed', empty: 'no configured providers', notifications: 'Notifications', markRead: 'Mark read', acknowledge: 'Acknowledge', noNotifications: 'No notifications.', selectAllFiltered: 'Select all shown', deselectAllFiltered: 'Deselect all shown', invertFiltered: 'Invert selection', filterContext100k: 'Context ≥ 100k', filterCheap: 'Cheap ≤ $1/1M', showMore: 'Show more (%d)', showingCount: 'Showing %d of %d models', deprecatesIn: 'Deprecates in %dd', cacheHoursAgo: 'cache %dh ago', noHistory: 'No synchronization history.', searchPlaceholder: 'Search id/name/tag', sortName: 'Name A→Z', sortPrice: 'Price ↑', sortContext: 'Context ↓', sortDate: 'Newest ↓', schedulerSettings: 'Scheduler configuration', scheduleEnabled: 'Enable background schedule', intervalMinutes: 'Interval (minutes)', autoApplySchedule: 'Auto-apply changes', saveScheduler: 'Save scheduler', schedulerSaved: 'Saved', scopeUnavailable: 'Settings service unavailable.',
|
|
45
|
+
}
|
|
46
|
+
const ru = {
|
|
47
|
+
title: 'Синхронизация моделей', showSettings: 'Показать настройки', hideSettings: 'Скрыть настройки', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', refreshAll: 'Обновить все', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск', previewTitle: 'Предпросмотр изменений', previewNone: 'Изменений не обнаружено.', previewAdded: 'Добавлено', previewRemoved: 'Удалено', previewChanged: 'Изменено', previewApply: 'Применить изменения', previewDismiss: 'Закрыть предпросмотр', selectModels: 'Выбрать модели', saveSelection: 'Сохранить и применить выбор', selectAtLeastOne: 'Выберите хотя бы одну модель или нажмите «Все модели».', cancel: 'Отмена', selected: 'выбрано', allModels: 'Все модели', noModels: 'В текущем каталоге нет моделей.', capabilityFilter: 'Фильтр по возможностям', capabilityCount: 'показано', noCapabilities: 'В этом каталоге нет данных о возможностях.', pickerNote: 'Выбор сразу применяется к пикеру моделей DSH.', health: 'Проверить доступность', healthOk: 'доступен', healthError: 'недоступен', healthDormant: 'не настроен', policy: 'Ручной выбор моделей', savePolicy: 'Сохранить политику', policyInclude: 'Включать (по одному шаблону в строке)', policyExclude: 'Исключать (по одному шаблону в строке)', policyRequire: 'Обязательные возможности (через запятую)', policyDeny: 'Запрещённые возможности (через запятую)', policyNote: 'Шаблоны ищутся в id, названии и тегах модели. Нажмите «Применить», чтобы обновить пикер DSH.', scheduler: 'Планировщик', nextRun: 'следующий запуск', schedulerOff: 'выключен', schedulerOn: 'включён', removeMissing: 'Подтвердить удаление устаревших', stale: 'устарела', deprecated: 'deprecated', history: 'История синхронизаций', historyRun: 'Запуск', diffAdded: 'добавлено', diffRemoved: 'удалено', diffRenamed: 'переименовано', diffChanged: 'изменена метаинформация', rollback: 'Откатить каталог', rollbackConfirm: 'Откатить каталог этого провайдера к выбранному снимку?', credentials: 'Диагностика ключей', checkCredentials: 'Проверить ключи', missingCredential: 'не настроен', credentialReady: 'настроен', report: 'Итог запуска', success: 'успех', partial: 'частичный успех', failure: 'ошибка', empty: 'нет настроенных провайдеров', notifications: 'Уведомления', markRead: 'Прочитано', acknowledge: 'Подтвердить', noNotifications: 'Новых уведомлений нет.', selectAllFiltered: 'Выбрать все показанные', deselectAllFiltered: 'Снять выбор с показанных', invertFiltered: 'Инвертировать выбор', filterContext100k: 'Контекст ≥ 100k', filterCheap: 'Дешевые ≤ $1/1M', showMore: 'Показать ещё (%d)', showingCount: 'Показано %d из %d моделей', deprecatesIn: 'Устареет через %d дн.', cacheHoursAgo: 'кеш %dч назад', noHistory: 'Истории синхронизаций нет.', searchPlaceholder: 'Поиск id/name/tag', sortName: 'Имя A→Z', sortPrice: 'Цена ↑', sortContext: 'Контекст ↓', sortDate: 'Новые ↓', schedulerSettings: 'Настройка планировщика', scheduleEnabled: 'Включить фоновое расписание', intervalMinutes: 'Интервал (минуты)', autoApplySchedule: 'Автоматически применять изменения', saveScheduler: 'Сохранить расписание', schedulerSaved: 'Сохранено', scopeUnavailable: 'Служба настроек недоступна.',
|
|
48
|
+
}
|
|
49
|
+
const API = '/dsh-model-sync'
|
|
50
|
+
const CAPABILITY_LABELS = { vision: { en: 'Vision', ru: 'Зрение' }, tools: { en: 'Tools', ru: 'Инструменты' }, reasoning: { en: 'Reasoning', ru: 'Рассуждение' }, embeddings: { en: 'Embeddings', ru: 'Эмбеддинги' } }
|
|
51
|
+
function capabilityKeys(models) { return [...new Set((models ?? []).flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort() }
|
|
52
|
+
function filterModelsByCapabilities(models, selected) { const required = [...new Set((selected ?? []).filter((key) => typeof key === 'string' && key))]; return (models ?? []).filter((model) => required.every((key) => model?.capabilities?.[key] === true)) }
|
|
53
|
+
const NS = 'dsh-model-sync'
|
|
54
|
+
function useLocale(ctx) {
|
|
55
|
+
return React.useSyncExternalStore(React.useMemo(() => (cb) => ctx.locale?.subscribe?.(cb) ?? (() => {}), [ctx]), React.useCallback(() => ctx.locale?.getSnapshot?.().active ?? 'en', [ctx]))
|
|
56
|
+
}
|
|
57
|
+
function ModelSyncSection({ ctx, includeIntro = true }) {
|
|
58
|
+
const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en
|
|
59
|
+
const [status, setStatus] = React.useState(null); const [provider, setProvider] = React.useState(''); const [dryRun, setDryRun] = React.useState(true); const [removeMissing, setRemoveMissing] = React.useState(false); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(''); const [credentialStatus, setCredentialStatus] = React.useState(null)
|
|
60
|
+
const [selectionProvider, setSelectionProvider] = React.useState(''); const [selectionDraft, setSelectionDraft] = React.useState([]); const [capabilityFilter, setCapabilityFilter] = React.useState([]); const [searchQuery, setSearchQuery] = React.useState(''); const [sortBy, setSortBy] = React.useState('name'); const [preview, setPreview] = React.useState(null); const [policyProvider, setPolicyProvider] = React.useState(''); const [policyDraft, setPolicyDraft] = React.useState({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' })
|
|
61
|
+
const [pageSize, setPageSize] = React.useState(50); const [quickFilter, setQuickFilter] = React.useState({ context100k: false, cheap: false })
|
|
62
|
+
const scope = React.useMemo(() => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined), [ctx])
|
|
63
|
+
const snapshot = React.useSyncExternalStore(React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]), React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]), React.useCallback(() => ({ status: 'loading' }), []))
|
|
64
|
+
const snapStatus = snapshot?.status || 'loading'
|
|
65
|
+
const stored = snapshot?.value || {}
|
|
66
|
+
const [schedDraft, setSchedDraft] = React.useState(null)
|
|
67
|
+
const [schedMsg, setSchedMsg] = React.useState('')
|
|
68
|
+
const [schedErr, setSchedErr] = React.useState('')
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
if (snapStatus === 'ready' && schedDraft === null) {
|
|
71
|
+
setSchedDraft({ scheduleEnabled: stored.scheduleEnabled ?? false, intervalMinutes: stored.intervalMinutes ?? 60, autoApply: stored.autoApply ?? false })
|
|
72
|
+
}
|
|
73
|
+
}, [snapStatus, stored, schedDraft])
|
|
74
|
+
const saveScheduler = async () => {
|
|
75
|
+
if (!scope || !schedDraft) return
|
|
76
|
+
setBusy(true); setSchedErr(''); setSchedMsg('')
|
|
77
|
+
const errors = []
|
|
78
|
+
try { await scope.set('scheduleEnabled', !!schedDraft.scheduleEnabled) } catch (e) { errors.push('scheduleEnabled: ' + (e?.message || String(e))) }
|
|
79
|
+
try { await scope.set('intervalMinutes', Math.max(1, Number(schedDraft.intervalMinutes) || 60)) } catch (e) { errors.push('intervalMinutes: ' + (e?.message || String(e))) }
|
|
80
|
+
try { await scope.set('autoApply', !!schedDraft.autoApply) } catch (e) { errors.push('autoApply: ' + (e?.message || String(e))) }
|
|
81
|
+
setBusy(false)
|
|
82
|
+
if (errors.length) { setSchedErr(errors.join('; ')) } else { setSchedMsg(t.schedulerSaved); setTimeout(() => setSchedMsg(''), 4000); load(); }
|
|
83
|
+
}
|
|
84
|
+
const [tryState, setTryState] = React.useState({})
|
|
85
|
+
const doTry = (provider, modelId) => { setTryState((s)=>({ ...s, [provider+':'+modelId]: { loading: true } })); fetch(`${API}/try`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider, model: modelId }) }).then(async r=>{ const b=await r.json(); if(!r.ok) throw new Error(b?.error?.message||`HTTP ${r.status}`); return b }).then(b=>setTryState((s)=>({ ...s, [provider+':'+modelId]: { ok: b.ok, latency: b.latencyMs, status: b.status, error: b.error } }))).catch(e=>setTryState((s)=>({ ...s, [provider+':'+modelId]: { error: String(e.message||e) } }))) }
|
|
86
|
+
const load = React.useCallback(() => { setError(''); return fetch(`${API}/status`).then((r) => r.json()).then(setStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
|
|
87
|
+
const loadCredentials = React.useCallback(() => { setError(''); return fetch(`${API}/credentials`).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(setCredentialStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
|
|
88
|
+
React.useEffect(() => { ensureStyles(); load(); loadCredentials(); const timer = setInterval(load, 15000); return () => clearInterval(timer) }, [load, loadCredentials])
|
|
89
|
+
const execute = (targetProvider, dryRunOverride = dryRun) => { if (!dryRunOverride && removeMissing && !window.confirm(t.removeMissing)) return; setBusy(true); setError(''); fetch(`${API}/run`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider || undefined, dryRun: dryRunOverride, removeMissing }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setStatus(body); if (dryRunOverride) setPreview({ ...body, provider: targetProvider || '' }); else setPreview(null) }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
90
|
+
const checkHealth = (targetProvider) => { setBusy(true); setError(''); fetch(`${API}/health`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(targetProvider ? { provider: targetProvider } : {}) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(setStatus).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
91
|
+
const checkCredentials = (targetProvider) => { setBusy(true); setError(''); fetch(`${API}/credentials/check`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(targetProvider ? { provider: targetProvider } : {}) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setCredentialStatus(body.credentials); setStatus((current) => ({ ...(current ?? {}), lastHealth: body.health })) }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
92
|
+
const updateNotification = (id, action) => { setBusy(true); setError(''); fetch(API + '/notifications/' + action, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? ('HTTP ' + r.status)); return body }).then(() => load()).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
93
|
+
const openSelection = (row) => { const available = row.availableModels ?? row.models ?? []; const defaults = row.selectableModels ?? available; closePolicy(); setSelectionProvider(row.provider); setSelectionDraft(row.selectedModels?.length ? row.selectedModels : defaults.map((model) => model.id)); setCapabilityFilter([]); setSearchQuery(''); setSortBy('name'); setPageSize(50); setQuickFilter({ context100k: false, cheap: false }) }
|
|
94
|
+
const closeSelection = () => { setSelectionProvider(''); setSelectionDraft([]); setCapabilityFilter([]); setSearchQuery(''); setSortBy('name'); setPageSize(50); setQuickFilter({ context100k: false, cheap: false }) }
|
|
95
|
+
const openPolicy = (row) => { const policy = status?.policies?.[row.provider] ?? row.policy ?? {}; closeSelection(); setPolicyProvider(row.provider); setPolicyDraft({ include: (policy.include ?? []).join('\n'), exclude: (policy.exclude ?? []).join('\n'), requireCapabilities: Object.keys(policy.requireCapabilities ?? {}).join(', '), denyCapabilities: Object.keys(policy.denyCapabilities ?? {}).join(', ') }) }
|
|
96
|
+
const closePolicy = () => { setPolicyProvider(''); setPolicyDraft({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' }) }
|
|
97
|
+
const splitValues = (value) => [...new Set(String(value ?? '').split(/[,\n]+/).map((item) => item.trim()).filter(Boolean))]
|
|
98
|
+
const capabilityValues = (value) => Object.fromEntries(splitValues(value).map((key) => [key, true]))
|
|
99
|
+
const savePolicy = () => { setBusy(true); setError(''); fetch(`${API}/policy`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: policyProvider, include: splitValues(policyDraft.include), exclude: splitValues(policyDraft.exclude), requireCapabilities: capabilityValues(policyDraft.requireCapabilities), denyCapabilities: capabilityValues(policyDraft.denyCapabilities) }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(() => { closePolicy(); return load() }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
100
|
+
const saveSelection = () => { const row = rows.find((item) => item.provider === selectionProvider); const available = row?.availableModels ?? row?.models ?? []; if (available.length > 0 && selectionDraft.length === 0) { setError(t.selectAtLeastOne); return } const models = selectionDraft.length === available.length ? [] : selectionDraft; setBusy(true); setError(''); fetch(`${API}/selection`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: selectionProvider, models }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setStatus(body); closeSelection() }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
101
|
+
const rollback = (entry, targetProvider) => { if (!window.confirm(t.rollbackConfirm)) return; setBusy(true); setError(''); fetch(`${API}/history/rollback`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ historyId: entry.id, provider: targetProvider }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(() => load()).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
102
|
+
const isNewModel = (model, row) => { const added = status?.lastRun?.results?.find(r=>r.provider===row.provider)?.diff?.added||[]; const isAdded = added.some(m=>m.id===model.id); if(!isAdded) return false; const finishedAt = status?.lastRun?.finishedAt; if(!finishedAt) return false; return (Date.now()-new Date(finishedAt).getTime()) < 7*24*60*60*1000; };
|
|
103
|
+
const rows = (status?.providers ?? []).filter((row) => row.configured); const selectedRow = rows.find((row) => row.provider === selectionProvider); const choices = selectedRow?.availableModels ?? selectedRow?.models ?? []; const filteredChoicesBase = filterModelsByCapabilities(choices, capabilityFilter);
|
|
104
|
+
const tokens = searchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
105
|
+
const searchedChoices = tokens.length ? filteredChoicesBase.filter((m) => { const haystack = (m.id + ' ' + (m.name||'') + ' ' + (m.aliases||[]).join(' ') + ' ' + (m.tags||[]).join(' ') + ' ' + (m.description||'')).toLowerCase(); return tokens.every((tok) => haystack.includes(tok)); }) : filteredChoicesBase;
|
|
106
|
+
const quickFilteredChoices = searchedChoices.filter((m) => { if (quickFilter.context100k && (!m.contextWindow || m.contextWindow < 100000)) return false; if (quickFilter.cheap && (!m.pricing?.inputPerToken || (m.pricing.inputPerToken * 1000000) > 1.0)) return false; return true; });
|
|
107
|
+
const filteredChoices = [...quickFilteredChoices].sort((a,b) => { if (sortBy==='price') { const pa=a.pricing?.inputPerToken??Infinity; const pb=b.pricing?.inputPerToken??Infinity; return pa-pb; } if (sortBy==='context') return (b.contextWindow??0)-(a.contextWindow??0); if (sortBy==='date') return (b.firstSeenAt??0)-(a.firstSeenAt??0); return (a.name || a.id || '').localeCompare(b.name || b.id || ''); }); const maxContext = filteredChoices.reduce((max, m) => Math.max(max, m.contextWindow || 0), 0); const minPrice = filteredChoices.reduce((min, m) => m.pricing?.inputPerToken !== undefined ? Math.min(min, m.pricing.inputPerToken) : min, Infinity); const availableCapabilities = [...new Set(choices.flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort(); const visibleChoices = filteredChoices.slice(0, pageSize); const button = (label, onClick) => React.createElement('button', { type: 'button', disabled: busy, onClick, className: 'dms-btn' }, label)
|
|
108
|
+
const renderNotifications = () => {
|
|
109
|
+
const entries = status?.notifications ?? []
|
|
110
|
+
if (entries.length === 0) return React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noNotifications)
|
|
111
|
+
return entries.map((item) => React.createElement('div', { key: item.id, style: { marginTop: 6 } },
|
|
112
|
+
React.createElement('span', null, item.title + ': ' + item.message + ' · ' + (item.occurrences ?? 1) + '×'),
|
|
113
|
+
item.readAt ? null : button(t.markRead, () => updateNotification(item.id, 'read')),
|
|
114
|
+
item.acknowledgedAt ? null : button(t.acknowledge, () => updateNotification(item.id, 'acknowledge')),
|
|
115
|
+
React.createElement('small', { style: { display: 'block', marginLeft: 12 } }, (item.providers ?? []).map((row) => row.provider + ': ' + row.status + (row.message ? ' · ' + row.message : '')).join('; '))))
|
|
116
|
+
}
|
|
117
|
+
const renderScheduler = () => {
|
|
118
|
+
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%' } },
|
|
119
|
+
status?.scheduler ? React.createElement('small', { style: { display: 'block' } }, `${t.scheduler}: ${status.scheduler.active ? t.schedulerOn : t.schedulerOff}${status.scheduler.nextRunAt ? ` · ${t.nextRun}: ${new Date(status.scheduler.nextRunAt).toLocaleString()}` : ''}`) : null,
|
|
120
|
+
snapStatus === 'loading' ? React.createElement('small', null, t.loading) : null,
|
|
121
|
+
snapStatus === 'unavailable' ? React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.scopeUnavailable) : null,
|
|
122
|
+
schedDraft ? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
|
|
123
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12, cursor: 'pointer' } }, React.createElement('input', { type: 'checkbox', checked: !!schedDraft.scheduleEnabled, onChange: (e) => setSchedDraft((d) => ({ ...d, scheduleEnabled: e.target.checked })), disabled: busy }), t.scheduleEnabled),
|
|
124
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12 } }, React.createElement('span', { style: { minWidth: 140 } }, t.intervalMinutes + ':'), React.createElement('input', { type: 'number', min: 1, max: 1440, value: schedDraft.intervalMinutes, onChange: (e) => setSchedDraft((d) => ({ ...d, intervalMinutes: Math.max(1, Number(e.target.value) || 1) })), disabled: busy, style: { width: 90, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)', color: 'inherit' } })),
|
|
125
|
+
React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, fontSize: 12, cursor: 'pointer' } }, React.createElement('input', { type: 'checkbox', checked: !!schedDraft.autoApply, onChange: (e) => setSchedDraft((d) => ({ ...d, autoApply: e.target.checked })), disabled: busy }), t.autoApplySchedule),
|
|
126
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 } }, button(t.saveScheduler, saveScheduler), schedMsg ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-success-primary)' } }, schedMsg) : null, schedErr ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, schedErr) : null)
|
|
127
|
+
) : null
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
const renderHistory = () => {
|
|
131
|
+
const entries = status?.history ?? []
|
|
132
|
+
if (entries.length === 0) return React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noHistory)
|
|
133
|
+
return entries.map((entry) => React.createElement('details', { key: entry.id, style: { marginTop: 6 } },
|
|
134
|
+
React.createElement('summary', null, `${t.historyRun} #${entry.version} · ${new Date(entry.finishedAt).toLocaleString()}`),
|
|
135
|
+
(entry.providers ?? []).map((item) => {
|
|
136
|
+
const counts = item.counts ?? {}
|
|
137
|
+
const names = (items) => (items ?? []).map((model) => model?.after?.id ?? model?.id).filter(Boolean).join(', ')
|
|
138
|
+
const details = `+ ${names(item.diff?.added)}; - ${names(item.diff?.removed)}; ⇄ ${(item.diff?.renamed ?? []).map((pair) => `${pair.before?.id} → ${pair.after?.id}`).join(', ')}; ~ ${(item.diff?.changed ?? []).map((pair) => pair.after?.id).join(', ')}`
|
|
139
|
+
return React.createElement('div', { key: item.provider, style: { margin: '6px 0 6px 12px' } },
|
|
140
|
+
React.createElement('span', null, `${item.provider}: ${item.status} · +${counts.added ?? 0} ${t.diffAdded}, -${counts.removed ?? 0} ${t.diffRemoved}, ${counts.renamed ?? 0} ${t.diffRenamed}, ${counts.changed ?? 0} ${t.diffChanged}`),
|
|
141
|
+
item.hasRollback ? button(t.rollback, () => rollback(entry, item.provider)) : null,
|
|
142
|
+
React.createElement('small', { style: { display: 'block', marginTop: 4 } }, details))
|
|
143
|
+
})))
|
|
144
|
+
}
|
|
145
|
+
return React.createElement('div', { className: 'dms-wrap' },
|
|
146
|
+
includeIntro ? React.createElement('p', null, t.desc) : null,
|
|
147
|
+
React.createElement('div', { className: 'dms-toolbar' }, React.createElement('select', { value: provider, onChange: (e) => setProvider(e.target.value) }, React.createElement('option', { value: '' }, t.all), rows.map((row) => React.createElement('option', { key: row.provider, value: row.provider }, row.provider))), React.createElement('label', { style: { marginLeft: 12 } }, React.createElement('input', { type: 'checkbox', checked: dryRun, onChange: (e) => setDryRun(e.target.checked) }), ` ${t.dryRun}`), React.createElement('label', { style: { marginLeft: 12 } }, React.createElement('input', { type: 'checkbox', checked: removeMissing, onChange: (e) => setRemoveMissing(e.target.checked) }), ` ${t.removeMissing}`)),
|
|
148
|
+
React.createElement('div', { className: 'dms-actions' }, button(t.refresh, load), button(t.refreshAll, () => execute(undefined)), button(dryRun ? t.run : t.apply, () => execute(provider || undefined)), button(t.health, () => checkHealth(provider || undefined)), button(t.checkCredentials, () => checkCredentials(provider || undefined))),
|
|
149
|
+
status?.lastRun ? React.createElement('small', null, t.lastRun + ': ' + new Date(status.lastRun.finishedAt).toLocaleString()) : null, preview ? React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.previewTitle), (preview.results ?? []).filter((row) => row.status === 'ok').map((row) => React.createElement('div', { key: row.provider, style: { marginTop: 8 } }, React.createElement('strong', null, row.provider), React.createElement('div', null, t.previewAdded + ': ' + ((row.diff?.added ?? []).map((model) => model.name ?? model.id).join(', ') || '—')), React.createElement('div', null, t.previewRemoved + ': ' + ((row.diff?.removed ?? []).map((model) => model.name ?? model.id).join(', ') || '—')), React.createElement('div', null, t.previewChanged + ': ' + ((row.diff?.changed ?? []).map((change) => change.after?.name ?? change.after?.id).join(', ') || '—')))), (preview.results ?? []).every((row) => row.status !== 'ok' || !row.diff?.hasChanges) ? React.createElement('small', { style: { display: 'block', marginTop: 8 } }, t.previewNone) : null, React.createElement('div', { className: 'dms-actions', style: { marginTop: 8 } }, button(t.previewApply, () => execute(preview.provider || undefined, false)), button(t.previewDismiss, () => setPreview(null)))) : null, status?.lastReport ? React.createElement('div', { style: { marginTop: 8 } }, React.createElement('strong', null, t.report), React.createElement('small', { style: { display: 'block' } }, (t[status.lastReport.outcome] ?? status.lastReport.outcome) + ' · ' + (status.lastReport.counts?.succeeded ?? 0) + '/' + (status.lastReport.counts?.providers ?? 0) + ' providers · ' + (status.lastReport.counts?.models?.added ?? 0) + ' ' + t.diffAdded)) : null, status?.scheduler ? React.createElement('small', null, `${t.scheduler}: ${status.scheduler.active ? t.schedulerOn : t.schedulerOff}${status.scheduler.nextRunAt ? ` · ${t.nextRun}: ${new Date(status.scheduler.nextRunAt).toLocaleString()}` : ''}`) : null, status?.running || busy ? React.createElement('small', null, t.running) : null, error ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, error) : null, status === null ? React.createElement('small', null, t.loading) : null, rows.length === 0 && status ? React.createElement('small', null, t.noProviders) : null,
|
|
150
|
+
rows.map((row) => {
|
|
151
|
+
const health = status?.lastHealth?.results?.find((item) => item.provider === row.provider)
|
|
152
|
+
const healthText = health ? ` · ${health.status === 'ok' ? t.healthOk : health.status === 'dormant' ? t.healthDormant : t.healthError}` : ''
|
|
153
|
+
const lifecycleCounts = (row.lifecycleModels ?? []).reduce((counts, model) => { if (model.lifecycleStatus === 'stale') counts.stale += 1; if (model.lifecycleStatus === 'deprecated') counts.deprecated += 1; return counts }, { stale: 0, deprecated: 0 })
|
|
154
|
+
const lifecycleText = lifecycleCounts.stale || lifecycleCounts.deprecated ? ` · ${lifecycleCounts.stale} ${t.stale}, ${lifecycleCounts.deprecated} ${t.deprecated}` : ''
|
|
155
|
+
const modelCount = (row.availableModels ?? row.models ?? []).length
|
|
156
|
+
const latestRun = status?.lastRun?.results?.find((item) => item.provider === row.provider)
|
|
157
|
+
const addedCount = latestRun?.status === 'ok' ? (latestRun.diff?.added?.length ?? 0) : 0
|
|
158
|
+
const modelSummary = `${modelCount} models${addedCount > 0 ? ` +${addedCount}` : ''}`
|
|
159
|
+
const healthEntry = status?.lastHealth?.results?.find((item) => item.provider === row.provider)
|
|
160
|
+
const latencyText = healthEntry?.latencyMs ? ` · ${healthEntry.latencyMs}ms` : ''
|
|
161
|
+
const isOffline = latestRun?.status !== 'ok' && modelCount > 0
|
|
162
|
+
const offlineText = isOffline ? ' · ' + t.cacheHoursAgo.replace('%d', Math.max(0, Math.round((Date.now() - (status?.lastRun?.finishedAt ? new Date(status.lastRun.finishedAt).getTime() : Date.now()))/3600000))) : ''
|
|
163
|
+
const providerCard = React.createElement('div', { className: 'dms-provider' }, React.createElement('span', null, `${row.provider}: ${row.configured ? t.ok : t.dormant} · ${modelSummary}${lifecycleText}${healthText}${latencyText}${offlineText}`), button(t.selectModels, () => openSelection(row)), button(t.policy, () => openPolicy(row)))
|
|
164
|
+
const selectionEditor = selectionProvider === row.provider ? React.createElement('div', { className: 'dms-editor' }, React.createElement('strong', null, `${t.selectModels}: ${selectionProvider}`), React.createElement('div', { style: { display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' } }, React.createElement('input', { placeholder: t.searchPlaceholder, value: searchQuery, onChange: (e)=>setSearchQuery(e.target.value), style: { flex: '1 1 160px', padding: '6px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-2)' } }), React.createElement('select', { value: sortBy, onChange: (e)=>setSortBy(e.target.value), style: { padding: '6px 8px', borderRadius: 6 } }, React.createElement('option', {value:'name'}, t.sortName), React.createElement('option', {value:'price'}, t.sortPrice), React.createElement('option', {value:'context'}, t.sortContext), React.createElement('option', {value:'date'}, t.sortDate))), React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } }, React.createElement('button', { type: 'button', className: 'dms-btn', style: quickFilter.context100k ? { borderColor: 'var(--dsw-alias-brand-primary)', background: 'var(--dsw-alias-bg-layer-2)' } : {}, onClick: () => setQuickFilter((f) => ({ ...f, context100k: !f.context100k })) }, (quickFilter.context100k ? '✓ ' : '') + t.filterContext100k), React.createElement('button', { type: 'button', className: 'dms-btn', style: quickFilter.cheap ? { borderColor: 'var(--dsw-alias-brand-primary)', background: 'var(--dsw-alias-bg-layer-2)' } : {}, onClick: () => setQuickFilter((f) => ({ ...f, cheap: !f.cheap })) }, (quickFilter.cheap ? '✓ ' : '') + t.filterCheap)), React.createElement('div', { style: { margin: '8px 0' } }, choices.length === 0 ? React.createElement('small', null, t.noModels) : React.createElement(React.Fragment, null, availableCapabilities.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } }, React.createElement('strong', null, t.capabilityFilter), availableCapabilities.map((capability) => React.createElement('label', { key: capability, style: { display: 'inline-block', marginLeft: 8 } }, React.createElement('input', { type: 'checkbox', checked: capabilityFilter.includes(capability), onChange: (e) => setCapabilityFilter((current) => e.target.checked ? [...current, capability] : current.filter((item) => item !== capability)) }), ' ' + (CAPABILITY_LABELS[capability]?.[locale] ?? capability)))) : React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, t.noCapabilities), React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } }, button(t.selectAllFiltered, () => { const ids = new Set(selectionDraft); for (const m of filteredChoices) ids.add(m.id); setSelectionDraft([...ids]); }), button(t.deselectAllFiltered, () => { const filterSet = new Set(filteredChoices.map((m) => m.id)); setSelectionDraft(selectionDraft.filter((id) => !filterSet.has(id))); }), button(t.invertFiltered, () => { const current = new Set(selectionDraft); for (const m of filteredChoices) { if (current.has(m.id)) current.delete(m.id); else current.add(m.id); } setSelectionDraft([...current]); })), React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, filteredChoices.length + '/' + choices.length + ' ' + t.capabilityCount), visibleChoices.map((model) => { const deprecationDays = (() => { const d = model.lifecycle?.deprecationDate; if (!d) return null; const diff = new Date(d).getTime() - Date.now(); return diff > 0 ? Math.ceil(diff / (1000 * 60 * 60 * 24)) : 0; })(); return React.createElement('label', { key: model.id, style: { display: 'block', margin: '4px 0', padding: '4px 6px', borderRadius: 6, background: isNewModel(model,row) ? 'var(--dsw-alias-bg-layer-2)' : 'transparent' } }, React.createElement('input', { type: 'checkbox', checked: selectionDraft.includes(model.id), onChange: (e) => setSelectionDraft((current) => e.target.checked ? [...current, model.id] : current.filter((id) => id !== model.id)) }), ` ${model.name ?? model.id} (${model.id})`, isNewModel(model,row) ? React.createElement('span', { style: { marginLeft: 6, fontSize: 11, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-business-primary)', color: 'white' } }, 'NEW') : null, deprecationDays !== null ? React.createElement('span', { style: { marginLeft: 6, fontSize: 10, padding: '1px 5px', borderRadius: 4, background: 'var(--dsw-alias-state-warning-primary)', color: 'white' } }, deprecationDays > 0 ? t.deprecatesIn.replace('%d', deprecationDays) : t.deprecated) : null, (model.contextWindow && maxContext > 0 && model.contextWindow >= maxContext) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-warning-primary)', color: 'white' } }, '★') : null, (model.pricing && minPrice !== Infinity && model.pricing.inputPerToken === minPrice) ? React.createElement('span', { style: { marginLeft: 4, fontSize: 10, padding: '1px 4px', borderRadius: 3, background: 'var(--dsw-alias-state-success-primary)', color: 'white' } }, '$') : null, model.tags && model.tags.length ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)' } }, model.tags.join(', ')) : null, model.pricing ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, `in $${(model.pricing.inputPerToken*1000000).toFixed(2)}/1M` + (model.pricing.cacheReadPerToken!==undefined ? ` / cache $${(model.pricing.cacheReadPerToken*1000000).toFixed(2)}` : '') + (model.pricing.outputPerToken!==undefined ? ` / out $${(model.pricing.outputPerToken*1000000).toFixed(2)}` : '')) : null, model.description ? React.createElement('small', { title: model.description, style: { display: 'block', marginLeft: 22, color: 'var(--dsw-alias-label-secondary)', fontSize: 11 } }, model.description.slice(0,120)) : null, row.baseURL || row.api ? React.createElement('a', { href: (row.baseURL||'') + '/models/'+encodeURIComponent(model.id), target: '_blank', rel: 'noopener', style: { marginLeft: 8, fontSize: 11 } }, 'docs') : null, React.createElement('button', { type: 'button', onClick: (e)=>{e.preventDefault(); doTry(row.provider, model.id);}, style: { marginLeft: 8, fontSize: 11, padding: '1px 6px', borderRadius: 4 } }, (tryState[row.provider+':'+model.id] && tryState[row.provider+':'+model.id].loading) ? '…' : '▶'), tryState[row.provider+':'+model.id] && !tryState[row.provider+':'+model.id].loading ? React.createElement('small', { style: { marginLeft: 4, color: tryState[row.provider+':'+model.id].ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: 11 } }, tryState[row.provider+':'+model.id].ok ? `${tryState[row.provider+':'+model.id].latency}ms` : (tryState[row.provider+':'+model.id].error||tryState[row.provider+':'+model.id].status)) : null); }), filteredChoices.length > pageSize ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' } }, button(t.showMore.replace('%d', Math.min(50, filteredChoices.length - pageSize)), () => setPageSize((s) => s + 50)), React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.showingCount.replace('%d', visibleChoices.length).replace('%d', filteredChoices.length))) : null)), React.createElement('small', null, t.pickerNote), React.createElement('div', { style: { marginTop: 8 } }, button(t.allModels, () => setSelectionDraft(choices.map((model) => model.id))), button(t.saveSelection, saveSelection), button(t.cancel, closeSelection))) : null
|
|
165
|
+
const policyEditor = policyProvider === row.provider ? React.createElement('div', { className: 'dms-editor' }, React.createElement('strong', null, `${t.policy}: ${policyProvider}`), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyInclude, React.createElement('textarea', { rows: 3, value: policyDraft.include, onChange: (e) => setPolicyDraft((current) => ({ ...current, include: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyExclude, React.createElement('textarea', { rows: 3, value: policyDraft.exclude, onChange: (e) => setPolicyDraft((current) => ({ ...current, exclude: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyRequire, React.createElement('input', { value: policyDraft.requireCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, requireCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyDeny, React.createElement('input', { value: policyDraft.denyCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, denyCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })), React.createElement('small', null, t.policyNote), React.createElement('div', { style: { marginTop: 8 } }, button(t.savePolicy, savePolicy), button(t.cancel, closePolicy))) : null
|
|
166
|
+
return React.createElement('div', { key: row.provider, className: 'dms-provider-wrap' }, providerCard, selectionEditor, policyEditor)
|
|
167
|
+
}),
|
|
168
|
+
React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.notifications), renderNotifications()),
|
|
169
|
+
React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.credentials), credentialStatus === null ? React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.loading) : (credentialStatus.results ?? []).length === 0 ? React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noProviders) : (credentialStatus.results ?? []).map((item) => React.createElement('div', { key: item.provider, style: { marginTop: 6 } }, React.createElement('span', null, `${item.provider}: ${item.rotation?.configured ? `${item.rotation.keyCount} refs` : 'single ref'}${item.lastRequest ? ` · ${item.lastRequest.status}` : ''}`), (item.refs ?? []).map((ref) => React.createElement('small', { key: ref.ref || ref.label, style: { display: 'block', marginLeft: 12 } }, `${ref.order}. ${ref.label} · ${ref.configured === true ? t.credentialReady : t.missingCredential}${ref.source && ref.source !== 'unknown' ? ` · ${ref.source}` : ''}${ref.lastResolution?.status ? ` · ${ref.lastResolution.status}` : ''}`))))),
|
|
170
|
+
React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.schedulerSettings), renderScheduler()),
|
|
171
|
+
React.createElement('div', { className: 'dms-section' }, React.createElement('strong', null, t.history), renderHistory()),
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
function ModelSyncCard({ ctx }) {
|
|
175
|
+
const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en; const [open, setOpen] = React.useState(false)
|
|
176
|
+
const [cardStatus, setCardStatus] = React.useState(null)
|
|
177
|
+
React.useEffect(() => { ensureStyles(); const load = () => fetch(`${API}/status`).then(r=>r.json()).then(setCardStatus).catch(()=>{}); load(); const id=setInterval(load,15000); return ()=>clearInterval(id)}, [])
|
|
178
|
+
const cardBadge = (cardStatus?.lastRun?.results?.reduce((s,r)=>s+(r.diff?.added?.length||0),0) || 0)
|
|
179
|
+
return React.createElement('li', { className: 'dms-card' + (open ? ' dms-card-open' : '') },
|
|
180
|
+
React.createElement('button', { type: 'button', className: 'dms-card-header', 'aria-expanded': open, 'aria-label': `${t[open ? 'hideSettings' : 'showSettings']}: ${t.title}`, onClick: () => setOpen((value) => !value) },
|
|
181
|
+
React.createElement('span', { className: 'dms-card-head-text' }, React.createElement('span', { className: 'dms-card-name' }, t.title), React.createElement('span', { className: 'dms-card-description' }, t.desc)),
|
|
182
|
+
cardBadge ? React.createElement('span', { style: { background: 'var(--dsw-alias-state-business-primary)', color: 'white', padding: '2px 6px', borderRadius: 6, fontSize: 11, marginLeft: 8 } }, `+${cardBadge}`) : null,
|
|
183
|
+
React.createElement('svg', { width: 14, height: 14, className: 'dms-card-chevron' + (open ? ' dms-card-chevron-open' : ''), viewBox: '0 0 14 14', fill: 'none', 'aria-hidden': 'true' }, React.createElement('path', { d: 'M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5', fill: 'currentColor' }))),
|
|
184
|
+
open ? React.createElement('div', { className: 'dms-card-body' }, React.createElement(ModelSyncSection, { ctx, includeIntro: false })) : null)
|
|
185
|
+
}
|
|
186
|
+
function apply(ctx) {
|
|
187
|
+
ctx.effect(() => {
|
|
188
|
+
try { ctx.locale?.register?.(NS, { en, ru }) } catch {}
|
|
189
|
+
return () => {}
|
|
190
|
+
}, 'dsh-model-sync: dictionaries')
|
|
191
|
+
const registerPluginCard = () => {
|
|
192
|
+
try {
|
|
193
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, (props) => React.createElement(ModelSyncCard, { ...props, ctx })))
|
|
194
|
+
return true
|
|
195
|
+
} catch { return false }
|
|
196
|
+
}
|
|
197
|
+
if (!registerPluginCard()) {
|
|
198
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'dsh-model-sync', order: 25, label: () => 'Model Sync' }, (props) => React.createElement(ModelSyncSection, { ...props, ctx })))
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], filterModelsByCapabilities }; return module.exports
|
|
202
|
+
},
|
|
203
|
+
})
|