feat: frame-based positioning and mark system foundation
Core Changes: - Fix SearchView to use start_frame/end_frame directly (no time*fps conversion) - Add hard_delete support to delete_trace API - VideoPlayer: Main timeline + Mark system foundation - Proxy: Add local routes for auth, media, identity-matches, cluster-results - Add .gitignore to exclude build artifacts and dependencies Design Documents: - Multi-track Mark system design (.opencode/plans/) - Video editing positioning standards research Files Modified: - src/views/SearchView.vue: Frame positioning, ensureMinDuration (240 frames) - src/views/PeopleView.vue: batchDeleteGroups with hard_delete - src/api/index.ts: delete_trace with hard_delete body - src/components/VideoPlayer.vue: Timeline + Mark UI - src-tauri/src/proxy.rs: New local routes - AGENTS.md: Update documentation
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
<button class="admin-tab" :class="{ active: activeTab === 'dashboard' }" @click="activeTab = 'dashboard'">Dashboard</button>
|
||||
<button class="admin-tab" :class="{ active: activeTab === 'users' }" @click="activeTab = 'users'">Users</button>
|
||||
<button class="admin-tab" :class="{ active: activeTab === 'shares' }" @click="activeTab = 'shares'">Shares</button>
|
||||
<button class="admin-tab" :class="{ active: activeTab === 'settings' }" @click="activeTab = 'settings'">Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +42,108 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'settings'" class="settings-panel">
|
||||
<div v-if="settingsError" class="settings-error">{{ settingsError }}</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3>Upload Path</h3>
|
||||
<p class="settings-desc">Base directory where uploaded files are stored (appended with /<username>).</p>
|
||||
<div class="settings-row">
|
||||
<input class="settings-input" v-model="uploadPath" placeholder="/Users/accusys/momentry/var/sftpgo/data" />
|
||||
<button class="settings-save-btn" @click="saveUploadPath" :disabled="saving">{{ saving ? 'Saving...' : 'Save' }}</button>
|
||||
</div>
|
||||
<div v-if="saveMessage" :class="['settings-msg', saveError ? 'error' : 'ok']">{{ saveMessage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<h3>Service URLs</h3>
|
||||
<p class="settings-desc">Configure backend service endpoints.</p>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-header">
|
||||
<span class="service-name">Core API</span>
|
||||
<span :class="['service-status', coreStatus]">{{ coreStatus }}</span>
|
||||
</div>
|
||||
<div class="service-row">
|
||||
<input class="settings-input" v-model="coreApiUrl" placeholder="http://localhost:3002" />
|
||||
<button class="settings-test-btn" @click="testCoreApi" :disabled="testingCore">{{ testingCore ? 'Testing...' : 'Test' }}</button>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="service-stop-btn" @click="stopService('core')" :disabled="coreActionLoading">Stop</button>
|
||||
<button class="service-start-btn" @click="startService('core')" :disabled="coreActionLoading">Start</button>
|
||||
<span v-if="coreActionLoading" class="action-loading">Processing...</span>
|
||||
<span v-if="coreActionResult" :class="['action-result', coreActionResultOk ? 'ok' : 'error']">{{ coreActionResult }}</span>
|
||||
</div>
|
||||
<div v-if="coreInfo" class="service-info">
|
||||
<span>Version: {{ coreInfo.version || 'N/A' }}</span>
|
||||
<span>Uptime: {{ formatUptime(coreInfo.uptime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-header">
|
||||
<span class="service-name">MarkBase</span>
|
||||
<span :class="['service-status', mbStatus]">{{ mbStatus }}</span>
|
||||
</div>
|
||||
<div class="service-row">
|
||||
<input class="settings-input" v-model="markbaseUrl" placeholder="http://localhost:11438" />
|
||||
<button class="settings-test-btn" @click="testMarkbase" :disabled="testingMb">{{ testingMb ? 'Testing...' : 'Test' }}</button>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="service-stop-btn" @click="stopService('markbase')" :disabled="mbActionLoading">Stop</button>
|
||||
<button class="service-start-btn" @click="startService('markbase')" :disabled="mbActionLoading">Start</button>
|
||||
<span v-if="mbActionLoading" class="action-loading">Processing...</span>
|
||||
<span v-if="mbActionResult" :class="['action-result', mbActionResultOk ? 'ok' : 'error']">{{ mbActionResult }}</span>
|
||||
</div>
|
||||
<div v-if="mbInfo" class="service-info">
|
||||
<span>Version: {{ mbInfo.version || 'N/A' }}</span>
|
||||
<span>Uptime: {{ formatUptime(mbInfo.uptime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-header">
|
||||
<span class="service-name">MarkBaseEngine</span>
|
||||
<span :class="['service-status', mbeStatus]">{{ mbeStatus }}</span>
|
||||
</div>
|
||||
<div class="service-row">
|
||||
<input class="settings-input" v-model="mbeUrl" placeholder="http://localhost:8080" />
|
||||
<button class="settings-test-btn" @click="testMbe" :disabled="testingMbe">{{ testingMbe ? 'Testing...' : 'Test' }}</button>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="service-stop-btn" @click="stopService('mbe')" :disabled="mbeActionLoading">Stop</button>
|
||||
<button class="service-start-btn" @click="startService('mbe')" :disabled="mbeActionLoading">Start</button>
|
||||
<span v-if="mbeActionLoading" class="action-loading">Processing...</span>
|
||||
<span v-if="mbeActionResult" :class="['action-result', mbeActionResultOk ? 'ok' : 'error']">{{ mbeActionResult }}</span>
|
||||
</div>
|
||||
<div v-if="mbeInfo" class="service-info">
|
||||
<span>Version: {{ mbeInfo.version || 'N/A' }}</span>
|
||||
<span>Uptime: {{ formatUptime(mbeInfo.uptime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-header">
|
||||
<span class="service-name">Proxy</span>
|
||||
<span :class="['service-status', proxyStatus]">{{ proxyStatus }}</span>
|
||||
</div>
|
||||
<div class="service-row">
|
||||
<input class="settings-input" v-model="proxyUrl" placeholder="http://localhost:8888" />
|
||||
<button class="settings-test-btn" @click="testProxy" :disabled="testingProxy">{{ testingProxy ? 'Testing...' : 'Test' }}</button>
|
||||
</div>
|
||||
<div class="service-actions">
|
||||
<button class="service-stop-btn" @click="stopService('proxy')" :disabled="proxyActionLoading">Stop</button>
|
||||
<button class="service-start-btn" @click="startService('proxy')" :disabled="proxyActionLoading">Start</button>
|
||||
<span v-if="proxyActionLoading" class="action-loading">Processing...</span>
|
||||
<span v-if="proxyActionResult" :class="['action-result', proxyActionResultOk ? 'ok' : 'error']">{{ proxyActionResult }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="settings-save-btn" @click="saveServiceUrls" style="margin-top: 12px;">Save URLs</button>
|
||||
<div v-if="urlSaveMsg" :class="['settings-msg', urlSaveError ? 'error' : 'ok']">{{ urlSaveMsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -54,6 +157,226 @@ const stats = ref({ cpu: 0, memory: 0, disk: 0 })
|
||||
const users = ref<any[]>([])
|
||||
const shares = ref<any[]>([])
|
||||
|
||||
// Settings
|
||||
const uploadPath = ref('')
|
||||
const saving = ref(false)
|
||||
const saveMessage = ref('')
|
||||
const saveError = ref(false)
|
||||
const settingsError = ref('')
|
||||
|
||||
const coreApiUrl = ref('http://localhost:3002')
|
||||
const markbaseUrl = ref('http://localhost:11438')
|
||||
const mbeUrl = ref('http://localhost:8080')
|
||||
const proxyUrl = ref('http://localhost:8888')
|
||||
const coreStatus = ref('unknown')
|
||||
const mbStatus = ref('unknown')
|
||||
const mbeStatus = ref('unknown')
|
||||
const proxyStatus = ref('unknown')
|
||||
const coreInfo = ref<any>(null)
|
||||
const mbInfo = ref<any>(null)
|
||||
const mbeInfo = ref<any>(null)
|
||||
const testingCore = ref(false)
|
||||
const testingMb = ref(false)
|
||||
const testingMbe = ref(false)
|
||||
const testingProxy = ref(false)
|
||||
const urlSaveMsg = ref('')
|
||||
const urlSaveError = ref(false)
|
||||
|
||||
// Service stop/start state
|
||||
const coreActionLoading = ref(false)
|
||||
const coreActionResult = ref('')
|
||||
const coreActionResultOk = ref(false)
|
||||
const mbActionLoading = ref(false)
|
||||
const mbActionResult = ref('')
|
||||
const mbActionResultOk = ref(false)
|
||||
const mbeActionLoading = ref(false)
|
||||
const mbeActionResult = ref('')
|
||||
const mbeActionResultOk = ref(false)
|
||||
const proxyActionLoading = ref(false)
|
||||
const proxyActionResult = ref('')
|
||||
const proxyActionResultOk = ref(false)
|
||||
|
||||
async function stopService(name: string) {
|
||||
const s = serviceState(name)
|
||||
s.loading.value = true
|
||||
s.result.value = ''
|
||||
try {
|
||||
const result = await apiCall('stop_service', { name })
|
||||
s.result.value = result === 'stopped' ? 'Service stopped' : result
|
||||
s.ok.value = true
|
||||
setTimeout(() => s.test(), 1000)
|
||||
} catch (e: any) {
|
||||
s.result.value = 'Error: ' + (e.message || e)
|
||||
s.ok.value = false
|
||||
} finally {
|
||||
s.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startService(name: string) {
|
||||
const s = serviceState(name)
|
||||
s.loading.value = true
|
||||
s.result.value = ''
|
||||
try {
|
||||
const result = await apiCall('start_service', { name })
|
||||
const started = typeof result === 'string' ? result === 'started' : result?.result === 'started'
|
||||
s.result.value = started ? (name === 'mbe' ? 'Loading model...' : 'Service started') : JSON.stringify(result)
|
||||
s.ok.value = started
|
||||
// Poll up to 30s for the service to pass health check
|
||||
for (let i = 0; i < 15; i++) {
|
||||
await new Promise(r => setTimeout(r, 2000))
|
||||
await s.test()
|
||||
if (s.status.value === 'online') {
|
||||
s.result.value = 'Service started'
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
s.result.value = 'Error: ' + (e.message || e)
|
||||
s.ok.value = false
|
||||
} finally {
|
||||
s.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function serviceState(name: string) {
|
||||
switch (name) {
|
||||
case 'core': return { loading: coreActionLoading, result: coreActionResult, ok: coreActionResultOk, status: coreStatus, test: testCoreApi }
|
||||
case 'markbase': return { loading: mbActionLoading, result: mbActionResult, ok: mbActionResultOk, status: mbStatus, test: testMarkbase }
|
||||
case 'proxy': return { loading: proxyActionLoading, result: proxyActionResult, ok: proxyActionResultOk, status: proxyStatus, test: testProxy }
|
||||
default: return { loading: mbeActionLoading, result: mbeActionResult, ok: mbeActionResultOk, status: mbeStatus, test: testMbe }
|
||||
}
|
||||
}
|
||||
|
||||
function loadServiceUrls() {
|
||||
const stored = localStorage.getItem('service_urls')
|
||||
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
const currentOrigin = window.location.origin
|
||||
|
||||
if (stored) {
|
||||
try {
|
||||
const urls = JSON.parse(stored)
|
||||
coreApiUrl.value = urls.core || (isLocalhost ? 'http://localhost:3002' : currentOrigin)
|
||||
markbaseUrl.value = urls.markbase || (isLocalhost ? 'http://localhost:11438' : currentOrigin)
|
||||
mbeUrl.value = urls.mbe || (isLocalhost ? 'http://localhost:8080' : currentOrigin)
|
||||
proxyUrl.value = urls.proxy || (isLocalhost ? 'http://localhost:8888' : currentOrigin)
|
||||
} catch (e) {
|
||||
coreApiUrl.value = isLocalhost ? 'http://localhost:3002' : currentOrigin
|
||||
markbaseUrl.value = isLocalhost ? 'http://localhost:11438' : currentOrigin
|
||||
mbeUrl.value = isLocalhost ? 'http://localhost:8080' : currentOrigin
|
||||
proxyUrl.value = isLocalhost ? 'http://localhost:8888' : currentOrigin
|
||||
}
|
||||
} else {
|
||||
coreApiUrl.value = isLocalhost ? 'http://localhost:3002' : currentOrigin
|
||||
markbaseUrl.value = isLocalhost ? 'http://localhost:11438' : currentOrigin
|
||||
mbeUrl.value = isLocalhost ? 'http://localhost:8080' : currentOrigin
|
||||
proxyUrl.value = isLocalhost ? 'http://localhost:8888' : currentOrigin
|
||||
}
|
||||
}
|
||||
|
||||
function saveServiceUrls() {
|
||||
urlSaveMsg.value = ''
|
||||
urlSaveError.value = false
|
||||
try {
|
||||
localStorage.setItem('service_urls', JSON.stringify({
|
||||
core: coreApiUrl.value,
|
||||
markbase: markbaseUrl.value,
|
||||
mbe: mbeUrl.value,
|
||||
proxy: proxyUrl.value,
|
||||
}))
|
||||
;(window as any).__CORE_API_URL__ = coreApiUrl.value
|
||||
;(window as any).__MARKBASE_URL__ = markbaseUrl.value
|
||||
;(window as any).__MBE_URL__ = mbeUrl.value
|
||||
urlSaveMsg.value = 'Service URLs saved'
|
||||
} catch (e: any) {
|
||||
urlSaveError.value = true
|
||||
urlSaveMsg.value = 'Failed to save: ' + (e.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
async function testCoreApi() {
|
||||
testingCore.value = true
|
||||
coreInfo.value = null
|
||||
try {
|
||||
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
const healthUrl = isLocalhost ? `${coreApiUrl.value}/health` : `${coreApiUrl.value}/api/v1/health`
|
||||
const resp = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) })
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data = await resp.json()
|
||||
coreStatus.value = 'online'
|
||||
coreInfo.value = { version: data.version, uptime: data.uptime_ms }
|
||||
} catch (e: any) {
|
||||
coreStatus.value = 'offline'
|
||||
coreInfo.value = null
|
||||
} finally {
|
||||
testingCore.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testMarkbase() {
|
||||
testingMb.value = true
|
||||
mbInfo.value = null
|
||||
try {
|
||||
const resp = await fetch(`${markbaseUrl.value}/api/v2/health`, { signal: AbortSignal.timeout(5000) })
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data = await resp.json()
|
||||
mbStatus.value = 'online'
|
||||
mbInfo.value = { version: data.version, uptime: data.uptime }
|
||||
} catch (e: any) {
|
||||
mbStatus.value = 'offline'
|
||||
mbInfo.value = null
|
||||
} finally {
|
||||
testingMb.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testMbe() {
|
||||
testingMbe.value = true
|
||||
mbeInfo.value = null
|
||||
try {
|
||||
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
const healthUrl = isLocalhost ? `${mbeUrl.value}/health` : `${mbeUrl.value}/v1/health`
|
||||
const resp = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) })
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const text = await resp.text()
|
||||
if (text === 'OK') {
|
||||
mbeStatus.value = 'online'
|
||||
mbeInfo.value = { version: 'MarkBaseEngine', uptime: 0 }
|
||||
} else {
|
||||
throw new Error('Unexpected response: ' + text)
|
||||
}
|
||||
} catch (e: any) {
|
||||
mbeStatus.value = 'offline'
|
||||
mbeInfo.value = null
|
||||
} finally {
|
||||
testingMbe.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testProxy() {
|
||||
testingProxy.value = true
|
||||
try {
|
||||
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
const healthUrl = isLocalhost ? `${proxyUrl.value}/health` : `${proxyUrl.value}/api/v1/health`
|
||||
const resp = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) })
|
||||
proxyStatus.value = resp.ok ? 'online' : 'offline'
|
||||
} catch {
|
||||
proxyStatus.value = 'offline'
|
||||
} finally {
|
||||
testingProxy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatUptime(seconds: number | undefined): string {
|
||||
if (!seconds) return 'N/A'
|
||||
const d = Math.floor(seconds / 86400)
|
||||
const h = Math.floor((seconds % 86400) / 3600)
|
||||
const m = Math.floor((seconds % 3600) / 60)
|
||||
if (d > 0) return `${d}d ${h}h ${m}m`
|
||||
if (h > 0) return `${h}h ${m}m`
|
||||
return `${m}m`
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
try { stats.value = await apiCall('get_system_stats', {}) || { cpu: 0, memory: 0, disk: 0 } } catch (e) { console.error(e) }
|
||||
}
|
||||
@@ -66,20 +389,90 @@ async function loadShares() {
|
||||
try { shares.value = await apiCall('list_shares', {}) || [] } catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
onMounted(async () => { await loadDashboard(); await loadUsers(); await loadShares() })
|
||||
async function loadSettings() {
|
||||
settingsError.value = ''
|
||||
saveMessage.value = ''
|
||||
try {
|
||||
const resp = await fetch(`${markbaseUrl.value}/api/v2/config`)
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const cfg = await resp.json()
|
||||
uploadPath.value = cfg.server?.upload_path || ''
|
||||
} catch (e: any) {
|
||||
settingsError.value = 'Failed to load settings: ' + (e.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveUploadPath() {
|
||||
saving.value = true
|
||||
saveMessage.value = ''
|
||||
saveError.value = false
|
||||
try {
|
||||
const resp = await fetch(`${markbaseUrl.value}/api/v2/config/edit?key=server.upload_path&value=${encodeURIComponent(uploadPath.value)}`, { method: 'POST' })
|
||||
const data = await resp.json()
|
||||
if (data.ok) {
|
||||
saveMessage.value = 'Upload path saved successfully'
|
||||
} else {
|
||||
saveError.value = true
|
||||
saveMessage.value = data.error || 'Save failed'
|
||||
}
|
||||
} catch (e: any) {
|
||||
saveError.value = true
|
||||
saveMessage.value = 'Error: ' + (e.message || e)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadServiceUrls()
|
||||
await Promise.all([loadDashboard(), loadUsers(), loadShares(), loadSettings()])
|
||||
await Promise.all([testCoreApi(), testMarkbase(), testMbe(), testProxy()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#ms-view-admin { padding: 20px; }
|
||||
#ms-view-admin { padding: 20px; color: var(--text-primary); }
|
||||
.admin-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.admin-tabs { display: flex; gap: 10px; }
|
||||
.admin-tab { padding: 8px 16px; border: none; background: #f0f0f0; cursor: pointer; border-radius: 4px; }
|
||||
.admin-tab.active { background: #2563eb; color: white; }
|
||||
.admin-content { background: white; border-radius: 8px; padding: 20px; }
|
||||
.admin-tab { padding: 8px 16px; border: none; background: var(--hover-background); color: var(--text-primary); cursor: pointer; border-radius: 4px; }
|
||||
.admin-tab.active { background: var(--primary-color); color: white; }
|
||||
.admin-content { background: var(--card-background); border-radius: 8px; padding: 20px; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 15px; }
|
||||
.stat-card { background: #f8f9fa; padding: 15px; border-radius: 8px; text-align: center; }
|
||||
.stat-label { font-size: 12px; color: #666; }
|
||||
.stat-value { font-size: 24px; font-weight: bold; }
|
||||
.stat-card { background: var(--hover-background); padding: 15px; border-radius: 8px; text-align: center; }
|
||||
.stat-label { font-size: 12px; color: var(--text-secondary); }
|
||||
.stat-value { font-size: 24px; font-weight: bold; color: var(--text-primary); }
|
||||
.admin-table { width: 100%; border-collapse: collapse; }
|
||||
.admin-table th, .admin-table td { padding: 10px; border: 1px solid #ddd; text-align: left; }
|
||||
.admin-table th, .admin-table td { padding: 10px; border: 1px solid var(--border-color); text-align: left; color: var(--text-primary); }
|
||||
.settings-panel { max-width: 600px; }
|
||||
.settings-group { margin-bottom: 24px; }
|
||||
.settings-group h3 { margin: 0 0 4px; font-size: 16px; color: var(--text-primary); }
|
||||
.settings-desc { margin: 0 0 10px; font-size: 13px; color: var(--text-secondary); }
|
||||
.settings-row { display: flex; gap: 8px; align-items: center; }
|
||||
.settings-input { flex: 1; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 4px; font-size: 14px; background: var(--card-background); color: var(--text-primary); }
|
||||
.settings-save-btn { padding: 8px 16px; background: var(--primary-color); color: white; border: none; border-radius: 4px; cursor: pointer; }
|
||||
.settings-save-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
.settings-msg { margin-top: 8px; font-size: 13px; }
|
||||
.settings-msg.ok { color: var(--success-color); }
|
||||
.settings-msg.error { color: var(--danger-color); }
|
||||
.settings-error { background: var(--danger-background); color: var(--danger-color); padding: 10px; border-radius: 4px; margin-bottom: 16px; }
|
||||
.service-card { background: var(--hover-background); border-radius: 6px; padding: 12px; margin-top: 12px; }
|
||||
.service-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||
.service-name { font-weight: 600; font-size: 14px; color: var(--text-primary); }
|
||||
.service-status { font-size: 12px; padding: 2px 8px; border-radius: 4px; text-transform: uppercase; }
|
||||
.service-status.online { background: rgba(var(--success-color), 0.2); color: var(--success-color); }
|
||||
.service-status.offline { background: var(--danger-background); color: var(--danger-color); }
|
||||
.service-status.unknown { background: var(--hover-background); color: var(--text-secondary); }
|
||||
.service-row { display: flex; gap: 8px; align-items: center; }
|
||||
.service-test-btn { padding: 6px 12px; background: var(--text-secondary); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; }
|
||||
.service-test-btn:disabled { opacity: 0.6; cursor: default; }
|
||||
.service-info { margin-top: 8px; font-size: 12px; color: var(--text-secondary); display: flex; gap: 16px; }
|
||||
.service-actions { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
|
||||
.service-stop-btn { padding: 4px 12px; background: var(--danger-color); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||
.service-stop-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.service-start-btn { padding: 4px 12px; background: var(--success-color); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
|
||||
.service-start-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.action-loading { font-size: 12px; color: var(--text-secondary); }
|
||||
.action-result { font-size: 12px; }
|
||||
.action-result.ok { color: var(--success-color); }
|
||||
.action-result.error { color: var(--danger-color); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user