BuglistView.vue 3.08 KB
<template>
  <div class="bug-view">
    <div class="filters">
      <label>Status:
        <select v-model="filterStatus" @change="reload">
          <option value="">all</option>
          <option>open</option><option>fixing</option>
          <option>fixed</option><option>verified</option>
          <option>wontfix</option><option>duplicate</option>
        </select>
      </label>
      <span class="total">Total: {{ bugs.length }}</span>
      <button class="btn" @click="reload">Refresh</button>
    </div>

    <table v-if="bugs.length">
      <thead>
        <tr>
          <th>ID</th>
          <th>Status</th>
          <th>Unit</th>
          <th>Severity</th>
          <th>Category</th>
          <th>Title</th>
          <th>Updated</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="b in bugs" :key="b.id" :class="b.status">
          <td class="id">{{ b.id }}</td>
          <td><span :class="['badge', b.status]">{{ b.status }}</span></td>
          <td>{{ b.unit_id || '-' }}</td>
          <td>{{ b.severity || '-' }}</td>
          <td>{{ b.category || 'unknown' }}</td>
          <td class="title">{{ b.title }}</td>
          <td class="ts">{{ formatTs(b.last_updated_at || b.discovered_at) }}</td>
        </tr>
      </tbody>
    </table>
    <p v-else class="empty">no bugs (or none match filter)</p>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({ pid: { type: String, required: true } });

const bugs = ref([]);
const filterStatus = ref('');

async function reload() {
  const qs = filterStatus.value ? `?status=${filterStatus.value}` : '';
  const r = await fetch(`/api/devloop/projects/${props.pid}/bugs${qs}`);
  if (!r.ok) { bugs.value = []; return; }
  const j = await r.json();
  bugs.value = j.bugs || [];
}

function formatTs(s) {
  if (!s) return '-';
  try { return new Date(s).toLocaleString(); } catch { return s; }
}

onMounted(reload);
</script>

<style scoped>
.bug-view {}
.filters { display:flex; align-items:center; gap:14px; margin-bottom:12px; font-size:13px; }
.filters select { padding:4px 8px; }
.filters .total { color:#666; }
.btn { background:#222; color:#fff; border:none; border-radius:4px; padding:4px 12px; cursor:pointer; font-size:12px; }
table { width:100%; border-collapse:collapse; font-size:13px; }
th, td { padding:8px 10px; text-align:left; border-bottom:1px solid #eee; }
th { background:#fafafb; color:#666; font-weight:500; font-size:12px; }
tr:hover { background:#fafafa; }
.id { font-family: ui-monospace, monospace; color:#555; font-size:11px; }
.title { color:#222; }
.ts { color:#888; font-size:11px; }
.badge {
  display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; text-transform:uppercase;
}
.badge.open      { background:#fef0c8; color:#8d6708; }
.badge.fixing    { background:#cce5ff; color:#004085; }
.badge.fixed     { background:#d4edda; color:#155724; }
.badge.verified  { background:#a3e0b8; color:#0a4a1d; }
.badge.wontfix   { background:#ddd;    color:#444; }
.badge.duplicate { background:#eee;    color:#666; }
.empty { padding:30px; text-align:center; color:#888; }
</style>