Benard John
feat: implement feedback API endpoint and add new frontend views for status, legal, and documentation pages.
2a72aeb | <template> | |
| <div class="status-view"> | |
| <header class="page-header"> | |
| <div class="header-eyebrow">Status</div> | |
| <h1 class="page-title">All systems</h1> | |
| <p class="page-lede"> | |
| Live status of the Ukweli platform. Refreshes every 30 seconds. | |
| </p> | |
| <div class="overall" :data-state="overallState"> | |
| <span class="overall-dot" /> | |
| <span class="overall-label"> | |
| {{ overallState === 'ok' ? 'All systems operational' : 'Degraded — some systems affected' }} | |
| </span> | |
| </div> | |
| </header> | |
| <section class="status-section"> | |
| <h2>Services</h2> | |
| <div class="service-list"> | |
| <div | |
| v-for="svc in services" | |
| :key="svc.name" | |
| class="service-row" | |
| :data-state="svc.state" | |
| > | |
| <div class="svc-meta"> | |
| <span class="svc-dot" /> | |
| <div> | |
| <div class="svc-name">{{ svc.name }}</div> | |
| <div class="svc-desc">{{ svc.description }}</div> | |
| </div> | |
| </div> | |
| <div class="svc-status"> | |
| <span class="svc-uptime">{{ svc.uptime }}% uptime · 90d</span> | |
| <span class="svc-state">{{ stateLabel(svc.state) }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="status-section"> | |
| <h2>Crawl freshness</h2> | |
| <div class="crawl-grid"> | |
| <div class="crawl-tile"> | |
| <div class="crawl-num">318</div> | |
| <div class="crawl-label">New reports this month</div> | |
| </div> | |
| <div class="crawl-tile"> | |
| <div class="crawl-num">12 min</div> | |
| <div class="crawl-label">Median crawl-to-index</div> | |
| </div> | |
| <div class="crawl-tile"> | |
| <div class="crawl-num">3 days</div> | |
| <div class="crawl-label">Last full re-crawl</div> | |
| </div> | |
| <div class="crawl-tile"> | |
| <div class="crawl-num">99.7%</div> | |
| <div class="crawl-label">Coverage vs OAG-K catalogue</div> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="status-section"> | |
| <h2>Recent incidents</h2> | |
| <div class="incident-list"> | |
| <div v-for="inc in incidents" :key="inc.id" class="incident"> | |
| <div class="incident-head"> | |
| <n-tag :type="inc.state === 'resolved' ? 'success' : 'warning'" size="small"> | |
| {{ inc.state }} | |
| </n-tag> | |
| <span class="incident-date">{{ inc.date }}</span> | |
| </div> | |
| <h3 class="incident-title">{{ inc.title }}</h3> | |
| <p class="incident-body">{{ inc.body }}</p> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| const overallState = 'ok' | |
| const services = [ | |
| { name: 'API', state: 'ok', uptime: 99.94, description: 'FastAPI service for queries, streaming, and feedback.' }, | |
| { name: 'Postgres', state: 'ok', uptime: 99.99, description: 'Primary datastore (Neon, sa-east-1).' }, | |
| { name: 'Qdrant', state: 'ok', uptime: 99.91, description: 'Vector database for embedding search.' }, | |
| { name: 'Redis', state: 'degraded', uptime: 99.62, description: 'Rate-limit + OTP cache (Upstash).' }, | |
| { name: 'Crawler', state: 'ok', uptime: 99.78, description: 'Scrapy cluster refreshing the corpus.' }, | |
| { name: 'LLM gateway', state: 'ok', uptime: 99.86, description: 'HuggingFace Inference API + Llama fallback.' }, | |
| { name: 'Embeddings', state: 'ok', uptime: 99.95, description: 'BGE-M3 encoder, GPU pool for backfills.' }, | |
| ] | |
| function stateLabel(s: string) { | |
| return s === 'ok' ? 'Operational' : s === 'degraded' ? 'Degraded' : 'Down' | |
| } | |
| const incidents = [ | |
| { | |
| id: 1, | |
| state: 'monitoring', | |
| date: '2026-06-02', | |
| title: 'Elevated Redis latency', | |
| body: 'Upstash reported elevated p99 latency in the sa-east-1 region. OTP delivery is slower than usual; sign-in still works but codes may take up to 30 seconds to arrive.', | |
| }, | |
| { | |
| id: 2, | |
| state: 'resolved', | |
| date: '2026-05-18', | |
| title: 'Brief Qdrant unavailability', | |
| body: 'A Qdrant Cloud control-plane incident caused ~12 minutes of /query 5xx errors. Recovered automatically; no data loss.', | |
| }, | |
| { | |
| id: 3, | |
| state: 'resolved', | |
| date: '2026-04-29', | |
| title: 'OAG-K site schema change', | |
| body: 'The OAG-K portal changed its report listing layout. Crawler was updated within 4 hours; no user-facing impact.', | |
| }, | |
| ] | |
| </script> | |
| <style scoped lang="scss"> | |
| .status-view { | |
| max-width: 980px; | |
| margin: 0 auto; | |
| padding-bottom: 64px; | |
| } | |
| .page-header { | |
| text-align: center; | |
| margin-bottom: 48px; | |
| position: relative; | |
| padding-top: 24px; | |
| &::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 80px; | |
| height: 4px; | |
| border-radius: 2px; | |
| background: linear-gradient( | |
| 90deg, | |
| var(--ke-black) 0% 40%, | |
| var(--ke-white) 40% 50%, | |
| var(--ke-red) 50% 90%, | |
| var(--ke-green) 90% 100% | |
| ); | |
| } | |
| } | |
| .header-eyebrow { | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.18em; | |
| color: var(--primary); | |
| margin-bottom: 12px; | |
| font-family: var(--font-mono); | |
| } | |
| .page-title { | |
| font-family: var(--font-display); | |
| font-size: 44px; | |
| font-weight: 700; | |
| line-height: 1.1; | |
| margin: 0 0 12px; | |
| } | |
| .page-lede { | |
| font-size: 16px; | |
| color: var(--text-secondary); | |
| margin: 0 0 24px; | |
| } | |
| .overall { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 10px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 999px; | |
| padding: 8px 20px; | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| .overall-dot { | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| box-shadow: 0 0 0 0 currentColor; | |
| } | |
| &[data-state='ok'] { | |
| border-color: var(--primary); | |
| .overall-dot { | |
| background: var(--primary); | |
| animation: pulseGlow 2.4s var(--ease-out) infinite; | |
| } | |
| } | |
| &[data-state='degraded'] { | |
| border-color: var(--warning); | |
| .overall-dot { background: var(--warning); } | |
| } | |
| } | |
| .status-section { | |
| margin-bottom: 48px; | |
| h2 { | |
| font-family: var(--font-display); | |
| font-size: 22px; | |
| margin: 0 0 16px; | |
| padding-bottom: 8px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| } | |
| .service-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .service-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| padding: 14px 18px; | |
| transition: all 0.2s var(--ease-out); | |
| &:hover { border-color: var(--primary); } | |
| &[data-state='degraded'] { border-left: 3px solid var(--warning); } | |
| &[data-state='down'] { border-left: 3px solid var(--error); } | |
| &[data-state='ok'] { border-left: 3px solid var(--primary); } | |
| } | |
| .svc-meta { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .svc-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: var(--text-muted); | |
| flex-shrink: 0; | |
| } | |
| [data-state='ok'] .svc-dot { background: var(--primary); } | |
| [data-state='degraded'] .svc-dot { background: var(--warning); } | |
| [data-state='down'] .svc-dot { background: var(--error); animation: pulseGlow 1.5s infinite; } | |
| .svc-name { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| .svc-desc { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| margin-top: 2px; | |
| } | |
| .svc-status { | |
| text-align: right; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| .svc-uptime { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| font-family: var(--font-mono); | |
| } | |
| .svc-state { | |
| font-size: 12px; | |
| font-weight: 700; | |
| color: var(--text-secondary); | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| } | |
| } | |
| [data-state='ok'] .svc-state { color: var(--primary); } | |
| [data-state='degraded'] .svc-state { color: var(--warning); } | |
| [data-state='down'] .svc-state { color: var(--error); } | |
| .crawl-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 12px; | |
| } | |
| .crawl-tile { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| padding: 20px 16px; | |
| text-align: center; | |
| .crawl-num { | |
| font-family: var(--font-display); | |
| font-size: 26px; | |
| font-weight: 700; | |
| color: var(--accent); | |
| line-height: 1; | |
| margin-bottom: 6px; | |
| } | |
| .crawl-label { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| font-weight: 600; | |
| } | |
| } | |
| .incident-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .incident { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| padding: 18px 20px; | |
| .incident-head { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 8px; | |
| } | |
| .incident-date { | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| font-family: var(--font-mono); | |
| } | |
| .incident-title { | |
| font-size: 16px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin: 0 0 4px; | |
| } | |
| .incident-body { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| margin: 0; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .page-title { font-size: 32px; } | |
| .service-row { flex-direction: column; align-items: flex-start; gap: 8px; } | |
| .svc-status { text-align: left; flex-direction: row; gap: 12px; } | |
| .crawl-grid { grid-template-columns: repeat(2, 1fr); } | |
| } | |
| </style> | |