UnitDrawer.vue 5.05 KB
<template>
  <div class="drawer-backdrop" @click.self="$emit('close')">
    <aside class="drawer">
      <header>
        <h2>{{ unitId }}</h2>
        <button class="x" @click="$emit('close')">×</button>
      </header>

      <div v-if="loading" class="loading">loading…</div>
      <div v-else-if="err" class="err">{{ err }}</div>
      <div v-else class="body">
        <section>
          <h3>Metadata</h3>
          <div class="meta">
            <div><label>status</label><span :class="['badge', unit?.status]">{{ unit?.status || '?' }}</span></div>
            <div><label>type</label><span>{{ unit?.type || unit?.kind || '?' }}</span></div>
            <div><label>retries</label><span>{{ unit?.retries ?? 0 }}/{{ unit?.retry_budget ?? '?' }}</span></div>
            <div><label>contract</label><span class="hash">{{ unit?.contract_hash || '?' }}</span></div>
            <div><label>depends_on</label><span>{{ (unit?.depends_on || []).join(', ') || '∅' }}</span></div>
          </div>
        </section>

        <section>
          <h3>Recent runs ({{ runs.length }})</h3>
          <ul class="runs">
            <li v-for="r in runs" :key="r.run_id" :class="['run', r.result?.ok ? 'ok' : 'fail']">
              <div class="row">
                <span class="rid">{{ r.run_id }}</span>
                <span class="dot" :class="{ok: r.result?.ok, fail: r.result?.ok === false, crashed: r.result?.crashed }" />
                <span class="sum">
                  {{ r.result ? `${r.result.summary?.passed || 0}/${r.result.summary?.total || 0}` : 'no result' }}
                </span>
              </div>
              <div v-for="(f, i) in (r.result?.failures || [])" :key="i" class="failure">
                <code>{{ f.test || f.case_id }}</code>: {{ f.hint || f.stack?.slice(0, 100) || 'no hint' }}
              </div>
            </li>
            <li v-if="!runs.length" class="empty">no runs yet</li>
          </ul>
        </section>

        <section v-if="coderLog">
          <h3>coder.log (tail)</h3>
          <pre class="coder-log">{{ coderLog }}</pre>
        </section>
      </div>
    </aside>
  </div>
</template>

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

const loading = ref(true);
const err = ref('');
const unit = ref(null);
const runs = ref([]);
const coderLog = ref('');

onMounted(async () => {
  try {
    const r = await fetch(`/api/devloop/projects/${props.pid}/units/${props.unitId}`);
    if (!r.ok) { err.value = `HTTP ${r.status}`; return; }
    const j = await r.json();
    unit.value = j.unit;
    runs.value = j.runs || [];
    coderLog.value = j.coder_log_tail || '';
  } catch (e) {
    err.value = e.message;
  } finally {
    loading.value = false;
  }
});
</script>

<style scoped>
.drawer-backdrop {
  position:fixed; inset:0; background:rgba(0,0,0,.35); z-index:900;
  display:flex; justify-content:flex-end;
}
.drawer {
  background:#fff; width:520px; max-width:95vw; height:100vh; overflow-y:auto;
  padding:20px 24px; box-shadow:-8px 0 32px rgba(0,0,0,.15);
}
header { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
h2 { margin:0; font-family: ui-monospace, monospace; }
.x { background:none; border:none; font-size:24px; cursor:pointer; color:#888; }
section { margin:18px 0; }
h3 { font-size:13px; color:#666; margin:0 0 8px; text-transform:uppercase; }
.loading, .err { padding:20px; text-align:center; color:#888; }
.err { color:#c00; }
.meta { display:grid; grid-template-columns: 110px 1fr; gap:6px 12px; font-size:13px; }
.meta label { color:#888; }
.meta .hash { font-family: ui-monospace, monospace; font-size:11px; color:#555; }
.badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; text-transform:uppercase; background:#eee; }
.badge.passed  { background:#d4edda; color:#155724; }
.badge.failed  { background:#f8d7da; color:#721c24; }
.badge.coding  { background:#fff3cd; color:#856404; }
.badge.testing { background:#cce5ff; color:#004085; }
.badge.pending { background:#eee;    color:#666; }
.badge.blocked { background:#d6d8db; color:#383d41; }
.runs { list-style:none; padding:0; margin:0; }
.run { border:1px solid #eee; border-radius:4px; padding:8px 10px; margin-bottom:8px; }
.run.ok { background:#fafefa; }
.run.fail { background:#fef9f9; }
.run .row { display:flex; align-items:center; gap:10px; }
.rid { font-family: ui-monospace, monospace; font-size:12px; color:#555; }
.dot { width:8px; height:8px; border-radius:50%; background:#ccc; }
.dot.ok { background:#0a6; }
.dot.fail { background:#c00; }
.dot.crashed { background:#822; }
.sum { color:#666; font-size:12px; }
.failure { margin-top:4px; padding:4px 8px; background:#fff8f8; border-left:2px solid #c00; font-size:11px; color:#444; }
.failure code { background:#eee; padding:1px 4px; border-radius:2px; font-size:11px; }
.coder-log { background:#0f1115; color:#e6e6e6; padding:10px; border-radius:4px; font-size:11px; max-height:240px; overflow-y:auto; }
.empty { color:#888; padding:10px; }
</style>