Skip to content

feat: add admin presenter overview and current-state endpoints#64

Merged
toddeTV merged 5 commits into
mainfrom
feat/presenter-admin-routes
May 3, 2026
Merged

feat: add admin presenter overview and current-state endpoints#64
toddeTV merged 5 commits into
mainfrom
feat/presenter-admin-routes

Conversation

@toddeTV
Copy link
Copy Markdown
Owner

@toddeTV toddeTV commented May 3, 2026

Summary by CodeRabbit

  • New Features

    • Added admin presenter endpoints to fetch quiz overviews and live active-question state (includes per-option counts/percentages and an explicit "no active question" response).
  • Documentation

    • Clarified API auth accepts JWT or static admin token.
    • Added docs and examples for the new presenter endpoints.
  • Bug Fixes / Validation

    • Enforced case-insensitive uniqueness for answer option English texts on question creation.

@toddeTV toddeTV self-assigned this May 3, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

Adds presenter-facing types, utilities, and admin API endpoints to expose quiz overview and live polling state; centralizes per-option result aggregation; enforces unique English answer-option labels on question creation; and updates API docs to describe new endpoints and bearer auth options.

Changes

Presenter API and State Management

Layer / File(s) Summary
Data Shape
app/types.ts
Adds five exported interfaces: PresenterQuestionOverviewItem, PresenterQuestionsOverview, PresenterCurrentStateAnswerOption, PresenterCurrentQuestion, and PresenterCurrentState.
Result Aggregation Helper
server/utils/quiz-results.ts, server/utils/storage.ts
Introduces buildQuestionOptionResults(question, answerList) and replaces inline per-option aggregation in getResultsForQuestion() with this helper.
Presenter State Logic
server/utils/presenter.ts
Adds getPresenterQuestionsOverview() and getPresenterCurrentState(); includes helpers getPercent() and findActiveQuestion(); computes totals, option counts, and percentages; returns a null currentQuestion when none is active.
API Endpoints
server/api/admin/presenter/overview.get.ts, server/api/admin/presenter/current-state.get.ts
Adds two admin GET handlers that call verifyAdmin(event) then return getPresenterQuestionsOverview() and getPresenterCurrentState() respectively.
Input Validation
server/api/questions/create.post.ts
Enforces case-insensitive uniqueness of answer_options[].text.en, returning 400 on duplicates.
Documentation
docs/api.md
Documents bearer Authorization formats for /api/auth/verify, adds docs and examples for GET /api/admin/presenter/overview and GET /api/admin/presenter/current-state (including no-active-question variant), and documents the answer_options[].text.en uniqueness requirement for POST /api/questions/create.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin Client
    participant Handler as API Handler
    participant Auth as verifyAdmin()
    participant Presenter as Presenter Utility
    participant Storage as Storage/DB

    Admin->>Handler: GET /api/admin/presenter/current-state
    Handler->>Auth: verifyAdmin(event)
    Auth-->>Handler: authorized
    Handler->>Presenter: getPresenterCurrentState()
    Presenter->>Storage: getQuestions()
    Storage-->>Presenter: Question[]
    Presenter->>Storage: getPeers()
    Storage-->>Presenter: peer count
    Presenter->>Presenter: findActiveQuestion()
    alt active question found
      Presenter->>Storage: getAnswersForQuestion(activeQuestionId)
      Storage-->>Presenter: Answer[]
      Presenter->>Presenter: buildQuestionOptionResults()
      Presenter-->>Handler: PresenterCurrentState (hasActiveQuestion: true)
    else no active question
      Presenter-->>Handler: PresenterCurrentState (hasActiveQuestion: false)
    end
    Handler-->>Admin: PresenterCurrentState JSON
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly Related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: adding two new admin presenter API endpoints (overview and current-state) that are the focal point of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/utils/quiz-results.ts`:
- Around line 11-23: Results accumulator should be a null-prototype object and
must use a stable per-option key (not raw display text) to avoid collapsing
duplicate English labels; change the results init to Object.create(null),
iterate question.answer_options and assign each bucket under a uniqueKey (e.g.,
option.id ?? `idx:${i}`) storing { label: option.text.en, emoji: option.emoji,
count: 0 }, and when tallying answers (from answerList) resolve the
corresponding uniqueKey (prefer a selected_option id if available, otherwise
match by text+emoji or fallback to first matching option) before incrementing
results[uniqueKey].count so duplicate labels do not collide.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: d48b2185-b12b-449f-92b0-c2698d99eb2f

📥 Commits

Reviewing files that changed from the base of the PR and between 99c141a and d23d0d3.

📒 Files selected for processing (7)
  • app/types.ts
  • docs/api.md
  • server/api/admin/presenter/current-state.get.ts
  • server/api/admin/presenter/overview.get.ts
  • server/utils/presenter.ts
  • server/utils/quiz-results.ts
  • server/utils/storage.ts

Comment thread server/utils/quiz-results.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
server/utils/quiz-results.ts (1)

16-18: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid hard-failing presenter state aggregation on duplicate legacy labels

Line 17 throws and can take down /api/admin/presenter/current-state polling if older data contains duplicate option labels. Prefer a non-fatal merge path so presenter views stay available.

Suggested resilient fallback
 export function buildQuestionOptionResults(question: Question, answerList: Answer[]): Results['results'] {
   const results = Object.create(null) as Results['results']
-  const normalizedOptionLabels = new Set<string>()
+  const normalizedOptionLabels = new Set<string>()
+  const normalizedToResultKey = new Map<string, string>()

   for (const option of question.answer_options) {
     const resultKey = option.text.en
     const normalizedResultKey = resultKey.toLowerCase()

-    if (normalizedOptionLabels.has(normalizedResultKey)) {
-      throw new Error(`Duplicate answer option label is not supported: "${resultKey}"`)
+    if (normalizedOptionLabels.has(normalizedResultKey)) {
+      // Legacy/malformed data fallback: merge duplicates under first bucket
+      continue
     }

     normalizedOptionLabels.add(normalizedResultKey)
+    normalizedToResultKey.set(normalizedResultKey, resultKey)

     results[resultKey] = {
       count: 0,
       emoji: option.emoji,
     }
   }

   for (const answer of answerList) {
-    const selectedAnswer = answer.selected_answer.en
+    const selectedAnswer = answer.selected_answer.en.toLowerCase()
+    const resultKey = normalizedToResultKey.get(selectedAnswer)

-    if (Object.prototype.hasOwnProperty.call(results, selectedAnswer)) {
-      results[selectedAnswer]!.count += 1
+    if (resultKey && Object.prototype.hasOwnProperty.call(results, resultKey)) {
+      results[resultKey]!.count += 1
     }
   }

   return results
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/utils/quiz-results.ts` around lines 16 - 18, The current check throws
an Error when normalizedOptionLabels.has(normalizedResultKey), which crashes
presenter polling; change this to a non‑fatal merge: instead of throwing in the
duplicate branch for normalizedResultKey/resultKey, log a warning and generate a
unique fallback key (e.g., append a numeric suffix or “__dupN” to
normalizedResultKey) until it is not present in normalizedOptionLabels, then
continue processing and insert that remapped key into normalizedOptionLabels and
the same aggregated state. Ensure the warning includes the original resultKey
and the generated fallback key so debugging remains possible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@server/utils/quiz-results.ts`:
- Around line 16-18: The current check throws an Error when
normalizedOptionLabels.has(normalizedResultKey), which crashes presenter
polling; change this to a non‑fatal merge: instead of throwing in the duplicate
branch for normalizedResultKey/resultKey, log a warning and generate a unique
fallback key (e.g., append a numeric suffix or “__dupN” to normalizedResultKey)
until it is not present in normalizedOptionLabels, then continue processing and
insert that remapped key into normalizedOptionLabels and the same aggregated
state. Ensure the warning includes the original resultKey and the generated
fallback key so debugging remains possible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ec7c9757-1957-4ad3-b37a-546c9a363ee4

📥 Commits

Reviewing files that changed from the base of the PR and between d23d0d3 and 5ef3ed8.

📒 Files selected for processing (3)
  • docs/api.md
  • server/api/questions/create.post.ts
  • server/utils/quiz-results.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/api.md

@toddeTV toddeTV merged commit bfb4567 into main May 3, 2026
5 checks passed
@toddeTV toddeTV deleted the feat/presenter-admin-routes branch May 3, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant