@goodandready/dsh-model-sync 0.3.13 → 0.4.0

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/lib/client.js CHANGED
@@ -5,18 +5,28 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports
6
6
  const React = require('react')
7
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)}' +
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}' +
9
+ '.dms-card-open{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-border-l2)}' +
10
+ '.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}' +
11
+ '.dms-card-header:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2))}' +
12
+ '.dms-card-header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}' +
13
+ '.dms-card-head-text{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' +
14
+ '.dms-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
15
+ '.dms-card-description{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.45}' +
16
+ '.dms-card-chevron{color:var(--dsw-alias-label-tertiary);flex:0 0 auto;transition:transform .12s}.dms-card-chevron-open{transform:rotate(180deg)}' +
17
+ '.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}' +
18
+ '.dms-wrap{display:flex;flex-direction:column;gap:14px;max-width:760px;box-sizing:border-box;color:var(--dsw-alias-label-primary,inherit)}' +
9
19
  '.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}' +
20
+ '.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));border-radius:10px;background:var(--dsw-alias-bg-layer-1,transparent);box-sizing:border-box}' +
11
21
  '.dms-toolbar>select{flex:1 1 190px;min-width:190px}' +
12
22
  '.dms-toolbar>.dms-actions{flex-basis:100%}' +
13
23
  '.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}' +
24
+ '.dms-toolbar>select,.dms-editor input,.dms-editor textarea,.dms-input{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
25
  '.dms-toolbar>label input{accent-color:var(--dsw-alias-brand-primary,var(--dsw-alias-button-info-fill))}' +
16
26
  '.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)}' +
27
+ '.dms-wrap>.dms-actions{padding:10px 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)}' +
18
28
  '.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))}' +
29
+ '.dms-btn:hover:not(:disabled){border-color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2)}' +
20
30
  '.dms-btn:disabled{opacity:.45;cursor:default}' +
21
31
  '.dms-section>strong{display:block;font-size:13px;font-weight:650}' +
22
32
  '.dms-provider-wrap{margin:0 0 7px}' +
@@ -28,7 +38,26 @@ window.__ModuleLoader__.load({
28
38
  '.dms-editor label{display:block;gap:5px;margin-top:8px;color:var(--dsw-alias-label-secondary);font-size:12px}' +
29
39
  '.dms-editor label input,.dms-editor label textarea{width:100%}' +
30
40
  '.dms-editor input[type="checkbox"]{width:auto;accent-color:var(--dsw-alias-brand-primary)}' +
31
- '.dms-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px;box-sizing:border-box}' + '.dms-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;line-height:1.4}' + '.dms-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}' + '.dms-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}' + '.dms-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}' + '.dms-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' + '.dms-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' + '.dms-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' + '.dms-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' + '.dms-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;box-sizing:border-box}' + '.dms-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' + '.dms-btn-primary{background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;border-color:transparent!important;font-weight:600}' + '.dms-btn-primary:hover:not(:disabled){opacity:0.88}' + '.dms-btn-danger{color:var(--dsw-alias-state-error-primary)!important;border-color:rgba(239,68,68,0.3)!important}' + '.dms-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important;border-color:rgba(239,68,68,0.5)!important}' + '.dms-alert{padding:10px 14px;border-radius:8px;font-size:13px}' + '.dms-alert-ok{background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary)}' + '.dms-alert-bad{background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary)}' + '.dms-probe-result{padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px}' + '@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-section-card,.dms-editor{padding:11px 12px}}'
41
+ '.dms-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px;box-sizing:border-box}' +
42
+ '.dms-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;line-height:1.4}' +
43
+ '.dms-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}' +
44
+ '.dms-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center}' +
45
+ '.dms-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}' +
46
+ '.dms-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
47
+ '.dms-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-secondary,var(--dsw-alias-bg-layer-2))}' +
48
+ '.dms-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-state-warning-secondary,var(--dsw-alias-bg-layer-2))}' +
49
+ '.dms-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2))}' +
50
+ '.dms-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
51
+ '.dms-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
52
+ '.dms-btn-primary{background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;border-color:transparent!important;font-weight:600}' +
53
+ '.dms-btn-primary:hover:not(:disabled){opacity:0.88}' +
54
+ '.dms-btn-danger{color:var(--dsw-alias-state-error-primary)!important;border-color:var(--dsw-alias-state-error-primary)!important}' +
55
+ '.dms-btn-danger:hover:not(:disabled){background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2))!important;border-color:var(--dsw-alias-state-error-primary)!important}' +
56
+ '.dms-alert{padding:10px 14px;border-radius:8px;font-size:13px}' +
57
+ '.dms-alert-ok{background:var(--dsw-alias-state-success-secondary,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-success-primary)}' +
58
+ '.dms-alert-bad{background:var(--dsw-alias-state-error-secondary,var(--dsw-alias-bg-layer-2));color:var(--dsw-alias-state-error-primary)}' +
59
+ '.dms-probe-result{padding:2px 6px;border-radius:4px;font-family:monospace;font-size:11px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px}' +
60
+ '@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-section-card,.dms-editor{padding:11px 12px}}'
32
61
 
33
62
  function createErrorBoundary() {
34
63
  if (!React || typeof React.Component !== 'function') {
@@ -80,66 +109,707 @@ window.__ModuleLoader__.load({
80
109
  tag.setAttribute('data-plugin-css', cssId)
81
110
  document.head.appendChild(tag)
82
111
  }
112
+
83
113
  const en = {
84
- 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.', invalidPattern: 'Invalid regex pattern: %s',
114
+ title: 'Model synchronization',
115
+ showSettings: 'Show settings', hideSettings: 'Hide settings',
116
+ desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.',
117
+ loading: 'Loading…',
118
+ refresh: 'Refresh status',
119
+ refreshAll: 'Refresh all',
120
+ run: 'Discover',
121
+ apply: 'Apply changes',
122
+ dryRun: 'Preview only (dry-run)',
123
+ all: 'All API-key providers',
124
+ ok: 'ready',
125
+ dormant: 'not configured',
126
+ running: 'running…',
127
+ noProviders: 'No API-key providers found.',
128
+ lastRun: 'Last run',
129
+ previewTitle: 'Changes preview',
130
+ previewNone: 'No changes detected.',
131
+ previewAdded: 'Added',
132
+ previewRemoved: 'Removed',
133
+ previewChanged: 'Changed',
134
+ previewApply: 'Apply preview',
135
+ previewDismiss: 'Close preview',
136
+ selectModels: 'Choose models',
137
+ saveSelection: 'Save and apply selection',
138
+ selectAtLeastOne: 'Select at least one model or use All models.',
139
+ cancel: 'Cancel',
140
+ selected: 'selected',
141
+ allModels: 'All models',
142
+ noModels: 'No models in the current catalog.',
143
+ capabilityFilter: 'Filter by capabilities',
144
+ capabilityCount: 'shown',
145
+ noCapabilities: 'No capability metadata in this catalog.',
146
+ pickerNote: 'The selection is applied to the DSH model picker immediately.',
147
+ health: 'Check availability',
148
+ healthOk: 'available',
149
+ healthError: 'unavailable',
150
+ healthDormant: 'not configured',
151
+ policy: 'Manual model selection',
152
+ savePolicy: 'Save policy',
153
+ policyInclude: 'Include patterns (one per line)',
154
+ policyExclude: 'Exclude patterns (one per line)',
155
+ policyRequire: 'Required capabilities (comma-separated)',
156
+ policyDeny: 'Denied capabilities (comma-separated)',
157
+ policyNote: 'Patterns match model id, name, and tags. Apply changes to update the DSH picker.',
158
+ scheduler: 'Scheduler',
159
+ nextRun: 'next run',
160
+ schedulerOff: 'off',
161
+ schedulerOn: 'on',
162
+ removeMissing: 'Confirm stale removal',
163
+ stale: 'stale',
164
+ deprecated: 'deprecated',
165
+ history: 'Synchronization history',
166
+ historyRun: 'Run',
167
+ diffAdded: 'added',
168
+ diffRemoved: 'removed',
169
+ diffRenamed: 'renamed',
170
+ diffChanged: 'metadata changed',
171
+ rollback: 'Rollback catalog',
172
+ rollbackConfirm: 'Rollback this provider catalog to the selected snapshot?',
173
+ credentials: 'Credential diagnostics',
174
+ checkCredentials: 'Check credentials',
175
+ missingCredential: 'not configured',
176
+ credentialReady: 'configured',
177
+ report: 'Run report',
178
+ success: 'success',
179
+ partial: 'partial success',
180
+ failure: 'failed',
181
+ empty: 'no configured providers',
182
+ notifications: 'Notifications',
183
+ markRead: 'Mark read',
184
+ acknowledge: 'Acknowledge',
185
+ noNotifications: 'No notifications.',
186
+ selectAllFiltered: 'Select all shown',
187
+ deselectAllFiltered: 'Deselect all shown',
188
+ invertFiltered: 'Invert selection',
189
+ filterContext100k: 'Context ≥ 100k',
190
+ filterCheap: 'Cheap ≤ $1/1M',
191
+ showMore: 'Show more (%d)',
192
+ showingCount: 'Showing %d of %d models',
193
+ deprecatesIn: 'Deprecates in %dd',
194
+ cacheHoursAgo: 'cache %dh ago',
195
+ noHistory: 'No synchronization history.',
196
+ searchPlaceholder: 'Search id/name/tag',
197
+ sortName: 'Name A→Z',
198
+ sortPrice: 'Price ↑',
199
+ sortContext: 'Context ↓',
200
+ sortDate: 'Newest ↓',
201
+ schedulerSettings: 'Scheduler configuration',
202
+ scheduleEnabled: 'Enable background schedule',
203
+ intervalMinutes: 'Interval (minutes)',
204
+ autoApplySchedule: 'Auto-apply changes',
205
+ saveScheduler: 'Save scheduler',
206
+ schedulerSaved: 'Saved',
207
+ scopeUnavailable: 'Settings service unavailable.',
208
+ invalidPattern: 'Invalid regex pattern: %s',
209
+ batchTry: 'Test Selected',
210
+ batchTryRunning: 'Testing...',
211
+ batchTryDeselectUnreachable: 'Deselect unreachable',
212
+ batchTryResults: 'Batch test: %d/%d reached',
213
+ filterCostContext: 'Cost & Context limits',
214
+ enableCostFilter: 'Filter by maximum price',
215
+ maxPricePerMillion: 'Max price ($ / 1M tokens)',
216
+ enableContextFilter: 'Filter by minimum context',
217
+ minContextTokens: 'Min context window (tokens)',
218
+ exportConfig: 'Export configuration',
219
+ importConfig: 'Import configuration',
220
+ exportImportTitle: 'Configuration Backup & Restore',
221
+ importSuccess: 'Configuration imported successfully',
222
+ importError: 'Import failed: %s',
223
+ importPlaceholder: 'Paste JSON configuration here…',
224
+ modelAliases: 'Model Aliases',
225
+ aliasPlaceholder: 'Alias (e.g. gpt-4o-latest)',
226
+ targetModel: 'Target model',
227
+ addAlias: 'Save alias',
228
+ removeAlias: 'Delete alias',
229
+ noAliases: 'No model aliases defined.',
230
+ updaterTitle: 'Plugin Updates',
231
+ checkForUpdates: 'Check for updates',
232
+ checkingUpdates: 'Checking…',
233
+ updateAvailable: 'Update available: ',
234
+ upToDate: 'Up to date',
235
+ updateNow: 'Update now',
236
+ updating: 'Updating…',
237
+ updateSuccess: 'Updated to ',
238
+ restartRequired: ' (restart required)',
239
+ updateFailed: 'Update failed: ',
240
+ checkUpdateFailed: 'Failed to check for updates',
241
+ targetProvider: 'Provider',
85
242
  }
86
- const ru = {
87
- 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: 'Служба настроек недоступна.', invalidPattern: 'Некорректный regex-шаблон: %s',
243
+
244
+ const zh = {
245
+ title: '模型同步',
246
+ showSettings: '展开设置', hideSettings: '收起设置',
247
+ desc: '为配置了 API 密钥的服务商刷新模型目录。默认仅预览变更(dry-run);需显式确认以应用更改。',
248
+ loading: '加载中…',
249
+ refresh: '刷新状态',
250
+ refreshAll: '全部刷新',
251
+ run: '发现模型',
252
+ apply: '应用变更',
253
+ dryRun: '仅预览(dry-run)',
254
+ all: '所有 API 密钥服务商',
255
+ ok: '就绪',
256
+ dormant: '未配置',
257
+ running: '运行中…',
258
+ noProviders: '未找到任何已配置的服务商。',
259
+ lastRun: '上次运行',
260
+ previewTitle: '变更预览',
261
+ previewNone: '未检测到任何变更。',
262
+ previewAdded: '新增',
263
+ previewRemoved: '已移除',
264
+ previewChanged: '已更改',
265
+ previewApply: '应用预览中的变更',
266
+ previewDismiss: '关闭预览',
267
+ selectModels: '选择模型',
268
+ saveSelection: '保存并应用选择',
269
+ selectAtLeastOne: '请至少选择一个模型,或使用“全部模型”。',
270
+ cancel: '取消',
271
+ selected: '已选',
272
+ allModels: '全部模型',
273
+ noModels: '当前目录中没有模型。',
274
+ capabilityFilter: '按能力筛选',
275
+ capabilityCount: '已显示',
276
+ noCapabilities: '此目录中没有能力元数据。',
277
+ pickerNote: '所选模型将立即生效至 DSH 模型选择器。',
278
+ health: '检查可用性',
279
+ healthOk: '可用',
280
+ healthError: '不可用',
281
+ healthDormant: '未配置',
282
+ policy: '手动模型选择',
283
+ savePolicy: '保存策略',
284
+ policyInclude: '包含规则(每行一个正则表达式)',
285
+ policyExclude: '排除规则(每行一个正则表达式)',
286
+ policyRequire: '必需能力(逗号分隔)',
287
+ policyDeny: '禁止能力(逗号分隔)',
288
+ policyNote: '规则将匹配模型 ID、名称和标签。点击应用变更后更新 DSH 选择器。',
289
+ scheduler: '定时同步',
290
+ nextRun: '下次运行',
291
+ schedulerOff: '已关闭',
292
+ schedulerOn: '已开启',
293
+ removeMissing: '确认移除失效模型',
294
+ stale: '过期',
295
+ deprecated: '已弃用',
296
+ history: '同步历史',
297
+ historyRun: '运行记录',
298
+ diffAdded: '新增',
299
+ diffRemoved: '移除',
300
+ diffRenamed: '重命名',
301
+ diffChanged: '元数据变更',
302
+ rollback: '回滚目录',
303
+ rollbackConfirm: '确定要将该服务商目录回滚至所选快照吗?',
304
+ credentials: '凭据诊断',
305
+ checkCredentials: '检查凭据',
306
+ missingCredential: '未配置',
307
+ credentialReady: '已配置',
308
+ report: '运行报告',
309
+ success: '成功',
310
+ partial: '部分成功',
311
+ failure: '失败',
312
+ empty: '无配置的服务商',
313
+ notifications: '系统通知',
314
+ markRead: '标记已读',
315
+ acknowledge: '确认知晓',
316
+ noNotifications: '暂无通知。',
317
+ selectAllFiltered: '全选已筛选',
318
+ deselectAllFiltered: '取消全选已筛选',
319
+ invertFiltered: '反向选择',
320
+ filterContext100k: '上下文 ≥ 100k',
321
+ filterCheap: '经济型 ≤ $1/1M',
322
+ showMore: '加载更多 (%d)',
323
+ showingCount: '正在显示 %d / %d 个模型',
324
+ deprecatesIn: '将在 %d 天后弃用',
325
+ cacheHoursAgo: '缓存于 %d 小时前',
326
+ noHistory: '暂无同步历史。',
327
+ searchPlaceholder: '搜索 ID / 名称 / 标签',
328
+ sortName: '名称 A→Z',
329
+ sortPrice: '价格 ↑',
330
+ sortContext: '上下文 ↓',
331
+ sortDate: '最新 ↓',
332
+ schedulerSettings: '定时计划设置',
333
+ scheduleEnabled: '开启后台定时同步',
334
+ intervalMinutes: '间隔时间(分钟)',
335
+ autoApplySchedule: '自动应用变更',
336
+ saveScheduler: '保存计划',
337
+ schedulerSaved: '已保存',
338
+ scopeUnavailable: '设置服务不可用。',
339
+ invalidPattern: '正则表达式无效:%s',
340
+ batchTry: '批量测试选中模型',
341
+ batchTryRunning: '测试中...',
342
+ batchTryDeselectUnreachable: '取消选中不可用模型',
343
+ batchTryResults: '批量测试:%d/%d 成功',
344
+ filterCostContext: '价格与上下文限制',
345
+ enableCostFilter: '启用最高价格过滤',
346
+ maxPricePerMillion: '最高价格($/百万Token)',
347
+ enableContextFilter: '启用最小上下文过滤',
348
+ minContextTokens: '最小上下文窗口(Token)',
349
+ exportConfig: '导出配置',
350
+ importConfig: '导入配置',
351
+ exportImportTitle: '配置备份与恢复',
352
+ importSuccess: '配置导入成功',
353
+ importError: '导入失败:%s',
354
+ importPlaceholder: '在此粘贴 JSON 配置内容…',
355
+ modelAliases: '模型别名映射',
356
+ aliasPlaceholder: '别名(例如 gpt-4o-latest)',
357
+ targetModel: '目标模型',
358
+ addAlias: '保存别名',
359
+ removeAlias: '删除别名',
360
+ noAliases: '未定义任何模型别名。',
361
+ updaterTitle: '插件更新',
362
+ checkForUpdates: '检查更新',
363
+ checkingUpdates: '正在检查…',
364
+ updateAvailable: '发现新版本: ',
365
+ upToDate: '已是最新版本',
366
+ updateNow: '立即更新',
367
+ updating: '正在更新…',
368
+ updateSuccess: '已成功更新至 ',
369
+ restartRequired: ' (需重启 DSH)',
370
+ updateFailed: '更新失败: ',
371
+ checkUpdateFailed: '检查更新失败',
372
+ targetProvider: '服务商',
88
373
  }
374
+
89
375
  const API = '/dsh-model-sync'
90
- const CAPABILITY_LABELS = { vision: { en: 'Vision', ru: 'Зрение' }, tools: { en: 'Tools', ru: 'Инструменты' }, reasoning: { en: 'Reasoning', ru: 'Рассуждение' }, embeddings: { en: 'Embeddings', ru: 'Эмбеддинги' } }
91
- 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)) }
376
+ const CAPABILITY_LABELS = {
377
+ vision: { en: 'Vision', zh: '视觉' },
378
+ tools: { en: 'Tools', zh: '工具调用' },
379
+ reasoning: { en: 'Reasoning', zh: '深度思考' },
380
+ embeddings: { en: 'Embeddings', zh: '嵌入向量' },
381
+ }
382
+
383
+ function filterModelsByCapabilities(models, selected) {
384
+ const required = [...new Set((selected ?? []).filter((key) => typeof key === 'string' && key))]
385
+ return (models ?? []).filter((model) => required.every((key) => model?.capabilities?.[key] === true))
386
+ }
387
+
92
388
  const NS = 'dsh-model-sync'
389
+
93
390
  function useLocale(ctx) {
94
- return React.useSyncExternalStore(React.useMemo(() => (cb) => ctx.locale?.subscribe?.(cb) ?? (() => {}), [ctx]), React.useCallback(() => ctx.locale?.getSnapshot?.().active ?? 'en', [ctx]))
391
+ return React.useSyncExternalStore(
392
+ React.useMemo(() => (cb) => ctx.locale?.subscribe?.(cb) ?? (() => {}), [ctx]),
393
+ React.useCallback(() => ctx.locale?.getSnapshot?.().active ?? 'en', [ctx])
394
+ )
95
395
  }
396
+
397
+
398
+ function UpdaterSection({ t, api }) {
399
+ const [updaterStatus, setUpdaterStatus] = React.useState(null)
400
+ const [updaterChecking, setUpdaterChecking] = React.useState(false)
401
+ const [updaterUpdating, setUpdaterUpdating] = React.useState(false)
402
+ const [updaterNotice, setUpdaterNotice] = React.useState('')
403
+ const [updaterError, setUpdaterError] = React.useState('')
404
+
405
+ const checkUpdates = () => {
406
+ setUpdaterChecking(true)
407
+ setUpdaterError('')
408
+ setUpdaterNotice('')
409
+ fetch(`${api}/update`)
410
+ .then((res) => {
411
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
412
+ return res.json()
413
+ })
414
+ .then((data) => {
415
+ setUpdaterStatus(data)
416
+ setUpdaterChecking(false)
417
+ })
418
+ .catch(() => {
419
+ setUpdaterChecking(false)
420
+ setUpdaterError(t.checkUpdateFailed)
421
+ })
422
+ }
423
+
424
+ const runUpdate = () => {
425
+ if (updaterUpdating) return
426
+ setUpdaterUpdating(true)
427
+ setUpdaterError('')
428
+ setUpdaterNotice('')
429
+ fetch(`${api}/update`, {
430
+ method: 'POST',
431
+ headers: { 'x-dsh-plugin-update': '1', 'content-type': 'application/json' },
432
+ })
433
+ .then((res) => res.json())
434
+ .then((resData) => {
435
+ if (resData.error) throw new Error(resData.error.message || resData.error)
436
+ const cur = resData.updatedVersion || (updaterStatus && updaterStatus.latestVersion) || ''
437
+ const nextData = Object.assign({}, updaterStatus, { currentVersion: cur, updateAvailable: false })
438
+ setUpdaterUpdating(false)
439
+ setUpdaterStatus(nextData)
440
+ setUpdaterNotice(t.updateSuccess + cur + (resData.restartRequired ? t.restartRequired : ''))
441
+ })
442
+ .catch((err) => {
443
+ setUpdaterUpdating(false)
444
+ setUpdaterError(t.updateFailed + (err.message || String(err)))
445
+ })
446
+ }
447
+
448
+ return React.createElement('div', { style: { marginTop: 8 } },
449
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
450
+ React.createElement('div', { style: { fontSize: 13, color: 'var(--dsw-alias-label-primary)' } },
451
+ updaterStatus ? `v${updaterStatus.currentVersion}` : '',
452
+ updaterStatus && updaterStatus.updateAvailable ? ` → v${updaterStatus.latestVersion}` : ''
453
+ ),
454
+ React.createElement('div', null,
455
+ updaterStatus && updaterStatus.updateAvailable
456
+ ? React.createElement('button', {
457
+ type: 'button',
458
+ className: 'dms-btn dms-btn-primary',
459
+ disabled: updaterUpdating,
460
+ onClick: runUpdate,
461
+ }, updaterUpdating ? t.updating : t.updateNow)
462
+ : React.createElement('button', {
463
+ type: 'button',
464
+ className: 'dms-btn',
465
+ disabled: updaterChecking,
466
+ onClick: checkUpdates,
467
+ }, updaterChecking ? t.checkingUpdates : (updaterStatus ? t.upToDate : t.checkForUpdates))
468
+ )
469
+ ),
470
+ updaterNotice ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-success)', marginTop: 8, fontSize: 12 } }, updaterNotice) : null,
471
+ updaterError ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary)', marginTop: 8, fontSize: 12 } }, updaterError) : null
472
+ )
473
+ }
474
+
96
475
  function ModelSyncSection({ ctx, includeIntro = true }) {
97
- const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en
98
- 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)
99
- const [selectionProvider, setSelectionProvider] = React.useState(''); const [selectionDraft, setSelectionDraft] = React.useState([]); const [capabilityFilter, setCapabilityFilter] = React.useState([]); const [searchQuery, setSearchQuery] = React.useState(''); const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState(''); const [sortBy, setSortBy] = React.useState('name');
476
+ const locale = useLocale(ctx)
477
+ const t = locale === 'zh' ? zh : en
478
+ const [status, setStatus] = React.useState(null)
479
+ const [provider, setProvider] = React.useState('')
480
+ const [dryRun, setDryRun] = React.useState(true)
481
+ const [removeMissing, setRemoveMissing] = React.useState(false)
482
+ const [busy, setBusy] = React.useState(false)
483
+ const [error, setError] = React.useState('')
484
+ const [credentialStatus, setCredentialStatus] = React.useState(null)
485
+ const [selectionProvider, setSelectionProvider] = React.useState('')
486
+ const [selectionDraft, setSelectionDraft] = React.useState([])
487
+ const [capabilityFilter, setCapabilityFilter] = React.useState([])
488
+ const [searchQuery, setSearchQuery] = React.useState('')
489
+ const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState('')
490
+ const [sortBy, setSortBy] = React.useState('name')
491
+
100
492
  React.useEffect(() => {
101
- const timer = setTimeout(() => { setDebouncedSearchQuery(searchQuery) }, 150);
102
- return () => clearTimeout(timer);
103
- }, [searchQuery]); const [preview, setPreview] = React.useState(null); const [policyProvider, setPolicyProvider] = React.useState(''); const [policyDraft, setPolicyDraft] = React.useState({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' })
104
- const [pageSize, setPageSize] = React.useState(50); const [quickFilter, setQuickFilter] = React.useState({ context100k: false, cheap: false })
493
+ const timer = setTimeout(() => { setDebouncedSearchQuery(searchQuery) }, 150)
494
+ return () => clearTimeout(timer)
495
+ }, [searchQuery])
496
+
497
+ const [preview, setPreview] = React.useState(null)
498
+ const [policyProvider, setPolicyProvider] = React.useState('')
499
+ const [policyDraft, setPolicyDraft] = React.useState({
500
+ include: '',
501
+ exclude: '',
502
+ requireCapabilities: '',
503
+ denyCapabilities: '',
504
+ enableCostFilter: false,
505
+ maxPricePerMillion: '',
506
+ enableContextFilter: false,
507
+ minContextTokens: '',
508
+ })
509
+ const [pageSize, setPageSize] = React.useState(50)
510
+ const [quickFilter, setQuickFilter] = React.useState({ context100k: false, cheap: false })
511
+ const [tryState, setTryState] = React.useState({})
512
+ const [batchTrying, setBatchTrying] = React.useState(false)
513
+ const [batchSummary, setBatchSummary] = React.useState(null)
514
+
515
+ const [aliasDraft, setAliasDraft] = React.useState({ alias: '', provider: '', model: '' })
516
+ const [aliasMsg, setAliasMsg] = React.useState('')
517
+ const [aliasErr, setAliasErr] = React.useState('')
518
+
519
+ const [importText, setImportText] = React.useState('')
520
+ const [importMsg, setImportMsg] = React.useState('')
521
+ const [importErr, setImportErr] = React.useState('')
522
+ const [showImportBox, setShowImportBox] = React.useState(false)
523
+
105
524
  const scope = React.useMemo(() => (ctx && ctx.settingsScope ? ctx.settingsScope.bind({ namespace: NS }) : undefined), [ctx])
