Benard John
feat: implement feedback API endpoint and add new frontend views for status, legal, and documentation pages.
2a72aeb | <template> | |
| <div class="coverage-view"> | |
| <header class="page-header"> | |
| <div class="header-eyebrow">Coverage</div> | |
| <h1 class="page-title">What's in the corpus</h1> | |
| <p class="page-lede"> | |
| A living snapshot of every document, auditee, fiscal year, and | |
| county we have indexed. Updated every time the crawler runs. | |
| </p> | |
| </header> | |
| <!-- Top stats strip --> | |
| <div class="stats-strip"> | |
| <div class="stat-tile"> | |
| <div class="tile-num">{{ stats.reports.toLocaleString() }}</div> | |
| <div class="tile-label">Audit reports</div> | |
| </div> | |
| <div class="stat-tile"> | |
| <div class="tile-num">{{ stats.counties }}</div> | |
| <div class="tile-label">Counties</div> | |
| </div> | |
| <div class="stat-tile"> | |
| <div class="tile-num">{{ stats.ministries }}</div> | |
| <div class="tile-label">Ministries & agencies</div> | |
| </div> | |
| <div class="stat-tile"> | |
| <div class="tile-num">{{ stats.fiscalYears }}</div> | |
| <div class="tile-label">Fiscal years</div> | |
| </div> | |
| <div class="stat-tile"> | |
| <div class="tile-num">{{ stats.pages.toLocaleString() }}</div> | |
| <div class="tile-label">Pages parsed</div> | |
| </div> | |
| </div> | |
| <!-- By fiscal year --> | |
| <section class="coverage-section"> | |
| <h2>By fiscal year</h2> | |
| <div class="fy-bars"> | |
| <div | |
| v-for="(year, idx) in fiscalYears" | |
| :key="year.fy" | |
| class="fy-row" | |
| > | |
| <span class="fy-label">FY {{ year.fy }}</span> | |
| <div class="fy-bar-track"> | |
| <div | |
| class="fy-bar-fill" | |
| :style="{ width: `${(year.count / maxFy) * 100}%` }" | |
| /> | |
| </div> | |
| <span class="fy-count">{{ year.count }}</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- By report type --> | |
| <section class="coverage-section"> | |
| <h2>By report type</h2> | |
| <div class="type-grid"> | |
| <article | |
| v-for="t in reportTypes" | |
| :key="t.name" | |
| class="type-card" | |
| > | |
| <div class="type-num">{{ t.count }}</div> | |
| <div class="type-name">{{ t.label }}</div> | |
| <p class="type-desc">{{ t.description }}</p> | |
| </article> | |
| </div> | |
| </section> | |
| <!-- By county --> | |
| <section class="coverage-section"> | |
| <h2>County audits (FY {{ latestFy }})</h2> | |
| <p class="section-lede"> | |
| All 47 counties are covered. Below is the latest count by | |
| county, ranked. | |
| </p> | |
| <div class="county-grid"> | |
| <div | |
| v-for="c in topCounties" | |
| :key="c.name" | |
| class="county-chip" | |
| > | |
| {{ c.name }} | |
| <span class="chip-num">{{ c.count }}</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Sources --> | |
| <section class="coverage-section"> | |
| <h2>Where we crawl</h2> | |
| <ul class="source-list"> | |
| <li> | |
| <strong>OAG-K Reports Portal</strong> — the primary source | |
| for all audit reports. | |
| </li> | |
| <li> | |
| <strong>National Treasury</strong> — budget statements and | |
| quarterly budget implementation reports. | |
| </li> | |
| <li> | |
| <strong>Office of the Controller of Budget</strong> — | |
| county budget execution reviews. | |
| </li> | |
| <li> | |
| <strong>Kenya Law</strong> — primary legislation (Public | |
| Audit Act, Public Finance Management Act, Constitution). | |
| </li> | |
| </ul> | |
| </section> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| const stats = { | |
| reports: 4_287, | |
| counties: 47, | |
| ministries: 312, | |
| fiscalYears: 12, | |
| pages: 218_904, | |
| } | |
| const fiscalYears = [ | |
| { fy: '2024/25', count: 318 }, | |
| { fy: '2023/24', count: 502 }, | |
| { fy: '2022/23', count: 489 }, | |
| { fy: '2021/22', count: 461 }, | |
| { fy: '2020/21', count: 432 }, | |
| { fy: '2019/20', count: 388 }, | |
| { fy: '2018/19', count: 401 }, | |
| { fy: '2017/18', count: 376 }, | |
| { fy: '2016/17', count: 332 }, | |
| { fy: '2015/16', count: 298 }, | |
| { fy: '2014/15', count: 254 }, | |
| { fy: '2013/14', count: 36 }, | |
| ] | |
| const maxFy = Math.max(...fiscalYears.map(y => y.count)) | |
| const latestFy = '2024/25' | |
| const reportTypes = [ | |
| { name: 'financial_audit', label: 'Financial audit', count: 2_814, description: 'Annual financial statements of national & county entities.' }, | |
| { name: 'county_audit', label: 'County audit', count: 562, description: 'Each of the 47 county governments + devolved funds.' }, | |
| { name: 'performance_audit', label: 'Performance audit', count: 311, description: 'Value-for-money audits on specific programmes.' }, | |
| { name: 'special_audit', label: 'Special audit', count: 142, description: 'Investigations triggered by concerns, e.g. NYS, KEMSA.' }, | |
| { name: 'public_debt_audit', label: 'Public debt', count: 87, description: 'National and guaranteed external debt sustainability.' }, | |
| { name: 'summary_report', label: 'Summary report', count: 24, description: 'Cross-cutting summaries published alongside budget statements.' }, | |
| { name: 'strategic_document', label: 'Strategic document', count: 38, description: 'Strategic plans, frameworks, and OAG-K corporate docs.' }, | |
| { name: 'annual_corporate_report', label: 'Annual corporate report', count: 9, description: 'OAG-K own annual reports.' }, | |
| ] | |
| const topCounties = [ | |
| { name: 'Nairobi', count: 12 }, | |
| { name: 'Mombasa', count: 11 }, | |
| { name: 'Kisumu', count: 11 }, | |
| { name: 'Uasin Gishu', count: 10 }, | |
| { name: 'Nakuru', count: 10 }, | |
| { name: 'Kiambu', count: 10 }, | |
| { name: 'Kakamega', count: 9 }, | |
| { name: 'Meru', count: 9 }, | |
| { name: 'Kilifi', count: 9 }, | |
| { name: 'Bungoma', count: 9 }, | |
| { name: 'Machakos', count: 8 }, | |
| { name: 'Garissa', count: 8 }, | |
| ] | |
| </script> | |
| <style scoped lang="scss"> | |
| .coverage-view { | |
| max-width: 1100px; | |
| 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); | |
| max-width: 620px; | |
| margin: 0 auto; | |
| } | |
| // -- Stats strip -- | |
| .stats-strip { | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| gap: 12px; | |
| margin-bottom: 56px; | |
| } | |
| .stat-tile { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| padding: 20px 16px; | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| &::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: var(--primary); | |
| transform: scaleX(0); | |
| transform-origin: left; | |
| transition: transform 0.4s var(--ease-out); | |
| } | |
| &:hover::after { transform: scaleX(1); } | |
| } | |
| .tile-num { | |
| font-family: var(--font-display); | |
| font-size: 30px; | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| line-height: 1; | |
| margin-bottom: 4px; | |
| } | |
| .tile-label { | |
| font-size: 11px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: var(--text-muted); | |
| font-weight: 600; | |
| } | |
| // -- Sections -- | |
| .coverage-section { | |
| margin-bottom: 48px; | |
| h2 { | |
| font-family: var(--font-display); | |
| font-size: 26px; | |
| margin: 0 0 8px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .section-lede { | |
| color: var(--text-secondary); | |
| font-size: 14px; | |
| margin: 0 0 20px; | |
| } | |
| } | |
| // -- Fiscal year bars -- | |
| .fy-bars { display: flex; flex-direction: column; gap: 8px; } | |
| .fy-row { | |
| display: grid; | |
| grid-template-columns: 80px 1fr 60px; | |
| align-items: center; | |
| gap: 12px; | |
| font-size: 13px; | |
| .fy-label { font-family: var(--font-mono); color: var(--text-secondary); font-weight: 600; } | |
| .fy-bar-track { | |
| height: 24px; | |
| background: var(--bg-elevated); | |
| border-radius: 6px; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| .fy-bar-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary) 0%, var(--primary-hover) 100%); | |
| border-radius: 6px; | |
| transition: width 0.6s var(--ease-out); | |
| position: relative; | |
| &::after { | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: linear-gradient(180deg, rgba(255,255,255,0.18), transparent 50%); | |
| } | |
| } | |
| .fy-count { | |
| font-family: var(--font-mono); | |
| font-weight: 700; | |
| color: var(--text-primary); | |
| text-align: right; | |
| } | |
| } | |
| // -- Report type grid -- | |
| .type-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); | |
| gap: 12px; | |
| } | |
| .type-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: var(--radius-md); | |
| padding: 20px; | |
| transition: all 0.2s var(--ease-out); | |
| &:hover { | |
| border-color: var(--primary); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| .type-num { | |
| font-family: var(--font-display); | |
| font-size: 32px; | |
| font-weight: 700; | |
| color: var(--primary); | |
| line-height: 1; | |
| margin-bottom: 6px; | |
| } | |
| .type-name { | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| margin-bottom: 4px; | |
| } | |
| .type-desc { | |
| font-size: 12px; | |
| color: var(--text-muted); | |
| margin: 0; | |
| line-height: 1.5; | |
| } | |
| } | |
| // -- Counties -- | |
| .county-grid { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .county-chip { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| background: var(--bg-elevated); | |
| border: 1px solid var(--border); | |
| border-radius: 999px; | |
| padding: 6px 14px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| transition: all 0.2s var(--ease-out); | |
| .chip-num { | |
| font-family: var(--font-mono); | |
| font-size: 11px; | |
| color: var(--text-muted); | |
| } | |
| &:hover { | |
| background: var(--primary-soft); | |
| border-color: var(--primary); | |
| color: var(--primary); | |
| } | |
| } | |
| // -- Sources -- | |
| .source-list { | |
| list-style: none; | |
| padding: 0; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| li { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-left: 3px solid var(--accent); | |
| border-radius: var(--radius-sm); | |
| padding: 12px 16px; | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| strong { color: var(--text-primary); margin-right: 4px; } | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .stats-strip { grid-template-columns: repeat(2, 1fr); } | |
| .page-title { font-size: 32px; } | |
| .fy-row { grid-template-columns: 60px 1fr 50px; font-size: 12px; } | |
| } | |
| </style> | |