magpie/frontend/src/components/SubRulesView.vue
pyr0ball 2cc85d8fc5 feat: scaffold Magpie — campaign scheduler + social posting platform
FastAPI backend (SQLite + APScheduler), Vue 3 frontend, MCP server for
Claude integration, and Docker Compose stack. Includes campaign data model
(campaigns → variants → subs), post history, sub rules, and Playwright-based
Reddit posting layer migrated from claude-bridge/reddit-poster.

Also seeds legacy campaigns (6) and sub rules (14) from reddit-poster history.

Closes #1 (scaffold), resolves migration from claude-bridge.
2026-04-21 16:51:33 -07:00

174 lines
6.4 KiB
Vue

<template>
<div>
<div class="page-header">
<h1 class="page-title">Sub / Channel Rules</h1>
<button class="btn btn-primary" @click="showAdd = true">+ Add Sub</button>
</div>
<div class="card" style="padding: 0; overflow: hidden;">
<table class="table">
<thead>
<tr>
<th>Sub / Channel</th>
<th>Platform</th>
<th>Flair</th>
<th>Promo</th>
<th>Rule Warning</th>
<th>Notes</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="r in rules" :key="r.id">
<td style="font-weight: 500;">{{ r.sub }}</td>
<td><span class="badge badge-muted">{{ r.platform }}</span></td>
<td>
<span v-if="r.flair_required">{{ r.flair_to_use ?? '(required, unknown)' }}</span>
<span v-else style="color: var(--color-text-muted);">—</span>
</td>
<td>
<span v-if="r.promo_allowed === null" class="badge badge-muted">unknown</span>
<span v-else-if="r.promo_allowed" class="badge badge-success">allowed</span>
<span v-else class="badge badge-danger">banned</span>
</td>
<td>
<span v-if="r.rule_warning" class="badge badge-warning">yes</span>
<span v-else style="color: var(--color-text-muted);">—</span>
</td>
<td style="color: var(--color-text-muted); max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
{{ r.notes ?? '—' }}
</td>
<td>
<button class="btn btn-ghost btn-sm" @click="startEdit(r)">Edit</button>
</td>
</tr>
<tr v-if="rules.length === 0">
<td colspan="7" class="empty-state">No rules on record.</td>
</tr>
</tbody>
</table>
</div>
<!-- Add/Edit modal -->
<div v-if="showAdd || editing" class="modal-backdrop" @click.self="closeModal">
<div class="modal card" style="width: 480px;">
<h2 style="margin-bottom: var(--spacing-md); font-size: 16px;">
{{ editing ? `Edit r/${editing.sub}` : 'Add Sub / Channel' }}
</h2>
<div v-if="!editing" class="form-group">
<label class="form-label">Subreddit / channel name</label>
<input class="form-input" v-model="form.sub" placeholder="selfhosted" />
</div>
<div v-if="!editing" class="form-group">
<label class="form-label">Platform</label>
<select class="form-select" v-model="form.platform">
<option value="reddit">Reddit</option>
<option value="facebook">Facebook</option>
<option value="discord">Discord</option>
<option value="lemmy">Lemmy</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Flair required?</label>
<select class="form-select" v-model="form.flair_required">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div v-if="form.flair_required" class="form-group">
<label class="form-label">Flair to use</label>
<input class="form-input" v-model="form.flair_to_use" placeholder="Action / DIY / Activism" />
</div>
<div class="form-group">
<label class="form-label">Promo allowed?</label>
<select class="form-select" v-model="form.promo_allowed">
<option :value="null">Unknown</option>
<option :value="true">Yes</option>
<option :value="false">Hard ban — skip</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Shows rule-warning dialog?</label>
<select class="form-select" v-model="form.rule_warning">
<option :value="false">No</option>
<option :value="true">Yes</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Notes</label>
<textarea class="form-textarea" v-model="form.notes" rows="2" placeholder="Any posting quirks..." />
</div>
<div style="display: flex; gap: var(--spacing-sm); justify-content: flex-end;">
<button class="btn btn-ghost" @click="closeModal">Cancel</button>
<button class="btn btn-primary" @click="save" :disabled="!editing && !form.sub">Save</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { api, type SubRules } from '@/services/api'
const rules = ref<SubRules[]>([])
const showAdd = ref(false)
const editing = ref<SubRules | null>(null)
const form = reactive({
sub: '',
platform: 'reddit',
flair_required: false,
flair_to_use: '',
promo_allowed: null as boolean | null,
rule_warning: false,
notes: '',
})
onMounted(async () => {
rules.value = await api.subs.listRules()
})
function startEdit(r: SubRules) {
editing.value = r
Object.assign(form, {
sub: r.sub,
platform: r.platform,
flair_required: !!r.flair_required,
flair_to_use: r.flair_to_use ?? '',
promo_allowed: r.promo_allowed === null ? null : !!r.promo_allowed,
rule_warning: !!r.rule_warning,
notes: r.notes ?? '',
})
}
function closeModal() {
showAdd.value = false
editing.value = null
Object.assign(form, { sub: '', platform: 'reddit', flair_required: false, flair_to_use: '', promo_allowed: null, rule_warning: false, notes: '' })
}
async function save() {
const sub = editing.value ? editing.value.sub : form.sub
const platform = editing.value ? editing.value.platform : form.platform
const updated = await api.subs.upsertRules(sub, {
flair_required: form.flair_required,
flair_to_use: form.flair_to_use || null,
promo_allowed: form.promo_allowed,
rule_warning: form.rule_warning,
notes: form.notes || null,
}, platform)
const idx = rules.value.findIndex(r => r.sub === sub && r.platform === platform)
if (idx !== -1) {
rules.value = [...rules.value.slice(0, idx), updated, ...rules.value.slice(idx + 1)]
} else {
rules.value = [...rules.value, updated]
}
closeModal()
}
</script>
<style scoped>
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
.modal { padding: var(--spacing-lg); }
</style>