Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,441 changes: 63 additions & 1,378 deletions web-ui/src/views/Manage.vue

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions web-ui/src/views/manage/AuditTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script setup>
import { onMounted } from 'vue'
import { useManage } from './context.js'
import { cols, cellAt } from './cells.js'

const { data, fetchTab } = useManage()
onMounted(() => fetchTab('audit'))
</script>

<template>
<table v-if="Array.isArray(data.audit)">
<thead>
<tr><th v-for="c in cols(data.audit)" :key="c">{{ c }}</th></tr>
</thead>
<tbody>
<tr v-for="(row, i) in data.audit" :key="i">
<td v-for="c in cols(data.audit)" :key="c" :title="cellAt(row, c)">
{{ cellAt(row, c) }}
</td>
</tr>
</tbody>
</table>
<div v-else class="empty">loading…</div>
</template>
42 changes: 42 additions & 0 deletions web-ui/src/views/manage/ChannelsTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup>
import { ref, onMounted } from 'vue'
import { api } from '../../api.js'
import { useManage } from './context.js'

const { data, error, fetchTab, run } = useManage()
const newChannel = ref({ name: '', key: '' })
onMounted(() => fetchTab('channels'))

async function addChannel() {
const body = { name: newChannel.value.name.trim() }
if (newChannel.value.key.trim()) body.key = newChannel.value.key.trim()
if (!body.name) return
await run(api('/channels', { method: 'POST', json: body }), `added ${body.name}`, ['channels'])
if (!error.value) newChannel.value = { name: '', key: '' }
}
function removeChannel(name) {
if (!confirm(`Remove channel ${name}? This clears its radio slot.`)) return
run(api(`/channels/${encodeURIComponent(name)}`, { method: 'DELETE' }), `removed ${name}`, ['channels'])
}
</script>

<template>
<div>
<div class="toolbar">
<input v-model="newChannel.name" placeholder="name (e.g. #bot or MyChan)" style="flex: 1" />
<input v-model="newChannel.key" placeholder="hex key (non-# only)" style="flex: 1" />
<button @click="addChannel">Add channel</button>
</div>
<table>
<thead><tr><th>idx</th><th>name</th><th>secret</th><th></th></tr></thead>
<tbody>
<tr v-for="c in data.channels" :key="c.channel_idx">
<td>{{ c.channel_idx }}</td>
<td>{{ c.name }}</td>
<td class="mono muted">{{ (c.secret_hex || '').slice(0, 12) }}…</td>
<td><button @click="removeChannel(c.name)">remove</button></td>
</tr>
</tbody>
</table>
</div>
</template>
97 changes: 97 additions & 0 deletions web-ui/src/views/manage/CommandsTab.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script setup>
import { onMounted } from 'vue'
import { api } from '../../api.js'
import { useManage } from './context.js'
import SettingRows from './SettingRows.vue'

const {
data, error, notice, fetchTab, reload,
channelNames, ensureChannelNames, loadSettings,
} = useManage()

onMounted(async () => {
await fetchTab('command-config')
await loadSettings()
// the allowed-channels picker needs the channel names
await ensureChannelNames()
})

// Sync the local reactive row from the authoritative PATCH response. The
// inputs are one-way bound (:value / :checked), so when run()/notice
// triggered a re-render the input would snap back to the *stale* local
// value — that was the "cooldown reverts on edit" bug. Updating the row
// from the server's returned values keeps the input showing what was saved.
function applyCommandRow(updated) {
const rows = data.value['command-config']
if (!rows) return
const i = rows.findIndex((r) => r.command === updated.command)
if (i === -1) return
let chans = []
try {
chans = updated.allowed_channels ? JSON.parse(updated.allowed_channels) : []
} catch {
chans = []
}
rows[i] = { ...rows[i], ...updated, _chans: chans }
}
async function patchCommand(cmd, fields) {
error.value = ''
notice.value = ''
try {
const updated = await api(`/command-config/${encodeURIComponent(cmd)}`, {
method: 'PATCH',
json: fields,
})
applyCommandRow(updated)
notice.value = `${cmd}: saved`
} catch (e) {
error.value = e.message
await reload('command-config') // resync to server truth on failure
}
}
function addCommandChannel(row, channel) {
if (!channel || row._chans.includes(channel)) return
patchCommand(row.command, { allowed_channels: [...row._chans, channel] })
}
function removeCommandChannel(row, channel) {
patchCommand(row.command, { allowed_channels: row._chans.filter((c) => c !== channel) })
}
</script>

<template>
<div>
<SettingRows group="commands" />
<table>
<thead>
<tr>
<th>command</th><th>enabled</th><th>allow_dm</th>
<th>dm_only</th><th>cooldown</th>
<th style="width: 140px">allow channel</th>
<th>allowed channels</th>
</tr>
</thead>
<tbody>
<tr v-for="r in data['command-config']" :key="r.command">
<td>{{ r.command }}</td>
<td><input type="checkbox" :checked="!!r.enabled" @change="patchCommand(r.command, { enabled: $event.target.checked })" /></td>
<td><input type="checkbox" :checked="!!r.allow_dm" @change="patchCommand(r.command, { allow_dm: $event.target.checked })" /></td>
<td><input type="checkbox" :checked="!!r.dm_only" @change="patchCommand(r.command, { dm_only: $event.target.checked })" /></td>
<td><input type="number" :value="r.cooldown_seconds" style="width: 64px" @change="patchCommand(r.command, { cooldown_seconds: Number($event.target.value) })" /></td>
<td>
<select @change="addCommandChannel(r, $event.target.value); $event.target.value = ''">
<option value="">+ channel…</option>
<option v-for="c in channelNames" :key="c" :value="c">{{ c }}</option>
</select>
</td>
<td class="chips">
<span v-if="!r._chans.length" class="muted">(any)</span>
<span v-for="c in r._chans" :key="c" class="tag">
{{ c }}
<a href="#" @click.prevent="removeCommandChannel(r, c)" title="remove">×</a>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</template>
Loading
Loading