GET /api/plugins now lists all discovered plugins (enabled or not) with status; PUT /api/plugins/<name> toggles enabled (persists to plugins.json). New Settings > Plugins admin page with per-plugin toggles. Route changes apply on next restart. Replaces CLI-only enable/disable for self-serve admin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
3.3 KiB
Vue
141 lines
3.3 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h2>Plugins</h2>
|
|
<span v-if="contractVersion" class="contract-badge">contract v{{ contractVersion }}</span>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div v-if="loading" class="loading">Loading...</div>
|
|
|
|
<template v-else>
|
|
<p class="hint">
|
|
Disabling a plugin removes its pages and API on the next app restart.
|
|
</p>
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Plugin</th>
|
|
<th>Version</th>
|
|
<th>Description</th>
|
|
<th>Enabled</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="p in plugins" :key="p.name">
|
|
<td>{{ p.name }}</td>
|
|
<td class="mono">{{ p.version }}</td>
|
|
<td class="cell-truncate" :title="p.description">{{ p.description || '-' }}</td>
|
|
<td>
|
|
<button
|
|
class="toggle-btn"
|
|
:class="{ active: p.enabled }"
|
|
:disabled="saving"
|
|
@click="toggle(p)"
|
|
>
|
|
<span class="toggle-slider"></span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="error" class="error-message">{{ error }}</div>
|
|
<div v-if="message" class="success-message">{{ message }}</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue'
|
|
import { pluginsApi } from '../../api'
|
|
|
|
const plugins = ref([])
|
|
const contractVersion = ref('')
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const error = ref('')
|
|
const message = ref('')
|
|
|
|
onMounted(() => load())
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
try {
|
|
const response = await pluginsApi.list()
|
|
plugins.value = response.data.data.plugins || []
|
|
contractVersion.value = response.data.data.contract_version || ''
|
|
} catch (err) {
|
|
error.value = 'Failed to load plugins'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function toggle(p) {
|
|
error.value = ''
|
|
message.value = ''
|
|
saving.value = true
|
|
try {
|
|
const response = await pluginsApi.setEnabled(p.name, !p.enabled)
|
|
p.enabled = !p.enabled
|
|
message.value = response.data.message || 'Updated'
|
|
} catch (err) {
|
|
error.value = err.response?.data?.data?.error?.message || 'Failed to update plugin'
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.mono {
|
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
|
}
|
|
|
|
.hint {
|
|
color: var(--text-light);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.contract-badge {
|
|
font-size: 0.8rem;
|
|
color: var(--text-light);
|
|
background: var(--bg);
|
|
padding: 0.25rem 0.6rem;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.toggle-btn {
|
|
width: 44px;
|
|
height: 24px;
|
|
border-radius: 12px;
|
|
border: none;
|
|
background: var(--border);
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.toggle-btn.active {
|
|
background: var(--success);
|
|
}
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.toggle-btn.active .toggle-slider {
|
|
transform: translateX(20px);
|
|
}
|
|
</style>
|