Files
sub2api/frontend/src/components/admin/monitor/MonitorActionsCell.vue
T
yan9651688 e2e375d694 Make repeated channel-monitor setup safer
Admins often recreate monitors with the same endpoint, model, and request settings. A server-side duplicate keeps the stored API key out of the browser, creates a disabled copy for review, and uses stable operation identity to recover ambiguous retries without creating extra rows.

Constraint: Stored monitor API keys must never be returned to the browser
Constraint: Applying a request template must preserve internal duplicate recovery metadata
Rejected: Rebuild the monitor from list data | list responses only contain a masked API key
Rejected: Copy runtime state and history | a duplicate should start as an unverified configuration
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep duplicated monitors disabled until an administrator reviews and enables them
Tested: Go unit tests for repository, service, and admin handler; integration-tag compile; go vet; golangci-lint v2.9; frontend Vitest, ESLint, typecheck, production build; Playwright duplicate flow
Not-tested: PostgreSQL container integration locally because Docker is unavailable; CI will execute the database-backed suite
2026-07-16 16:30:19 +08:00

66 lines
2.6 KiB
Vue

<template>
<div class="flex items-center gap-1">
<button
@click="$emit('run', row)"
:disabled="running"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400"
>
<Icon name="refresh" size="sm" :class="running ? 'animate-spin' : ''" />
<span class="text-xs">{{ t('admin.channelMonitor.runNow') }}</span>
</button>
<button
data-testid="monitor-duplicate"
:title="duplicateTitle"
:disabled="duplicating || Boolean(row.api_key_decrypt_failed)"
@click="$emit('duplicate', row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-dark-700 dark:hover:text-primary-400"
>
<Icon name="copy" size="sm" />
<span class="text-xs">
{{ duplicating ? t('admin.channelMonitor.duplicating') : t('admin.channelMonitor.duplicate') }}
</span>
</button>
<button
@click="$emit('edit', row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400"
>
<Icon name="edit" size="sm" />
<span class="text-xs">{{ t('common.edit') }}</span>
</button>
<button
@click="$emit('delete', row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400"
>
<Icon name="trash" size="sm" />
<span class="text-xs">{{ t('common.delete') }}</span>
</button>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ChannelMonitor } from '@/api/admin/channelMonitor'
import Icon from '@/components/icons/Icon.vue'
const props = defineProps<{
row: ChannelMonitor
running: boolean
duplicating: boolean
}>()
defineEmits<{
(e: 'run', row: ChannelMonitor): void
(e: 'duplicate', row: ChannelMonitor): void
(e: 'edit', row: ChannelMonitor): void
(e: 'delete', row: ChannelMonitor): void
}>()
const { t } = useI18n()
const duplicateTitle = computed(() => {
if (props.row.api_key_decrypt_failed) return t('admin.channelMonitor.duplicateKeyUnavailable')
if (props.duplicating) return t('admin.channelMonitor.duplicating')
return t('admin.channelMonitor.duplicate')
})
</script>