106
- const snapshot = React.useSyncExternalStore(React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]), React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]), React.useCallback(() => ({ status: 'loading' }), []))
525
+ const snapshot = React.useSyncExternalStore(
526
+ React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
527
+ React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'loading' }), [scope]),
528
+ React.useCallback(() => ({ status: 'loading' }), [])
529
+ )
107
530
  const snapStatus = snapshot?.status || 'loading'
108
531
  const stored = snapshot?.value || {}
109
532
  const [schedDraft, setSchedDraft] = React.useState(null)
110
533
  const [schedMsg, setSchedMsg] = React.useState('')
111
534
  const [schedErr, setSchedErr] = React.useState('')
535
+
112
536
  React.useEffect(() => {
113
537
  if (snapStatus === 'ready' && schedDraft === null) {
114
538
  setSchedDraft({ scheduleEnabled: stored.scheduleEnabled ?? false, intervalMinutes: stored.intervalMinutes ?? 60, autoApply: stored.autoApply ?? false })
115
539
  }
116
540
  }, [snapStatus, stored, schedDraft])
541
+
117
542
  const saveScheduler = async () => {
118
543
  if (!scope || !schedDraft) return
119
544
  setBusy(true); setSchedErr(''); setSchedMsg('')
120
- const errors = []
121
- try { await scope.set('scheduleEnabled', !!schedDraft.scheduleEnabled) } catch (e) { errors.push('scheduleEnabled: ' + (e?.message || String(e))) }
122
- try { await scope.set('intervalMinutes', Math.max(1, Number(schedDraft.intervalMinutes) || 60)) } catch (e) { errors.push('intervalMinutes: ' + (e?.message || String(e))) }
123
- try { await scope.set('autoApply', !!schedDraft.autoApply) } catch (e) { errors.push('autoApply: ' + (e?.message || String(e))) }
124
- setBusy(false)
125
- if (errors.length) { setSchedErr(errors.join('; ')) } else { setSchedMsg(t.schedulerSaved); setTimeout(() => setSchedMsg(''), 4000); load(); }
545
+ try {
546
+ await scope.set('scheduleEnabled', !!schedDraft.scheduleEnabled)
547
+ await scope.set('intervalMinutes', Number(schedDraft.intervalMinutes) || 60)
548
+ await scope.set('autoApply', !!schedDraft.autoApply)
549
+ setSchedMsg(t.schedulerSaved)
550
+ load()
551
+ } catch (e) {
552
+ setSchedErr(String(e?.message ?? e))
553
+ } finally {
554
+ setBusy(false)
555
+ }
126
556
  }
127
- const [tryState, setTryState] = React.useState({})
128
- 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) } }))) }
129
- const load = React.useCallback(() => { setError(''); return fetch(`${API}/status`).then((r) => r.json()).then(setStatus).catch((e) => setError(String(e?.message ?? e))) }, [])
130
- 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))) }, [])
131
- React.useEffect(() => { ensureStyles(); load(); loadCredentials(); const timer = setInterval(() => { if (typeof document !== 'undefined' && document.hidden) return; load() }, 15000); return () => clearInterval(timer) }, [load, loadCredentials])
132
- 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)) }
133
- 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)) }
134
- 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)) }
135
- 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)) }
136
- 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 }) }
137
- const closeSelection = () => { setSelectionProvider(''); setSelectionDraft([]); setCapabilityFilter([]); setSearchQuery(''); setSortBy('name'); setPageSize(50); setQuickFilter({ context100k: false, cheap: false }) }
138
- 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(', ') }) }
139
- const closePolicy = () => { setPolicyProvider(''); setPolicyDraft({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' }) }
557
+
558
+ const doTry = (targetProvider, targetModel) => {
559
+ const key = `${targetProvider}:${targetModel}`
560
+ setTryState((current) => ({ ...current, [key]: { loading: true } }))
561
+ fetch(`${API}/try`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider, model: targetModel }) })
562
+ .then(async (r) => { const b = await r.json(); return { ok: r.ok && b.ok !== false, latency: b.latencyMs, status: b.status, error: b.error } })
563
+ .then((res) => { setTryState((current) => ({ ...current, [key]: { loading: false, ok: res.ok, latency: res.latency, status: res.status, error: res.error } })) })
564
+ .catch((err) => { setTryState((current) => ({ ...current, [key]: { loading: false, ok: false, error: String(err?.message ?? err) } })) })
565
+ }
566
+
567
+ const runBatchTry = () => {
568
+ if (!selectionProvider || !selectionDraft.length) return
569
+ setBatchTrying(true)
570
+ setError('')
571
+ fetch(`${API}/batch-try`, {
572
+ method: 'POST',
573
+ headers: { 'content-type': 'application/json' },
574
+ body: JSON.stringify({ provider: selectionProvider, models: selectionDraft }),
575
+ })
576
+ .then(async (r) => {
577
+ const data = await r.json()
578
+ if (!r.ok || data.error) throw new Error(data?.error?.message ?? `HTTP ${r.status}`)
579
+ return data
580
+ })
581
+ .then((data) => {
582
+ const next = { ...tryState }
583
+ for (const res of data.results ?? []) {
584
+ next[`${selectionProvider}:${res.model}`] = {
585
+ ok: res.ok,
586
+ latency: res.latencyMs,
587
+ status: res.status,
588
+ error: res.error,
589
+ loading: false,
590
+ }
591
+ }
592
+ setTryState(next)
593
+ setBatchSummary(data.summary)
594
+ })
595
+ .catch((e) => setError(String(e?.message ?? e)))
596
+ .finally(() => setBatchTrying(false))
597
+ }
598
+
599
+ const deselectUnreachable = () => {
600
+ if (!selectionProvider) return
601
+ setSelectionDraft((current) => current.filter((m) => {
602
+ const probe = tryState[`${selectionProvider}:${m}`]
603
+ return !probe || probe.ok !== false
604
+ }))
605
+ }
606
+
607
+ const handleExport = () => {
608
+ fetch(`${API}/export`)
609
+ .then((r) => r.json())
610
+ .then((data) => {
611
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
612
+ const url = URL.createObjectURL(blob)
613
+ const a = document.createElement('a')
614
+ a.href = url
615
+ a.download = `dsh-model-sync-config-${new Date().toISOString().slice(0, 10)}.json`
616
+ a.click()
617
+ URL.revokeObjectURL(url)
618
+ })
619
+ .catch((e) => setError(String(e?.message ?? e)))
620
+ }
621
+
622
+ const handleImport = () => {
623
+ if (!importText.trim()) return
624
+ setBusy(true); setImportErr(''); setImportMsg('')
625
+ try {
626
+ const parsed = JSON.parse(importText)
627
+ fetch(`${API}/import`, {
628
+ method: 'POST',
629
+ headers: { 'content-type': 'application/json' },
630
+ body: JSON.stringify(parsed),
631
+ })
632
+ .then(async (r) => {
633
+ const res = await r.json()
634
+ if (!r.ok) throw new Error(res.error?.message ?? `HTTP ${r.status}`)
635
+ return res
636
+ })
637
+ .then(() => {
638
+ setImportMsg(t.importSuccess)
639
+ setImportText('')
640
+ setShowImportBox(false)
641
+ load()
642
+ })
643
+ .catch((e) => setImportErr(t.importError.replace('%s', String(e?.message ?? e))))
644
+ .finally(() => setBusy(false))
645
+ } catch {
646
+ setImportErr(t.importError.replace('%s', 'Invalid JSON syntax'))
647
+ setBusy(false)
648
+ }
649
+ }
650
+
651
+ const saveAlias = () => {
652
+ if (!aliasDraft.alias.trim() || !aliasDraft.provider || !aliasDraft.model.trim()) return
653
+ setBusy(true); setAliasErr(''); setAliasMsg('')
654
+ fetch(`${API}/aliases`, {
655
+ method: 'POST',
656
+ headers: { 'content-type': 'application/json' },
657
+ body: JSON.stringify({ alias: aliasDraft.alias.trim(), provider: aliasDraft.provider, model: aliasDraft.model.trim() }),
658
+ })
659
+ .then(async (r) => {
660
+ const res = await r.json()
661
+ if (!r.ok) throw new Error(res.error?.message ?? `HTTP ${r.status}`)
662
+ return res
663
+ })
664
+ .then(() => {
665
+ setAliasDraft({ alias: '', provider: '', model: '' })
666
+ setAliasMsg(t.schedulerSaved)
667
+ load()
668
+ })
669
+ .catch((e) => setAliasErr(String(e?.message ?? e)))
670
+ .finally(() => setBusy(false))
671
+ }
672
+
673
+ const deleteAlias = (aliasName) => {
674
+ setBusy(true)
675
+ fetch(`${API}/aliases`, {
676
+ method: 'POST',
677
+ headers: { 'content-type': 'application/json' },
678
+ body: JSON.stringify({ alias: aliasName, action: 'delete' }),
679
+ })
680
+ .then(async (r) => {
681
+ const res = await r.json()
682
+ if (!r.ok) throw new Error(res.error?.message ?? `HTTP ${r.status}`)
683
+ return res
684
+ })
685
+ .then(() => load())
686
+ .catch((e) => setError(String(e?.message ?? e)))
687
+ .finally(() => setBusy(false))
688
+ }
689
+
690
+ const load = () => {
691
+ if (typeof document !== 'undefined' && document.hidden) return
692
+ setError('')
693
+ fetch(`${API}/status`)
694
+ .then((r) => r.json())
695
+ .then(setStatus)
696
+ .catch((e) => setError(String(e?.message ?? e)))
697
+ loadCredentials()
698
+ }
699
+
700
+ const loadCredentials = () => {
701
+ fetch(`${API}/credentials`)
702
+ .then((r) => r.json())
703
+ .then(setCredentialStatus)
704
+ .catch(() => {})
705
+ }
706
+
707
+ React.useEffect(() => { ensureStyles(); load() }, [])
708
+
709
+ const execute = (targetProvider) => {
710
+ setBusy(true); setError(''); setPreview(null)
711
+ fetch(`${API}/run`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider, dryRun, removeMissing }) })
712
+ .then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body })
713
+ .then((body) => {
714
+ setStatus(body)
715
+ if (dryRun && body.results) setPreview(body.results)
716
+ else setPreview(null)
717
+ })
718
+ .catch((e) => setError(String(e?.message ?? e)))
719
+ .finally(() => setBusy(false))
720
+ }
721
+
722
+ const checkHealth = (targetProvider) => {
723
+ setBusy(true); setError('')
724
+ fetch(`${API}/health`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider }) })
725
+ .then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body })
726
+ .then(setStatus)
727
+ .catch((e) => setError(String(e?.message ?? e)))
728
+ .finally(() => setBusy(false))
729
+ }
730
+
731
+ const checkCredentials = (targetProvider) => {
732
+ setBusy(true); setError('')
733
+ fetch(`${API}/credentials/check`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: targetProvider }) })
734
+ .then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body })
735
+ .then((body) => { if (body.credentials) setCredentialStatus(body.credentials); load() })
736
+ .catch((e) => setError(String(e?.message ?? e)))
737
+ .finally(() => setBusy(false))
738
+ }
739
+
740
+ const updateNotification = (id, action) => {
741
+ setBusy(true); setError('')
742
+ fetch(`${API}/notifications/${action}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id }) })
743
+ .then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body })
744
+ .then(() => load())
745
+ .catch((e) => setError(String(e?.message ?? e)))
746
+ .finally(() => setBusy(false))
747
+ }
748
+
749
+ const openSelection = (row) => {
750
+ const available = row.availableModels ?? row.models ?? []
751
+ const defaults = row.selectableModels ?? available
752
+ closePolicy()
753
+ setSelectionProvider(row.provider)
754
+ setSelectionDraft(row.selectedModels?.length ? row.selectedModels : defaults.map((model) => model.id))
755
+ setCapabilityFilter([])
756
+ setSearchQuery('')
757
+ setSortBy('name')
758
+ setPageSize(50)
759
+ setQuickFilter({ context100k: false, cheap: false })
760
+ setBatchSummary(null)
761
+ }
762
+
763
+ const closeSelection = () => {
764
+ setSelectionProvider('')
765
+ setSelectionDraft([])
766
+ setCapabilityFilter([])
767
+ setSearchQuery('')
768
+ setSortBy('name')
769
+ setPageSize(50)
770
+ setQuickFilter({ context100k: false, cheap: false })
771
+ setBatchSummary(null)
772
+ }
773
+
774
+ const openPolicy = (row) => {
775
+ const policy = status?.policies?.[row.provider] ?? row.policy ?? {}
776
+ closeSelection()
777
+ setPolicyProvider(row.provider)
778
+ setPolicyDraft({
779
+ include: (policy.include ?? []).join('\n'),
780
+ exclude: (policy.exclude ?? []).join('\n'),
781
+ requireCapabilities: Object.keys(policy.requireCapabilities ?? {}).join(', '),
782
+ denyCapabilities: Object.keys(policy.denyCapabilities ?? {}).join(', '),
783
+ enableCostFilter: !!policy.enableCostFilter,
784
+ maxPricePerMillion: policy.maxPricePerMillion !== undefined ? String(policy.maxPricePerMillion) : '',
785
+ enableContextFilter: !!policy.enableContextFilter,
786
+ minContextTokens: policy.minContextTokens !== undefined ? String(policy.minContextTokens) : '',
787
+ })
788
+ }
789
+
790
+ const closePolicy = () => {
791
+ setPolicyProvider('')
792
+ setPolicyDraft({
793
+ include: '',
794
+ exclude: '',
795
+ requireCapabilities: '',
796
+ denyCapabilities: '',
797
+ enableCostFilter: false,
798
+ maxPricePerMillion: '',
799
+ enableContextFilter: false,
800
+ minContextTokens: '',
801
+ })
802
+ }
803
+
140
804
  const splitValues = (value) => [...new Set(String(value ?? '').split(/[,\n]+/).map((item) => item.trim()).filter(Boolean))]
141
805
  const capabilityValues = (value) => Object.fromEntries(splitValues(value).map((key) => [key, true]))
142
- const validatePatterns = (patterns) => { for (const p of patterns) { try { new RegExp(p, 'i') } catch { return p } } return null }
806
+ const validatePatterns = (patterns) => {
807
+ for (const p of patterns) {
808
+ try { new RegExp(p, 'i') } catch { return p }
809
+ }
810
+ return null
811
+ }
812
+
143
813
  const savePolicy = () => {
144
814
  const includeList = splitValues(policyDraft.include)
145
815
  const excludeList = splitValues(policyDraft.exclude)
@@ -147,21 +817,103 @@ window.__ModuleLoader__.load({
147
817
  if (badInclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badInclude)); return }
148
818
  const badExclude = validatePatterns(excludeList)
149
819
  if (badExclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badExclude)); return }
150
- setBusy(true); setError(''); fetch(`${API}/policy`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: policyProvider, include: includeList, exclude: excludeList, 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))
820
+ setBusy(true); setError('')
821
+ const payload = {
822
+ provider: policyProvider,
823
+ include: includeList,
824
+ exclude: excludeList,
825
+ requireCapabilities: capabilityValues(policyDraft.requireCapabilities),
826
+ denyCapabilities: capabilityValues(policyDraft.denyCapabilities),
827
+ enableCostFilter: !!policyDraft.enableCostFilter,
828
+ ...(policyDraft.enableCostFilter && policyDraft.maxPricePerMillion !== '' ? { maxPricePerMillion: Number(policyDraft.maxPricePerMillion) } : {}),
829
+ enableContextFilter: !!policyDraft.enableContextFilter,
830
+ ...(policyDraft.enableContextFilter && policyDraft.minContextTokens !== '' ? { minContextTokens: Number(policyDraft.minContextTokens) } : {}),
831
+ }
832
+ fetch(`${API}/policy`, {
833
+ method: 'POST',
834
+ headers: { 'content-type': 'application/json' },
835
+ body: JSON.stringify(payload),
836
+ })
837
+ .then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body })
838
+ .then(() => { closePolicy(); return load() })
839
+ .catch((e) => setError(String(e?.message ?? e)))
840
+ .finally(() => setBusy(false))
841
+ }
842
+
843
+ const saveSelection = () => {
844
+ const row = rows.find((item) => item.provider === selectionProvider)
845
+ const available = row?.availableModels ?? row?.models ?? []
846
+ if (available.length > 0 && selectionDraft.length === 0) { setError(t.selectAtLeastOne); return }
847
+ const models = selectionDraft.length === available.length ? [] : selectionDraft
848
+ setBusy(true); setError('')
849
+ fetch(`${API}/selection`, {
850
+ method: 'POST',
851
+ headers: { 'content-type': 'application/json' },
852
+ body: JSON.stringify({ provider: selectionProvider, models }),
853
+ })
854
+ .then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body })
855
+ .then((body) => { setStatus(body); closeSelection() })
856
+ .catch((e) => setError(String(e?.message ?? e)))
857
+ .finally(() => setBusy(false))
858
+ }
859
+
860
+ const rollback = (entry, targetProvider) => {
861
+ if (!window.confirm(t.rollbackConfirm)) return
862
+ setBusy(true); setError('')
863
+ fetch(`${API}/history/rollback`, {
864
+ method: 'POST',
865
+ headers: { 'content-type': 'application/json' },
866
+ body: JSON.stringify({ historyId: entry.id, provider: targetProvider }),
867
+ })
868
+ .then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body })
869
+ .then(() => load())
870
+ .catch((e) => setError(String(e?.message ?? e)))
871
+ .finally(() => setBusy(false))
872
+ }
873
+
874
+ const isNewModel = (model, row) => {
875
+ const added = status?.lastRun?.results?.find((r) => r.provider === row.provider)?.diff?.added || []
876
+ const isAdded = added.some((m) => m.id === model.id)
877
+ if (!isAdded) return false
878
+ const finishedAt = status?.lastRun?.finishedAt
879
+ if (!finishedAt) return false
880
+ return (Date.now() - new Date(finishedAt).getTime()) < 7 * 24 * 60 * 60 * 1000
151
881
  }
152
- 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)) }
153
- 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)) }
154
- 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; };
155
- const rows = (status?.providers ?? []).filter((row) => row.configured); const selectedRow = rows.find((row) => row.provider === selectionProvider); const choices = selectedRow?.availableModels ?? selectedRow?.models ?? [];
882
+
883
+ const rows = (status?.providers ?? []).filter((row) => row.configured)
884
+ const selectedRow = rows.find((row) => row.provider === selectionProvider)
885
+ const choices = selectedRow?.availableModels ?? selectedRow?.models ?? []
886
+
156
887
  const filteredChoices = React.useMemo(() => {
157
- const filteredChoicesBase = filterModelsByCapabilities(choices, capabilityFilter);
158
- const tokens = debouncedSearchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean);
159
- 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;
160
- 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; });
161
- return [...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 || ''); });
162
- }, [choices, capabilityFilter, debouncedSearchQuery, quickFilter, sortBy]);
163
- const maxContext = React.useMemo(() => filteredChoices.reduce((max, m) => Math.max(max, m.contextWindow || 0), 0), [filteredChoices]);
164
- const minPrice = React.useMemo(() => filteredChoices.reduce((min, m) => m.pricing?.inputPerToken !== undefined ? Math.min(min, m.pricing.inputPerToken) : min, Infinity), [filteredChoices]); 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, variant) => React.createElement('button', { type: 'button', disabled: busy, onClick, className: 'dms-btn' + (variant ? ' dms-btn-' + variant : '') }, label)
888
+ const filteredChoicesBase = filterModelsByCapabilities(choices, capabilityFilter)
889
+ const tokens = debouncedSearchQuery.trim().toLowerCase().split(/\s+/).filter(Boolean)
890
+ const searchedChoices = tokens.length ? filteredChoicesBase.filter((m) => {
891
+ const haystack = (m.id + ' ' + (m.name || '') + ' ' + (m.aliases || []).join(' ') + ' ' + (m.tags || []).join(' ') + ' ' + (m.description || '')).toLowerCase()
892
+ return tokens.every((tok) => haystack.includes(tok))
893
+ }) : filteredChoicesBase
894
+ const quickFilteredChoices = searchedChoices.filter((m) => {
895
+ if (quickFilter.context100k && (!m.contextWindow || m.contextWindow < 100000)) return false
896
+ if (quickFilter.cheap && (!m.pricing?.inputPerToken || (m.pricing.inputPerToken * 1000000) > 1.0)) return false
897
+ return true
898
+ })
899
+ return [...quickFilteredChoices].sort((a, b) => {
900
+ if (sortBy === 'price') {
901
+ const pa = a.pricing?.inputPerToken ?? Infinity
902
+ const pb = b.pricing?.inputPerToken ?? Infinity
903
+ return pa - pb
904
+ }
905
+ if (sortBy === 'context') return (b.contextWindow ?? 0) - (a.contextWindow ?? 0)
906
+ if (sortBy === 'date') return (b.firstSeenAt ?? 0) - (a.firstSeenAt ?? 0)
907
+ return (a.name || a.id || '').localeCompare(b.name || b.id || '')
908
+ })
909
+ }, [choices, capabilityFilter, debouncedSearchQuery, quickFilter, sortBy])
910
+
911
+ const maxContext = React.useMemo(() => filteredChoices.reduce((max, m) => Math.max(max, m.contextWindow || 0), 0), [filteredChoices])
912
+ const minPrice = React.useMemo(() => filteredChoices.reduce((min, m) => m.pricing?.inputPerToken !== undefined ? Math.min(min, m.pricing.inputPerToken) : min, Infinity), [filteredChoices])
913
+ const availableCapabilities = [...new Set(choices.flatMap((model) => Object.entries(model?.capabilities ?? {}).filter(([, enabled]) => enabled === true).map(([key]) => key)))].sort()
914
+ const visibleChoices = filteredChoices.slice(0, pageSize)
915
+ const button = (label, onClick, variant) => React.createElement('button', { type: 'button', disabled: busy, onClick, className: 'dms-btn' + (variant ? ' dms-btn-' + variant : '') }, label)
916
+
165
917
  const renderNotifications = () => {
166
918
  const entries = status?.notifications ?? []
167
919
  if (entries.length === 0) return React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noNotifications)
@@ -171,6 +923,7 @@ window.__ModuleLoader__.load({
171
923
  item.acknowledgedAt ? null : button(t.acknowledge, () => updateNotification(item.id, 'acknowledge')),
172
924
  React.createElement('small', { style: { display: 'block', marginLeft: 12 } }, (item.providers ?? []).map((row) => row.provider + ': ' + row.status + (row.message ? ' · ' + row.message : '')).join('; '))))
173
925
  }
926
+
174
927
  const renderScheduler = () => {
175
928
  return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%' } },
176
929
  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,
@@ -184,6 +937,75 @@ window.__ModuleLoader__.load({
184
937
  ) : null
185
938
  )
186
939
  }
940
+
941
+ const renderAliases = () => {
942
+ const aliasMap = status?.aliases ?? {}
943
+ const entries = Object.entries(aliasMap)
944
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%' } },
945
+ entries.length === 0 ? React.createElement('small', { style: { display: 'block', marginTop: 4 } }, t.noAliases) : entries.map(([aliasName, data]) => {
946
+ return React.createElement('div', { key: aliasName, className: 'dms-row', style: { justifyContent: 'space-between', padding: '6px 10px', border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 6, background: 'var(--dsw-alias-bg-layer-2)' } },
947
+ React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
948
+ React.createElement('strong', { style: { fontFamily: 'monospace', fontSize: 13 } }, aliasName),
949
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } }, `→ ${data.provider} / ${data.model}`)
950
+ ),
951
+ button(t.removeAlias, () => deleteAlias(aliasName), 'danger')
952
+ )
953
+ }),
954
+ React.createElement('div', { className: 'dms-row', style: { marginTop: 8, gap: 8 } },
955
+ React.createElement('input', {
956
+ className: 'dms-input',
957
+ placeholder: t.aliasPlaceholder,
958
+ value: aliasDraft.alias,
959
+ onChange: (e) => setAliasDraft((d) => ({ ...d, alias: e.target.value })),
960
+ style: { flex: '1 1 140px' }
961
+ }),
962
+ React.createElement('select', {
963
+ className: 'dms-input',
964
+ value: aliasDraft.provider,
965
+ onChange: (e) => setAliasDraft((d) => ({ ...d, provider: e.target.value, model: '' })),
966
+ style: { flex: '1 1 120px' }
967
+ },
968
+ React.createElement('option', { value: '' }, `-- ${t.targetProvider} --`),
969
+ rows.map((r) => React.createElement('option', { key: r.provider, value: r.provider }, r.provider))
970
+ ),
971
+ React.createElement('input', {
972
+ className: 'dms-input',
973
+ placeholder: t.targetModel,
974
+ value: aliasDraft.model,
975
+ onChange: (e) => setAliasDraft((d) => ({ ...d, model: e.target.value })),
976
+ style: { flex: '1 1 140px' }
977
+ }),
978
+ button(t.addAlias, saveAlias, 'primary')
979
+ ),
980
+ aliasMsg ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-success-primary)' } }, aliasMsg) : null,
981
+ aliasErr ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, aliasErr) : null
982
+ )
983
+ }
984
+
985
+ const renderExportImport = () => {
986
+ return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, width: '100%' } },
987
+ React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' } },
988
+ button(t.exportConfig, handleExport),
989
+ button(t.importConfig, () => setShowImportBox((v) => !v)),
990
+ importMsg ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-success-primary)' } }, importMsg) : null,
991
+ importErr ? React.createElement('small', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, importErr) : null
992
+ ),
993
+ showImportBox ? React.createElement('div', { className: 'dms-editor', style: { marginTop: 6 } },
994
+ React.createElement('textarea', {
995
+ rows: 4,
996
+ value: importText,
997
+ onChange: (e) => setImportText(e.target.value),
998
+ placeholder: t.importPlaceholder,
999
+ style: { width: '100%', fontFamily: 'monospace', fontSize: 12 }
1000
+ }),
1001
+ React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 6 } },
1002
+ button(t.apply, handleImport, 'primary'),
1003
+ button(t.cancel, () => { setShowImportBox(false); setImportErr('') })
1004
+ )
1005
+ ) : null
1006
+ )
1007
+ }
1008
+
187
1009
  const renderHistory = () => {
188
1010
  const entries = status?.history ?? []
189
1011
  if (entries.length === 0) return React.createElement('small', { style: { display: 'block', marginTop: 6 } }, t.noHistory)
@@ -199,69 +1021,259 @@ window.__ModuleLoader__.load({
199
1021
  React.createElement('small', { style: { display: 'block', marginTop: 4 } }, details))
200
1022
  })))
201
1023
  }
1024
+
202
1025
  return React.createElement('div', { className: 'dms-wrap' },
203
1026
  includeIntro ? React.createElement('p', null, t.desc) : null,
204
- 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}`)),
205
- React.createElement('div', { className: 'dms-actions' }, button(t.refresh, load), button(t.refreshAll, () => execute(undefined)), button(dryRun ? t.run : t.apply, () => execute(provider || undefined), dryRun ? undefined : 'primary'), button(t.health, () => checkHealth(provider || undefined)), button(t.checkCredentials, () => checkCredentials(provider || undefined))),
206
- 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), 'primary'), 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,
207
- rows.map((row) => {
208
- const health = status?.lastHealth?.results?.find((item) => item.provider === row.provider)
209
- const healthText = health ? ` · ${health.status === 'ok' ? t.healthOk : health.status === 'dormant' ? t.healthDormant : t.healthError}` : ''
210
- 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 })
211
- const lifecycleText = lifecycleCounts.stale || lifecycleCounts.deprecated ? ` · ${lifecycleCounts.stale} ${t.stale}, ${lifecycleCounts.deprecated} ${t.deprecated}` : ''
212
- const modelCount = (row.availableModels ?? row.models ?? []).length
1027
+ React.createElement('div', { className: 'dms-toolbar' },
1028
+ React.createElement('select', { value: provider, onChange: (e) => setProvider(e.target.value) },
1029
+ React.createElement('option', { value: '' }, t.all),
1030
+ rows.map((row) => React.createElement('option', { key: row.provider, value: row.provider }, row.provider))
1031
+ ),
1032
+ React.createElement('label', { style: { marginLeft: 12 } }, React.createElement('input', { type: 'checkbox', checked: dryRun, onChange: (e) => setDryRun(e.target.checked) }), ` ${t.dryRun}`),
1033
+ React.createElement('label', { style: { marginLeft: 12 } }, React.createElement('input', { type: 'checkbox', checked: removeMissing, onChange: (e) => setRemoveMissing(e.target.checked) }), ` ${t.removeMissing}`)
1034
+ ),
1035
+ React.createElement('div', { className: 'dms-actions' },
1036
+ button(t.refresh, load),
1037
+ button(t.refreshAll, () => execute(undefined)),
1038
+ button(dryRun ? t.run : t.apply, () => execute(provider || undefined), dryRun ? undefined : 'primary'),
1039
+ button(t.health, () => checkHealth(provider || undefined)),
1040
+ button(t.checkCredentials, () => checkCredentials(provider || undefined))
1041
+ ),
1042
+ busy ? React.createElement('span', null, t.running) : null,
1043
+ error ? React.createElement('div', { className: 'dms-alert dms-alert-bad' }, error) : null,
1044
+ preview ? React.createElement('div', { className: 'dms-editor' },
1045
+ React.createElement('strong', null, t.previewTitle),
1046
+ preview.length === 0 ? React.createElement('small', null, t.previewNone) : preview.map((item) => {
1047
+ const counts = item.counts ?? {}
1048
+ const names = (items) => (items ?? []).map((model) => model?.after?.id ?? model?.id).filter(Boolean).join(', ')
1049
+ 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(', ')}`
1050
+ return React.createElement('div', { key: item.provider, style: { margin: '4px 0' } },
1051
+ React.createElement('span', null, `${item.provider}: ${item.status} · +${counts.added ?? 0} ${t.previewAdded}, -${counts.removed ?? 0} ${t.previewRemoved}, ${counts.changed ?? 0} ${t.previewChanged}`),
1052
+ React.createElement('small', { style: { display: 'block', marginLeft: 8 } }, details)
1053
+ )
1054
+ }),
1055
+ React.createElement('div', { style: { marginTop: 8 } },
1056
+ button(t.previewApply, () => execute(provider || undefined), 'primary'),
1057
+ button(t.previewDismiss, () => setPreview(null))
1058
+ )
1059
+ ) : null,
1060
+ status?.lastRun ? React.createElement('small', null, `${t.lastRun}: ${new Date(status.lastRun.finishedAt).toLocaleString()}`) : null,
1061
+ rows.length === 0 ? React.createElement('p', null, t.noProviders) : rows.map((row) => {
213
1062
  const latestRun = status?.lastRun?.results?.find((item) => item.provider === row.provider)
214
1063
  const addedCount = latestRun?.status === 'ok' ? (latestRun.diff?.added?.length ?? 0) : 0
215
- const modelSummary = `${modelCount} models${addedCount > 0 ? ` +${addedCount}` : ''}`
216
- const healthEntry = status?.lastHealth?.results?.find((item) => item.provider === row.provider)
217
- const latencyText = healthEntry?.latencyMs ? ` · ${healthEntry.latencyMs}ms` : ''
218
- const isOffline = latestRun?.status !== 'ok' && modelCount > 0
219
- 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))) : ''
220
- const statusBadgeClass = row.configured ? 'dms-badge dms-badge-ok' : 'dms-badge dms-badge-warn'
221
- const healthBadgeClass = health?.status === 'ok' ? 'dms-badge dms-badge-ok' : health?.status === 'dormant' ? 'dms-badge' : 'dms-badge dms-badge-bad'
222
- const providerCard = React.createElement('div', { className: 'dms-provider' },
223
- React.createElement('span', { style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
224
- React.createElement('strong', { style: { fontSize: 13, marginRight: 2 } }, row.provider),
225
- React.createElement('span', { className: statusBadgeClass }, row.configured ? t.ok : t.dormant),
226
- React.createElement('span', { className: 'dms-badge' }, modelSummary),
227
- health ? React.createElement('span', { className: healthBadgeClass }, `${health.status === 'ok' ? t.healthOk : health.status === 'dormant' ? t.healthDormant : t.healthError}${latencyText}`) : null,
228
- lifecycleText ? React.createElement('span', { className: 'dms-badge dms-badge-warn' }, lifecycleText.replace(/^ · /, '')) : null,
229
- offlineText ? React.createElement('span', { className: 'dms-badge dms-badge-warn' }, offlineText.replace(/^ · /, '')) : null
1064
+ const count = row.models?.length ?? 0
1065
+ const availableCount = row.availableModels?.length ?? count
1066
+ const staleCount = row.staleModels?.length ?? 0
1067
+ const deprecatedCount = row.deprecatedModels?.length ?? 0
1068
+ const cachedHours = row.cachedAt ? Math.round((Date.now() - new Date(row.cachedAt).getTime()) / 3600000) : null
1069
+ const isStaleDiscovery = row.staleDiscovery === true
1070
+ const metaBadges = []
1071
+ if (cachedHours !== null) metaBadges.push(t.cacheHoursAgo.replace('%d', cachedHours))
1072
+ if (staleCount > 0) metaBadges.push(`${staleCount} ${t.stale}`)
1073
+ if (deprecatedCount > 0) metaBadges.push(`${deprecatedCount} ${t.deprecated}`)
1074
+ const modelSummary = count === availableCount ? `${count} ${t.selected}` : `${count}/${availableCount} ${t.selected}`
1075
+ const healthBadge = row.health?.status ? React.createElement('span', { className: 'dms-badge ' + (row.health.status === 'ok' ? 'dms-badge-ok' : row.health.status === 'dormant' ? 'dms-badge-warn' : 'dms-badge-bad') }, row.health.status === 'ok' ? t.healthOk : row.health.status === 'dormant' ? t.healthDormant : t.healthError) : null
1076
+ const staleBadge = isStaleDiscovery ? React.createElement('span', { className: 'dms-badge dms-badge-warn' }, t.stale) : null
1077
+ const providerCard = React.createElement('div', { key: row.provider, className: 'dms-provider' },
1078
+ React.createElement('span', null,
1079
+ React.createElement('strong', null, row.provider),
1080
+ ` · ${row.configured ? t.ok : t.dormant} · ${modelSummary}`,
1081
+ metaBadges.length ? ` · ${metaBadges.join(' · ')}` : '',
1082
+ ' ', healthBadge, ' ', staleBadge
230
1083
  ),
231
1084
  button(t.selectModels, () => openSelection(row)),
232
1085
  button(t.policy, () => openPolicy(row))
233
1086
  )
234
- 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, 'primary'), button(t.cancel, closeSelection))) : null
235
- 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, 'primary'), button(t.cancel, closePolicy))) : null
1087
+
1088
+ const selectionEditor = selectionProvider === row.provider ? React.createElement('div', { className: 'dms-editor' },
1089
+ React.createElement('strong', null, `${t.selectModels}: ${selectionProvider}`),
1090
+ React.createElement('div', { style: { display: 'flex', gap: 8, margin: '8px 0', flexWrap: 'wrap' } },
1091
+ 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)' } }),
1092
+ React.createElement('select', { value: sortBy, onChange: (e) => setSortBy(e.target.value), style: { padding: '6px 8px', borderRadius: 6 } },
1093
+ React.createElement('option', { value: 'name' }, t.sortName),
1094
+ React.createElement('option', { value: 'price' }, t.sortPrice),
1095
+ React.createElement('option', { value: 'context' }, t.sortContext),
1096
+ React.createElement('option', { value: 'date' }, t.sortDate)
1097
+ )
1098
+ ),
1099
+ React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } },
1100
+ 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),
1101
+ 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)
1102
+ ),
1103
+ React.createElement('div', { style: { margin: '8px 0' } },
1104
+ choices.length === 0 ? React.createElement('small', null, t.noModels) : React.createElement(React.Fragment, null,
1105
+ availableCapabilities.length > 0 ? React.createElement('div', { style: { marginBottom: 8 } },
1106
+ React.createElement('strong', null, t.capabilityFilter),
1107
+ availableCapabilities.map((capability) => React.createElement('label', { key: capability, style: { display: 'inline-block', marginLeft: 8 } },
1108
+ React.createElement('input', { type: 'checkbox', checked: capabilityFilter.includes(capability), onChange: (e) => setCapabilityFilter((current) => e.target.checked ? [...current, capability] : current.filter((item) => item !== capability)) }),
1109
+ ' ' + (CAPABILITY_LABELS[capability]?.[locale] ?? capability)
1110
+ ))
1111
+ ) : React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, t.noCapabilities),
1112
+ React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap' } },
1113
+ button(t.selectAllFiltered, () => { const ids = new Set(selectionDraft); for (const m of filteredChoices) ids.add(m.id); setSelectionDraft([...ids]) }),
1114
+ button(t.deselectAllFiltered, () => { const filterSet = new Set(filteredChoices.map((m) => m.id)); setSelectionDraft(selectionDraft.filter((id) => !filterSet.has(id))) }),
1115
+ 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]) })
1116
+ ),
1117
+ React.createElement('div', { style: { display: 'flex', gap: 6, margin: '6px 0', flexWrap: 'wrap', alignItems: 'center' } },
1118
+ button(batchTrying ? t.batchTryRunning : t.batchTry, runBatchTry),
1119
+ batchSummary && batchSummary.unreachable > 0 ? button(t.batchTryDeselectUnreachable, deselectUnreachable, 'danger') : null,
1120
+ batchSummary ? React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.batchTryResults.replace('%d', batchSummary.reachable).replace('%d', batchSummary.total)) : null
1121
+ ),
1122
+ React.createElement('small', { style: { display: 'block', marginBottom: 8 } }, `${filteredChoices.length}/${choices.length} ${t.capabilityCount}`),
1123
+ visibleChoices.map((model) => {
1124
+ const deprecationDays = (() => {
1125
+ const d = model.lifecycle?.deprecationDate
1126
+ if (!d) return null
1127
+ const diff = new Date(d).getTime() - Date.now()
1128
+ return diff > 0 ? Math.ceil(diff / (1000 * 60 * 60 * 24)) : 0
1129
+ })()
1130
+ const probe = tryState[`${row.provider}:${model.id}`]
1131
+ 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' } },
1132
+ 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)) }),
1133
+ ` ${model.name ?? model.id} (${model.id})`,
1134
+ 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,
1135
+ 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,
1136
+ (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,
1137
+ (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,
1138
+ model.tags && model.tags.length ? React.createElement('small', { style: { marginLeft: 6, color: 'var(--dsw-alias-label-secondary)' } }, model.tags.join(', ')) : null,
1139
+ 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,
1140
+ 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,
1141
+ 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,
1142
+ React.createElement('button', { type: 'button', onClick: (e) => { e.preventDefault(); doTry(row.provider, model.id) }, style: { marginLeft: 8, fontSize: 11, padding: '1px 6px', borderRadius: 4 } }, (probe && probe.loading) ? '…' : '▶'),
1143
+ probe && !probe.loading ? React.createElement('span', { className: 'dms-badge ' + (probe.ok ? 'dms-badge-ok' : 'dms-badge-bad'), style: { marginLeft: 6 } }, probe.ok ? `${probe.latency}ms` : (probe.error || probe.status || 'offline')) : null
1144
+ )
1145
+ }),
1146
+ filteredChoices.length > pageSize ? React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' } },
1147
+ button(t.showMore.replace('%d', Math.min(50, filteredChoices.length - pageSize)), () => setPageSize((s) => s + 50)),
1148
+ React.createElement('small', { style: { color: 'var(--dsw-alias-label-secondary)' } }, t.showingCount.replace('%d', visibleChoices.length).replace('%d', filteredChoices.length))
1149
+ ) : null
1150
+ )
1151
+ ),
1152
+ React.createElement('small', null, t.pickerNote),
1153
+ React.createElement('div', { style: { marginTop: 8 } },
1154
+ button(t.allModels, () => setSelectionDraft(choices.map((model) => model.id))),
1155
+ button(t.saveSelection, saveSelection, 'primary'),
1156
+ button(t.cancel, closeSelection)
1157
+ )
1158
+ ) : null
1159
+
1160
+ const policyEditor = policyProvider === row.provider ? React.createElement('div', { className: 'dms-editor' },
1161
+ React.createElement('strong', null, `${t.policy}: ${policyProvider}`),
1162
+ React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyInclude,
1163
+ React.createElement('textarea', { rows: 3, value: policyDraft.include, onChange: (e) => setPolicyDraft((current) => ({ ...current, include: e.target.value })), style: { display: 'block', width: '100%' } })
1164
+ ),
1165
+ React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyExclude,
1166
+ React.createElement('textarea', { rows: 3, value: policyDraft.exclude, onChange: (e) => setPolicyDraft((current) => ({ ...current, exclude: e.target.value })), style: { display: 'block', width: '100%' } })
1167
+ ),
1168
+ React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyRequire,
1169
+ React.createElement('input', { value: policyDraft.requireCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, requireCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })
1170
+ ),
1171
+ React.createElement('label', { style: { display: 'block', marginTop: 8 } }, t.policyDeny,
1172
+ React.createElement('input', { value: policyDraft.denyCapabilities, onChange: (e) => setPolicyDraft((current) => ({ ...current, denyCapabilities: e.target.value })), style: { display: 'block', width: '100%' } })
1173
+ ),
1174
+ React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, marginTop: 8, cursor: 'pointer' } },
1175
+ React.createElement('input', { type: 'checkbox', checked: !!policyDraft.enableCostFilter, onChange: (e) => setPolicyDraft((c) => ({ ...c, enableCostFilter: e.target.checked })) }),
1176
+ t.enableCostFilter
1177
+ ),
1178
+ policyDraft.enableCostFilter ? React.createElement('label', { style: { display: 'block', marginTop: 4, marginLeft: 22 } },
1179
+ t.maxPricePerMillion,
1180
+ React.createElement('input', { type: 'number', step: '0.1', min: '0', value: policyDraft.maxPricePerMillion ?? '', onChange: (e) => setPolicyDraft((c) => ({ ...c, maxPricePerMillion: e.target.value })), style: { display: 'block', width: 140, marginTop: 4 } })
1181
+ ) : null,
1182
+ React.createElement('label', { style: { display: 'flex', alignItems: 'center', gap: 7, marginTop: 8, cursor: 'pointer' } },
1183
+ React.createElement('input', { type: 'checkbox', checked: !!policyDraft.enableContextFilter, onChange: (e) => setPolicyDraft((c) => ({ ...c, enableContextFilter: e.target.checked })) }),
1184
+ t.enableContextFilter
1185
+ ),
1186
+ policyDraft.enableContextFilter ? React.createElement('label', { style: { display: 'block', marginTop: 4, marginLeft: 22 } },
1187
+ t.minContextTokens,
1188
+ React.createElement('input', { type: 'number', step: '1000', min: '0', value: policyDraft.minContextTokens ?? '', onChange: (e) => setPolicyDraft((c) => ({ ...c, minContextTokens: e.target.value })), style: { display: 'block', width: 140, marginTop: 4 } })
1189
+ ) : null,
1190
+ React.createElement('small', null, t.policyNote),
1191
+ React.createElement('div', { style: { marginTop: 8 } },
1192
+ button(t.savePolicy, savePolicy, 'primary'),
1193
+ button(t.cancel, closePolicy)
1194
+ )
1195
+ ) : null
1196
+
236
1197
  return React.createElement('div', { key: row.provider, className: 'dms-provider-wrap' }, providerCard, selectionEditor, policyEditor)
237
1198
  }),
238
- React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.notifications)), renderNotifications()),
239
- React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, 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}` : ''}`))))),
240
- React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.schedulerSettings)), renderScheduler()),
241
- React.createElement('div', { className: 'dms-section-card dms-section' }, React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.history)), renderHistory()),
1199
+ React.createElement('div', { className: 'dms-section-card dms-section' },
1200
+ React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.modelAliases)),
1201
+ renderAliases()
1202
+ ),
1203
+ React.createElement('div', { className: 'dms-section-card dms-section' },
1204
+ React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.exportImportTitle)),
1205
+ renderExportImport()
1206
+ ),
1207
+ React.createElement('div', { className: 'dms-section-card dms-section' },
1208
+ React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.notifications)),
1209
+ renderNotifications()
1210
+ ),
1211
+ React.createElement('div', { className: 'dms-section-card dms-section' },
1212
+ React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.credentials)),
1213
+ 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}` : ''}`))))
1214
+ ),
1215
+ React.createElement('div', { className: 'dms-section-card dms-section' },
1216
+ React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.schedulerSettings)),
1217
+ renderScheduler()
1218
+ ),
1219
+ React.createElement('div', { className: 'dms-section-card dms-section' },
1220
+ React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.history)),
1221
+ renderHistory()
1222
+ ),
1223
+ React.createElement('div', { className: 'dms-section-card dms-section' },
1224
+ React.createElement('div', { className: 'dms-section-title' }, React.createElement('strong', null, t.updaterTitle)),
1225
+ React.createElement(UpdaterSection, { t, api: API })
1226
+ )
242
1227
  )
243
1228
  }
1229
+
244
1230
  function ModelSyncCard({ ctx }) {
245
- const locale = useLocale(ctx); const t = locale === 'ru' ? ru : en; const [open, setOpen] = React.useState(false)
1231
+ const locale = useLocale(ctx)
1232
+ const t = locale === 'zh' ? zh : en
1233
+ const [open, setOpen] = React.useState(false)
246
1234
  const [cardStatus, setCardStatus] = React.useState(null)
247
- React.useEffect(() => { ensureStyles(); const load = () => { if (typeof document !== 'undefined' && document.hidden) return; fetch(`${API}/status`).then(r=>r.json()).then(setCardStatus).catch(()=>{}) }; load(); if (open) return; const id = setInterval(load, 15000); return () => clearInterval(id) }, [open])
248
- const cardBadge = (cardStatus?.lastRun?.results?.reduce((s,r)=>s+(r.diff?.added?.length||0),0) || 0)
1235
+ React.useEffect(() => {
1236
+ ensureStyles()
1237
+ const load = () => {
1238
+ if (typeof document !== 'undefined' && document.hidden) return
1239
+ fetch(`${API}/status`).then((r) => r.json()).then(setCardStatus).catch(() => {})
1240
+ }
1241
+ load()
1242
+ if (open) return
1243
+ const id = setInterval(load, 15000)
1244
+ return () => clearInterval(id)
1245
+ }, [open])
1246
+ const cardBadge = (cardStatus?.lastRun?.results?.reduce((s, r) => s + (r.diff?.added?.length || 0), 0) || 0)
249
1247
  return React.createElement('li', { className: 'dms-card' + (open ? ' dms-card-open' : '') },
250
1248
  React.createElement('button', { type: 'button', className: 'dms-card-header', 'aria-expanded': open, 'aria-label': `${t[open ? 'hideSettings' : 'showSettings']}: ${t.title}`, onClick: () => setOpen((value) => !value) },
251
- 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)),
1249
+ React.createElement('span', { className: 'dms-card-head-text' },
1250
+ React.createElement('span', { className: 'dms-card-name' }, t.title),
1251
+ React.createElement('span', { className: 'dms-card-description' }, t.desc)
1252
+ ),
252
1253
  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,
253
- 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' }))),
254
- open ? React.createElement('div', { className: 'dms-card-body' }, React.createElement(ErrorBoundary, null, React.createElement(ModelSyncSection, { ctx, includeIntro: false }))) : null)
1254
+ 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' },
1255
+ 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' })
1256
+ )
1257
+ ),
1258
+ open ? React.createElement('div', { className: 'dms-card-body' }, React.createElement(ErrorBoundary, null, React.createElement(ModelSyncSection, { ctx, includeIntro: false }))) : null
1259
+ )
255
1260
  }
1261
+
256
1262
  function apply(ctx) {
257
1263
  ctx.effect(() => {
258
- try { ctx.locale?.register?.(NS, { en, ru }) } catch {}
1264
+ try { ctx.locale?.register?.(NS, { en, zh }) } catch (err) {
1265
+ try { console.debug('[dsh-model-sync] locale register best-effort:', err) } catch { /* console unavailable */ }
1266
+ }
259
1267
  return () => {}
260
1268
  }, 'dsh-model-sync: dictionaries')
261
1269
  try {
262
1270
  ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) }, (props) => React.createElement(ErrorBoundary, null, React.createElement(ModelSyncCard, { ...props, ctx }))))
263
- } catch {}
1271
+ } catch (err) {
1272
+ try { console.debug('[dsh-model-sync] slots inject best-effort:', err) } catch { /* console unavailable */ }
1273
+ }
264
1274
  }
265
- module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], filterModelsByCapabilities }; return module.exports
1275
+
1276
+ module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'], filterModelsByCapabilities }
1277
+ return module.exports
266
1278
  },
267
1279
  })