U of I × MindRouter App Gallery

Classroom Utilization Explorer

Administration Built with Goose v1.50.0 Critic status: shipped

Classroom Utilization Explorer

Where and when is teaching space actually used?

Load a semester's section schedule and instantly see which rooms sit empty, which are overbooked, and which classes are in rooms far too big — the evidence space-planning decisions need.

Classroom Utilization Explorer running on a desktop screen, where and when is teaching space actually used? Desktop · click to enlarge
Classroom Utilization Explorer running on a phone, where and when is teaching space actually used?
Phone · click to enlarge

What it is ✎ blurb written by the AI critic

See which classrooms sit empty, which are double-booked and which classes are in rooms far too big. It opens with a clearly labeled synthetic sample term and can load your own schedule CSV, with column mapping and validation done entirely in your browser. Set the teaching week and targets, then explore a week-by-hour heatmap, a sortable rooms table, daily schedules per building, a find-a-room tool and a printable summary report.

Try this

  1. Open Schedule, pick Palouse Hall and Tuesday, then click a conflict under 'Conflicts on this day' to highlight the two double-booked sections.
  2. In Find a room, ask for 100 seats on TR at 1:00 PM for 75 minutes, then click 'View in schedule' on the best fit.
  3. On Import CSV, click 'Try an example CSV' and then 'Load data'. Messy day codes and times are normalized, a TBA row is counted as unscheduled and two bad rows are rejected.

Highlights ✎ written by the AI critic

  • Week × hour heatmap of rooms in use, filterable by building and room type; clicking a cell opens that day's schedule
  • Rooms table comparing hours used with hours available, with utilization and seat-fill bars against adjustable targets and flags for unused, over-capacity, too-big and double-booked rooms
  • Daily schedule per building that puts double-booked sections on separate red lanes and lists every conflict
  • Find a room: enter seats, meeting pattern, start time and duration to get free rooms ranked by best fit
  • CSV import with automatic column mapping, day and time normalization (M W F, TuTh, 0930, 12:50 p.m.) and a validation report, plus a one-click print-ready summary

Who it's for

Registrar and space-planning staff, facilities, department schedulers

Features

  • CSV import with column mapping and validation (days like MWF/TR, 12/24-hour times)
  • Week × hour heatmap of occupied rooms campus-wide, filterable by building and room type
  • Room-by-room utilization table: hours used vs available, seat-fill % (enrollment ÷ capacity), with sorting and targets
  • Gantt-style daily schedule per building with conflict (double-booking) detection
  • 'Find a room' tool: required seats + time slot → free rooms that fit best
  • Summary cards and a one-click PDF-ready summary report

Data & sources

  • No external downloads. data/raw/SOURCES.md, prepared and checked by the data-prep workflow (verdict ready-after-repair), explains that the app runs on the user's own schedule CSV.
  • Synthetic sample for a fictional term, generated by the harness-written scripts/generate_sample.py (fixed seed 20260913) into data/embed/sample_schedule.json. It is illustrative, not real University of Idaho data.
Full data provenance ✎ written by the AI reviewer

Data sources: Classroom Utilization Explorer (room-utilization)

Retrieval / preparation date: 2026-09-13

Summary

This app uses no external data, so nothing was downloaded. This directory holds no raw data files, only this note.

The app works on a section-schedule CSV that the user supplies. The built-in sample schedule is synthetic. It describes a fictional term and is generated by the app's own script(s) in scripts/, which write their output to data/embed/. No real University of Idaho schedule, enrollment, room-inventory or capacity data was collected, requested or used.

Files in data/raw/

FileSource URLPublisherContentsRecordsLicense
SOURCES.mdn/a (written locally)n/aThis provenance noten/an/a

No other files. No HTTP requests were made, so there is no URL to re-fetch.

Requirements for the synthetic sample (for the app build)

  • Derived/synthetic, not downloaded. The sample must be made by a reproducible script in scripts/ (Python 3 standard library only, fixed random seed so rebuilds match) and written to data/embed/. Nothing in the sample may be presented as real data.

  • Fictional term. Give the term a name that is clearly not real, for example "Sample Term (fictional)", and never a real U of I semester label presented as fact.

  • Building names. SPEC.md asks for "plausible U of I-style" building names, so names in the style of a campus hall are fine (for example "North Science Hall"). Everything attached to them is invented: room numbers, room types, capacities, courses, sections, meeting times and enrollments. Never present them as the real room inventory or real enrollment of any U of I building. If a name resembles a real campus building, the UI must still say the data about it is invented.

  • Required UI labelling. Wherever the sample is loaded, including summary cards, charts, tables and the PDF-ready report, show a visible marker such as "Illustrative sample, synthetic data for a fictional term, not real U of I enrollment or room data". The "Data & sources" panel must say the same. Once a user imports their own CSV, the marker should switch to show the user's file name.

  • License of the sample. It is generated for this app and contains no third-party data, so no outside license or terms apply.

Expected input schema (from SPEC.md; reference for the importer and generator)

SPEC.md lists these logical columns. The importer should let the user map their own header names to these fields (auto-suggest by case-insensitive header match, e.g. Cap/Room Cap/Capacity → room capacity, Enrl/Enrolled → enrollment, Bldg → building).

FieldMeaningType / format notes
courseCourse code, e.g. ABC 101string
sectionSection identifier, e.g. 01, L02string (keep leading zeros; do not parse as a number)
daysMeeting daysConventional US registrar single-letter codes: M=Mon, T=Tue, W=Wed, R=Thu, F=Fri, S=Sat, U=Sun (e.g. MWF, TR, MW). Normalize before validating: make it upper-case and strip spaces/commas/slashes (M W F, M,W,F → MWF), and map the two-letter forms Tu→T, Th→R, Sa→S, Su→U. Flag any other letter as invalid rather than guessing. Blank, TBA, ARR (arranged) or online markers mean no scheduled meeting. Exclude those rows from the room-time grid, heatmap and conflict checks, but count them as "unscheduled".
startMeeting start time12-hour (9:30 AM, 9:30am, 9:30 a.m.) or 24-hour (09:30, 9:30, 0930). Local campus time, no date part. 12-hour edge cases: 12:00 PM = 12:00 (noon), 12:30 AM = 00:30. A bare H:MM with no AM/PM is 24-hour.
endMeeting end timesame formats as start. end must be later than start on the same day; flag the row as invalid otherwise.
buildingBuilding name or codestring (trim whitespace; blank → treat as unplaced, exclude from room views)
roomRoom numberstring (e.g. 210, B12; keep as text). A room's identity is building + room.
enrollmentEnrolled students in the sectionnon-negative integer
room capacitySeats in the roompositive integer. Seat-fill % = enrollment ÷ capacity. Over 100% means the section is over capacity. If one room shows different capacities on different rows, flag it and use the largest.

The optional "room type" used by the heatmap filter (e.g. lecture, lab, seminar) is not in the SPEC column list. If the sample or an import includes it, treat it as an optional extra column. Otherwise use a type such as "Unspecified".

  • Derived values are app logic, not source data. Hours used = the sum of meeting minutes per room per week ÷ 60, with overlapping meetings in the same room counted once.

  • Hours available = the length of a teaching week that the user can set in the app (days × daily window, e.g. Mon–Fri 08:00–17:00 = 45 h). Show whatever default the app picks as a setting. It is not a sourced benchmark.

  • A double-booking conflict = two rows with the same building + room whose day sets intersect and whose times overlap (start₁ < end₂ and start₂ < end₁). Cross-listed sections can legitimately share a slot. The schema has no cross-list field, so show those as conflicts the user can review.

Suggested citation ("Data & sources" panel)

BRAND.md forbids network requests. Links in the panel should therefore be in-page anchors, for example to the How to use section, and not remote resources.

  • Sample schedule: "Illustrative synthetic schedule for a fictional term, generated by this app's build script (scripts/, fixed seed). Not real University of Idaho enrollment, schedule or room data." Link to the in-app How to use / methodology section.

  • User data: "Your imported schedule CSV (processed locally in your browser; nothing is uploaded)."

Verification log

  • 2026-09-13, independent verifier: data/raw holds only this note. There were no downloads, so nothing could be re-fetched live. No MindRouter API key material and no email addresses were found in data/raw. The only data commit touches only data/raw/SOURCES.md. Wording was tightened to match SPEC.md (U of I-style building names allowed), and a sample-license line, importer edge cases (day-code variants, 12 AM/PM, unscheduled rows) and definitions of the derived values were added.

How it was built

Written by Goose 1.50.0 (github.com) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.

14harness rounds
127.0minutes of harness work (2.1 h)
2,282lines of code
226KB, single-file app

Rounds by kind

build1
feedback8
continue2
critic3

Lines of code per file

body html239
app css367
app js1,352
scripts324

Critic scores ✎ scored by the independent AI critic, 1–10

visual 7 out of 10
usefulness 8 out of 10
correctness 8 out of 10
interactivity 8 out of 10

Independent audits ✎ by the AI critic

Data honesty: checked against raw data Authorship audit: every app-code commit is a harness round

Automated checks

Console no console errors No network 0 external requests Phone layout 0 px overflow Canvases no empty canvases Brand header header present No key leak no key in source

Checked by critic-verify.

Data spot checks ✎ written by the AI critic

  • Leather Field House 396: displayed 26h 30m used, 59% util, 44% seat fill. An independent Python pass on data/embed/sample_schedule.json (Mon-Fri 8-5, overlaps merged) gives 1590 min; 1590/2700 = 58.9%; mean enrollment/capacity over its 8 sections = 44.5%.
  • Heatmap: displayed Tue 4 PM '47 of 59 rooms in use' and Mon 10 AM 30. Python distinct-room counts are 47 (the grid maximum) and 30.
  • Summary cards: displayed 476 scheduled / 25 unscheduled, 35% avg util, 52% avg seat fill, 3 over capacity, 152 far too big, 8 double-bookings, 3 unused. Python gives 476 / 25 / 34.57% / 52.29% / 3 / 152 / 8 / 3.
  • Report: displayed 'BIO 220 04 - Huckleberry Hall 180 - 165/155 (106%)'. The raw record is MWF 500-550, enrollment 165, capacity 155.
  • Conflict list: displayed 'Palouse Hall 417 CHEM 320 01 (3:00 PM) <-> CHEM 320 02 (3:34 PM)'. The raw TR sections run 900-975 and 934-1009.
Show all 6 spot checks
  • Histogram bins 4,5,60,83,77,95,47,50,35,20 match Python. Find a room (30 seats, MWF 10:30, 50 min) shows 11 rooms with best fit Clearwater Library Annex 279 (cap 32); Python agrees.
Build notes ✎ written by the AI reviewer

Ten rounds in total. By kind: round 1 build; round 3 continue; rounds 2 and 4-10 feedback, which is the limit of 8. Harness time was 6429 s. There were no network errors and the TAMPER GUARD never tripped. I wrote only FEEDBACK.md, prompts and test scripts, never app code.

  • The hardest problem was that Goose with qwen3.8-27b kept degenerating into repetition loops whenever a round had to investigate or patch existing code: Round 2 re-ran the same Python stats check about 157 times.

  • Round 3 re-ran the same grep about 234 times.

  • Round 4 about 164 times.

  • Round 5 re-ran a grep for 'svgEl' 88 times.

  • Round 6 re-ran a data check 35 times.

  • Round 7 re-ran a 'node -e' debug script 18 times.

  • Round 8 cycled through 9 greps about 8 times each.

MindRouter was reachable throughout (401). The request logs showed the full conversation was being sent (364 messages, about 214 KB), so the model saw the tool output and still repeated. The Goose config has GOOSE_TEMPERATURE 0.2, which I did not modify.

Rounds 2-8 were killed via kill_round.sh once a loop was confirmed. Only rounds 2 and 6 got anything done before dying (sample-data regeneration), plus round 7's rebuild. Rounds 3, 4, 5 and 8 changed no app code.

  • After rounds 2 and 3 looped on the same prompt, I stopped re-running identical prompts as 'continue', since that clearly repeated the failure. What finally worked was asking for from-scratch front-end rebuilds: 'do not open existing src/, template.html, index.html or the JSON; write complete new files; do not write test/debug scripts'. Each FEEDBACK.md in that mode was a full requirements list, with the embedded data format described and the previous version's bugs marked FIX. Round 7 (rebuild, then a late debug loop, killed after writing), round 9 (completed, 8 min) and round 10 (completed, 7.5 min) all produced working apps. Each rebuild fixed most of the previous version's bugs but introduced a few new ones: Round 9: a heatmap rendered as a vertical list, report metrics printing '[object Object]', a NaN room count, wrong seat-fill averages, and broken click-throughs.

Round 10 fixed all of those. Its remaining flaws, listed above, could not be fed back because all 8 feedback rounds were used. The most noticeable is the stray histogram band painted across the top of the page, which is why the status is needs-work rather than shipped. Functionally the app is complete and useful; every SPEC feature works in my Playwright checks at 1440 px and 390 px.

Two small wording issues in the FEEDBACK.md files: early versions named a suspect function, and I removed that before the round ran. They also gave implementation-flavoured guidance (header alias lists, the data schema, and how to write large files in parts); no code was written for the harness.

Round-by-round timeline

Every harness round, in order. The prompts and FEEDBACK.md were written by the AI reviewer; the app code was written by the harness. Expand a round to see the exact prompt and the FEEDBACK.md the harness received.

    1 build 7.1 min · 6 files changed, 3279 insertions(+), 3 deletions(-) Show prompt

    Prompt given to the harness:

    Read AGENTS.md, SPEC.md and BRAND.md in this directory. Then build the complete app they describe, step by step: this app has no raw downloads (data/raw/SOURCES.md explains why and gives the CSV schema, parsing rules and labelling requirements), so first write a script in scripts/ (Python 3 standard library only, fixed random seed) that generates the clearly labeled synthetic sample schedule for a fictional term with plausible U of I-style building names, rooms, room types, capacities and sections, writes it as compact JSON to data/embed/, and run it; then write src/body.html, src/app.css and src/app.js; run python3 build.py --check and fix any error it reports. Implement every required feature in SPEC.md with a polished, modern, responsive University of Idaho design. Do not stop until python3 build.py --check succeeds and every feature is implemented.
    

    No feedback file was sent in this round.

    3 continue 19.1 min · — Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory — it lists problems a reviewer found when testing index.html in a real browser. Fix every item, keeping everything that already works, then run python3 build.py --check.
    

    No feedback file was sent in this round.

    11 critic 0.2 min · 1 file changed, 87 insertions(+) Show prompt

    Prompt given to the harness:

    Read AGENTS.md and FEEDBACK.md in this directory. The app in src/ already works; FEEDBACK.md lists 12 specific bugs found by a reviewer testing it in a browser. Fix every item with small, targeted edits to src/body.html, src/app.css and src/app.js — do NOT rebuild the files from scratch, and do not change data/, scripts/, template.html or build.py. Work efficiently: view each of the three src files once, then edit; never run the same search or command twice in a row, and do not write or run test or debug scripts (the reviewer tests in a browser). When all 12 items are done, run python3 build.py --check, fix any error it reports, and stop.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — critic pass on the current version

    I tested the current build in Chromium at 1440×900 and 390×844. Most of it works well: the numbers are right, and the heatmap, report and Find a room behave as they should. Keep the current design and code. Fix only the problems below, with small targeted edits to src/body.html, src/app.css and src/app.js. Do not rebuild from scratch. Do not change data/ or scripts/.

    The items are in priority order.

    1. FIX — Teal stripe across the top of every page (seat-fill histogram)
    • What I see: a translucent teal band with dashed top and bottom edges covers the full width of the viewport. It runs from under the header through the intro and the How to use steps, and the dashed line crosses the step text. On phone it also tints the black header.

    • Why: in the browser, the histogram's target-band element (.hist__band) has a bounding box 1440 px wide and 270 px tall, positioned against the page. The histogram container is not its positioning context.

    • Also: #ov-hist itself has the class hist, and the rendered markup puts a second .hist plus the caption paragraph inside it. The result:

      • the bars are squeezed into a narrow strip on the left of the card,

      • the "Seat fill of 476 scheduled sections…" caption sits beside the bars,

      • the header caption reads "Distribution of the 0 scheduled sections" (it should say 476).

    • Expected:

      • The shaded band sits only inside the histogram's plot area, behind the bars, between the seat-fill min % and max % (65–90% by default).

      • The bars span the full card width, about 240 px tall.

      • The x labels are readable (0%, 10%, … 90%+).

      • There is one caption, below the chart, that says 476.

    2. FIX — Inline bars never show their fill
    • Where: the Util % and Seat fill % columns in the Rooms table, and the fit bar on each Find a room card.

    • What I see: only the grey track and the black target tick. The coloured fill is invisible for every row.

    • Why: the fill elements are inline <span>s, so their width and height are ignored.

    • Expected:

      • Util % bars are gold, with width equal to the utilization.

      • Seat-fill bars are coloured by status: calm grey below the min, Clearwater within the band, Raspberry above the max. Width equals the seat fill, capped at 100%.

      • Find a room fit bars are visible.

    3. FIX — Import panel: preview hidden, mapper won't collapse
    • Preview: after choosing a file or clicking "Try an example CSV", the "Preview (first 8 rows)" card never appears. It is un-hidden and then immediately hidden again. Show it, with invalid cells in red, and refresh it whenever a mapping dropdown changes.

    • Mapper after Load data: "Map columns" stays open even though the mapper has the hidden attribute, because a CSS display rule overrides hidden. After Load it should collapse, leaving a small "Edit mapping" button.

    • A second file: reading another file must show the mapper again. Today it keeps the hidden attribute.

    4. FIX — Import validation counts and required columns
    • Unscheduled count is always 0:

      • The example CSV has an ONLINE row, yet the summary reads "9 rows loaded · 0 unscheduled · 0 rejected".

      • With my own file containing a TBA row, it read "5 rows loaded · 0 unscheduled · 3 rejected", while the Sections card said "1 unscheduled".

      • Rows whose days are blank, TBA, ARR or ONLINE must count as unscheduled in this line.

    • Unmapped required fields: my file used the headers Meeting Days and Start Time, which auto-mapping missed. Load data still went ahead, and every row silently became unscheduled.

      • Add meeting days and start time to the auto-mapping aliases.

      • Block Load data with a clear message naming any unmapped required field (course, days, start, end, building, room, enrollment, capacity).

    • Missing capacity: a blank capacity currently rejects the row. Instead, load the row, show "—" for its seat fill, and add a warning to the problems table.

    5. FIX — "Try an example CSV" is too clean to demonstrate validation

    The built-in example should show off the normalization. Use these headers:

    Subject Course,Sect,Mtg Days,Begin Time,End Time,Bldg,Rm,Enrl,Room Cap,Room Type

    • About 12 rows, including: days written M W F, TuTh and mwf

    • times written 9:30 AM, 13:00, 0930 and 12:50 p.m.

    • one TBA row with blank times

    • one bad day code, MXF

    • one row whose end time is before its start time

    • two sections double-booked in the same room

    The validation line should then read something like "10 rows loaded · 1 unscheduled · 2 rejected".

    6. FIX — Schedule opens on "All buildings" (59 rows), and room numbers repeat
    • Default building: the Schedule tab opens on "All buildings" with 59 rows. Open it on the first building in the list instead, keeping "All buildings" as an option.

    • Row labels: rows are labelled with the room number only. Five room numbers exist in two buildings (131, 172, 286, 389, 462), so the rows look identical. In the All buildings view, show the building under the room number, e.g. "131 · Lupine Arts Center · studio · cap 22".

    7. FIX — Gantt time axis wraps "9 PM" onto a second line

    The axis has 15 hour labels (7 AM … 9 PM) in a 14-column row, so "9 PM" drops onto a second line and the header is two rows tall. Keep the axis on one line, with each label aligned to its gridline and 9 PM at the right edge.

    8. FIX — Gantt tooltip and conflict highlighting
    • Tooltip: a bar's tooltip shows only course, time and enrollment. Add the building and room, and enrollment ÷ capacity (e.g. "34 / 232 seats").

    • Conflict highlighting: clicking a conflict in "Conflicts on this day" highlights every bar in that room. In Palouse Hall 417 on Tuesday it lights up 3 bars. Highlight only the two conflicting sections, and scroll them into view.

    9. FIX — Rooms table "Too big" flag is on almost every room
    • What I see: the flag appears on 52 of the 56 rooms that have classes, because it fires when any single section is below 40%. That makes it noise.

    • Expected: show the count in the pill, e.g. "3 too-big", so rooms can be told apart. Make the pill's title say "3 sections below 40% seat fill".

    10. FIX — Day toggle buttons wrap at 1440 px

    In "Teaching week & targets", the "U" (Sunday) button wraps onto a second line at 1440 px. All 7 day buttons should fit on one row on desktop; wrapping on phone is fine.

    11. FIX — Nested <main>

    src/body.html has a <main class="panels">, but the template already wraps the body in <main id="app">. Use a <div> for the panels container.

    12. Polish — Overview balance
    • The histogram card is half empty next to the tall 10-room list. Once item 1 is fixed, make the chart taller so the two cards look balanced.

    • Add a small legend chip for the shaded target band, e.g. "▢ target 65–90%".

    Working rules
    • Read each of the three src/ files once, then make the edits. Do not repeat the same search or command. If a search finds nothing, move on.

    • Do not write or run test or debug scripts; the reviewer tests in a browser.

    • Run python3 build.py --check, fix any error it reports, and stop.

    12 continue 5.2 min · — Show prompt

    Prompt given to the harness:

    Read AGENTS.md and FEEDBACK.md in this directory. The app in src/ already works; FEEDBACK.md lists 12 specific bugs found by a reviewer testing it in a browser. Fix every item with small, targeted edits to src/body.html, src/app.css and src/app.js — do NOT rebuild the files from scratch, and do not change data/, scripts/, template.html or build.py. Work efficiently: view each of the three src files once, then edit; never run the same search or command twice in a row, and do not write or run test or debug scripts (the reviewer tests in a browser). When all 12 items are done, run python3 build.py --check, fix any error it reports, and stop.
    

    No feedback file was sent in this round.

    13 critic 11.9 min · 4 files changed, 192 insertions(+), 66 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory. The app in src/ already works; FEEDBACK.md lists 12 specific bugs a reviewer found in a browser. Fix them with targeted edits to src/body.html, src/app.css and src/app.js. Do not rebuild the files from scratch and do not change data/, scripts/, template.html or build.py.
    
    Tool rules (important: previous attempts got stuck running the same grep 70+ times):
    1. Do NOT use shell commands to search or read files: no grep, sed, cat, head, tail, rg, node or python. Use only the text editor tool: "view" to read and "str_replace" to edit.
    2. Never open index.html. It is generated by build.py, so it does not help.
    3. View src/body.html once and src/app.css once. View src/app.js in about four ranges of 350 lines, each range once.
    4. Then fix the items in order, 1 to 12. After each str_replace, go straight on to the next item. Do not re-read or re-check what you just changed.
    5. If a str_replace fails because the text is not found, view only that small range once, retry once, then move on.
    6. The only shell command you may run is python3 build.py --check, once all 12 items are done. If it reports an error, fix it, run the check again, and stop.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — critic pass on the current version

    I tested the current build in Chromium at 1440×900 and 390×844. Most of it works well: the numbers are right, and the heatmap, report and Find a room behave as they should. Keep the current design and code. Fix only the problems below, with small targeted edits to src/body.html, src/app.css and src/app.js. Do not rebuild from scratch. Do not change data/ or scripts/.

    The items are in priority order.

    1. FIX — Teal stripe across the top of every page (seat-fill histogram)
    • What I see: a translucent teal band with dashed top and bottom edges covers the full width of the viewport. It runs from under the header through the intro and the How to use steps, and the dashed line crosses the step text. On phone it also tints the black header.

    • Why: in the browser, the histogram's target-band element (.hist__band) has a bounding box 1440 px wide and 270 px tall, positioned against the page. The histogram container is not its positioning context.

    • Also: #ov-hist itself has the class hist, and the rendered markup puts a second .hist plus the caption paragraph inside it. The result:

      • the bars are squeezed into a narrow strip on the left of the card,

      • the "Seat fill of 476 scheduled sections…" caption sits beside the bars,

      • the header caption reads "Distribution of the 0 scheduled sections" (it should say 476).

    • Expected:

      • The shaded band sits only inside the histogram's plot area, behind the bars, between the seat-fill min % and max % (65–90% by default).

      • The bars span the full card width, about 240 px tall.

      • The x labels are readable (0%, 10%, … 90%+).

      • There is one caption, below the chart, that says 476.

    2. FIX — Inline bars never show their fill
    • Where: the Util % and Seat fill % columns in the Rooms table, and the fit bar on each Find a room card.

    • What I see: only the grey track and the black target tick. The coloured fill is invisible for every row.

    • Why: the fill elements are inline <span>s, so their width and height are ignored.

    • Expected:

      • Util % bars are gold, with width equal to the utilization.

      • Seat-fill bars are coloured by status: calm grey below the min, Clearwater within the band, Raspberry above the max. Width equals the seat fill, capped at 100%.

      • Find a room fit bars are visible.

    3. FIX — Import panel: preview hidden, mapper won't collapse
    • Preview: after choosing a file or clicking "Try an example CSV", the "Preview (first 8 rows)" card never appears. It is un-hidden and then immediately hidden again. Show it, with invalid cells in red, and refresh it whenever a mapping dropdown changes.

    • Mapper after Load data: "Map columns" stays open even though the mapper has the hidden attribute, because a CSS display rule overrides hidden. After Load it should collapse, leaving a small "Edit mapping" button.

    • A second file: reading another file must show the mapper again. Today it keeps the hidden attribute.

    4. FIX — Import validation counts and required columns
    • Unscheduled count is always 0:

      • The example CSV has an ONLINE row, yet the summary reads "9 rows loaded · 0 unscheduled · 0 rejected".

      • With my own file containing a TBA row, it read "5 rows loaded · 0 unscheduled · 3 rejected", while the Sections card said "1 unscheduled".

      • Rows whose days are blank, TBA, ARR or ONLINE must count as unscheduled in this line.

    • Unmapped required fields: my file used the headers Meeting Days and Start Time, which auto-mapping missed. Load data still went ahead, and every row silently became unscheduled.

      • Add meeting days and start time to the auto-mapping aliases.

      • Block Load data with a clear message naming any unmapped required field (course, days, start, end, building, room, enrollment, capacity).

    • Missing capacity: a blank capacity currently rejects the row. Instead, load the row, show "—" for its seat fill, and add a warning to the problems table.

    5. FIX — "Try an example CSV" is too clean to demonstrate validation

    The built-in example should show off the normalization. Use these headers:

    Subject Course,Sect,Mtg Days,Begin Time,End Time,Bldg,Rm,Enrl,Room Cap,Room Type

    • About 12 rows, including: days written M W F, TuTh and mwf

    • times written 9:30 AM, 13:00, 0930 and 12:50 p.m.

    • one TBA row with blank times

    • one bad day code, MXF

    • one row whose end time is before its start time

    • two sections double-booked in the same room

    The validation line should then read something like "10 rows loaded · 1 unscheduled · 2 rejected".

    6. FIX — Schedule opens on "All buildings" (59 rows), and room numbers repeat
    • Default building: the Schedule tab opens on "All buildings" with 59 rows. Open it on the first building in the list instead, keeping "All buildings" as an option.

    • Row labels: rows are labelled with the room number only. Five room numbers exist in two buildings (131, 172, 286, 389, 462), so the rows look identical. In the All buildings view, show the building under the room number, e.g. "131 · Lupine Arts Center · studio · cap 22".

    7. FIX — Gantt time axis wraps "9 PM" onto a second line

    The axis has 15 hour labels (7 AM … 9 PM) in a 14-column row, so "9 PM" drops onto a second line and the header is two rows tall. Keep the axis on one line, with each label aligned to its gridline and 9 PM at the right edge.

    8. FIX — Gantt tooltip and conflict highlighting
    • Tooltip: a bar's tooltip shows only course, time and enrollment. Add the building and room, and enrollment ÷ capacity (e.g. "34 / 232 seats").

    • Conflict highlighting: clicking a conflict in "Conflicts on this day" highlights every bar in that room. In Palouse Hall 417 on Tuesday it lights up 3 bars. Highlight only the two conflicting sections, and scroll them into view.

    9. FIX — Rooms table "Too big" flag is on almost every room
    • What I see: the flag appears on 52 of the 56 rooms that have classes, because it fires when any single section is below 40%. That makes it noise.

    • Expected: show the count in the pill, e.g. "3 too-big", so rooms can be told apart. Make the pill's title say "3 sections below 40% seat fill".

    10. FIX — Day toggle buttons wrap at 1440 px

    In "Teaching week & targets", the "U" (Sunday) button wraps onto a second line at 1440 px. All 7 day buttons should fit on one row on desktop; wrapping on phone is fine.

    11. FIX — Nested <main>

    src/body.html has a <main class="panels">, but the template already wraps the body in <main id="app">. Use a <div> for the panels container.

    12. Polish — Overview balance
    • The histogram card is half empty next to the tall 10-room list. Once item 1 is fixed, make the chart taller so the two cards look balanced.

    • Add a small legend chip for the shaded target band, e.g. "▢ target 65–90%".

    Working rules
    • Read each of the three src/ files once, then make the edits. Do not repeat the same search or command. If a search finds nothing, move on.

    • Do not write or run test or debug scripts; the reviewer tests in a browser.

    • Run python3 build.py --check, fix any error it reports, and stop.

    14 critic 2.6 min · 5 files changed, 129 insertions(+), 182 deletions(-) Show prompt

    Prompt given to the harness:

    Read FEEDBACK.md in this directory. The app in src/ works; FEEDBACK.md lists 12 remaining bugs found by a reviewer in a browser. Fix them with targeted edits to src/body.html, src/app.css and src/app.js. Do not rebuild the files from scratch, and do not change data/, scripts/, template.html or build.py.
    
    Tool rules. These matter: previous attempts got stuck repeating the same grep 70+ times, then appended the same CSV row 26 times.
    1. Do NOT use shell commands to search or read files. That means no grep, sed, cat, head, tail, rg, node or python. Use only the text editor tool: "view" to read and "str_replace" to edit.
    2. Never open index.html. It is generated.
    3. View src/body.html once and src/app.css once. View src/app.js in about four ranges of 350 lines, each range once.
    4. Fix the items in order, 1 to 12, with one str_replace per change. After each edit, go straight to the next item. Do not re-read or re-verify it. Never make the same kind of edit twice in a row.
    5. Item 1 is a deletion. Replace the whole block of duplicate CHEM 101 rows 04–29 with a single ENGL 102 row, in ONE str_replace. Do not add any other rows.
    6. If a str_replace fails, view only that small range once, retry once, then move on.
    7. The only shell command you may run is python3 build.py --check, after item 12. If it reports an error, fix it, run the check again, and stop.
    

    FEEDBACK.md the harness received ✎ written by the AI reviewer:

    Reviewer feedback — final critic pass

    • I re-tested the current build in Chromium at 1440×900 and 390×844. The last round fixed a lot: the teal stripe across the top of the page is gone

    • the inline bars now show their fill

    • the import preview appears, and the new header aliases work

    • unmapped required fields now block loading

    Keep all of that. Fix only the items below, with small targeted edits. Do not rebuild files from scratch, and do not change data/ or scripts/. The items are in priority order.

    1. FIX — The example CSV has 26 duplicate rows (and 351 fake conflicts)

    In EXAMPLE_CSV in src/app.js, the rows CHEM 101,04 through CHEM 101,29 are all the same class: MWF 0830–0920 in Palouse Hall 101. After "Try an example CSV" → Load data, the Double-bookings card shows 351 conflicting pairs.

    • Delete the rows from CHEM 101,04 through CHEM 101,29.

    • The example should end up with exactly the header plus the first 11 rows, from CHEM 101,01 to MATH 121,02.

    • Then add one more row that double-books a room, e.g. ENGL 102,01,MWF,11:00 AM,11:50 AM,Garnet Engineering Hall,329,20,35,seminar.

    That makes 12 data rows in total. Do not add any other rows.

    2. FIX — Import validation still says "0 unscheduled"
    • Example CSV: the result reads "35 rows loaded · 0 unscheduled · 2 rejected", although the file has a TBA row and the Sections card says "1 unscheduled".

    • My own file: a TBA row also gave "0 unscheduled".

    • Why: rows whose days are blank, TBA, ARR or ONLINE are detected as unscheduled, but they are never marked unscheduled, so the count stays 0.

    • Expected: those rows count in "N unscheduled".

    3. FIX — "Map columns" still stays open after Load data
    • What I see: after Load data the mapper grid is still fully visible, with the new Edit mapping button next to it.

    • Why: in the browser the mapper has the hidden attribute, but its computed display is still grid. The .mapper CSS rule overrides hidden, and there is no .mapper[hidden] rule.

    • Expected: after Load the grid collapses, leaving the Load data / Edit mapping buttons. Edit mapping opens it again.

    4. FIX — Seat-fill histogram: half width, and the band is in the wrong direction
    • Width: the bars and band fill only the left half of the card (about 290 of 586 px). This is because #ov-hist has the class hist and its content creates another .hist inside it. Remove the class hist from the #ov-hist container in src/body.html, so the chart spans the full card width.

    • Band direction: seat-fill % is the x-axis of this chart. The shaded target band is currently a horizontal strip positioned by bar height (bottom 65%, height 25% of the chart). It should be a vertical column behind the bins from 65% to 90% on the x-axis. With the defaults it shades the 60–70%, 70–80% and 80–90% bins, with left and right dashed edges. It must update when seat-fill min or max changes.

    5. FIX — Schedule opens on "All buildings" (59 rows), and room numbers repeat
    • Default building: open the Schedule tab on the first building in the list, "Clearwater Library Annex", instead of "All buildings". Keep "All buildings" as an option.

    • Row labels: in the All buildings view, include the building on the row label, e.g. "131 · Lupine Arts Center". Room numbers 131, 172, 286, 389 and 462 each exist in two buildings.

    6. FIX — Gantt time axis wraps "9 PM" onto a second line

    The axis has 15 hour labels (7 AM … 9 PM) in a 14-column row, so "9 PM" drops onto a second line and the axis is 60 px tall. It should be one line, about 30 px tall.

    7. FIX — Day toggle buttons wrap at 1440 px

    In Teaching week & targets, "U" wraps onto a second row at 1440 px. All 7 buttons should fit on one row on desktop. For example, give the Days field two grid columns, or make the buttons 34 px wide.

    8. FIX — Gantt tooltip and conflict highlighting
    • Tooltip: add the building and room, and enrollment ÷ capacity. Example: "MATH 401 03 · 4:15–5:30 PM · Palouse Hall 172 · 34 / 232 seats".

    • Conflict highlighting: clicking an item in "Conflicts on this day" highlights every bar in that room (3 bars in Palouse Hall 417 on Tuesday). Highlight only the two sections named in the item.

    9. FIX — Rooms table "Too big" pill is on nearly every room

    It shows on 52 of 56 rooms that have classes. Show the number of too-big sections in the pill, e.g. "3 too-big", with a title such as "3 sections below 40% seat fill".

    10. FIX — Find a room fit bar is reversed
    • What I see: the best fit (room 279, 30 seats needed, capacity 32) has the shortest bar, and the worst fit (capacity 304) has the longest.

    • Expected: the bar width is seats needed ÷ capacity. Room 279 should be about 94%; the room with capacity 304 should be about 10%.

    11. FIX — Nested <main>

    src/body.html wraps the panels in <main class="panels">, but the template already has <main id="app">. Change the inner one to a <div>.

    12. FIX — A missing capacity shows "0%" instead of "—"
    • What I see: an imported row with a blank capacity is loaded with capacity 0, so the Rooms table shows capacity 0 and seat fill 0%.

    • Expected: show "—" for capacity and seat fill, and leave such sections out of the seat-fill averages and the histogram.

    Working rules
    • Use only the text editor tool: view to read, str_replace to edit.

    • Do not repeat an edit, and never append rows one at a time.

    • The only shell command allowed is python3 build.py --check, once, at the end.

What the agent was asked

The full specification the harness received, rendered from the original Markdown.

Read the specification

Classroom Utilization Explorer — Where and when is teaching space actually used?

Category: Administration Who it is for: Registrar and space-planning staff, facilities, department schedulers

What it should do

Load a semester's section schedule and instantly see which rooms sit empty, which are overbooked, and which classes are in rooms far too big — the evidence space-planning decisions need.

Data

Works on the user's own schedule CSV (course, section, days, start, end, building, room, enrollment, room capacity). Ship a clearly labeled synthetic sample for a fictional term with plausible U of I-style building names (the sample must be marked as illustrative, not real enrollment).

Raw downloads (if any) are in data/raw/ with data/raw/SOURCES.md describing where each file came from. Transform what the app needs into compact JSON in data/embed/ (write a small script in scripts/ to do it, so the transformation is reproducible). Every dataset the app shows must be credited in a visible "Data & sources" section of the app, with links. If a dataset is illustrative/synthetic, label it clearly as such in the UI — never present made-up numbers as real University of Idaho figures.

Required features

  • CSV import with column mapping and validation (days like MWF/TR, 12/24-hour times)

  • Week × hour heatmap of occupied rooms campus-wide, filterable by building and room type

  • Room-by-room utilization table: hours used vs available, seat-fill % (enrollment ÷ capacity), with sorting and targets

  • Gantt-style daily schedule per building with conflict (double-booking) detection

  • 'Find a room' tool: required seats + time slot → free rooms that fit best

  • Summary cards and a one-click PDF-ready summary report

Quality bar

This app is going into a University of Idaho gallery that shows faculty, staff and students what they can build with AI coding agents. It must look polished and modern (think a well-designed data product, not a homework demo), feel alive (smooth transitions, responsive hover/selection states, animated where it helps understanding), and do real, useful work — every control should change something meaningful. It must work well on a laptop and on a phone. Include a short "How to use" hint for first-time visitors and a helpful empty state wherever the user must supply data.

Known limitations

Reported by the independent critic. ✎ written by the AI critic

  • After importing a CSV and clicking 'Back to sample', the building dropdowns keep the imported file's buildings (e.g. Clearwater Library Annex is missing), and Schedule stays on All buildings until the page is reloaded.
  • Phone Gantt: the 190 px room-label column leaves only about 150 px of timeline visible, so the chart needs horizontal scrolling and bar labels are truncated (e.g. 'GEOL 102 0').
  • Overview: the seat-fill histogram card is half empty next to the taller 10-room list, and the target band's dashed edge crosses the '60%' axis label.
  • The synthetic sample is realistic but flat against the default targets. No room reaches the 65% utilization target and only 2 rooms fall inside the 65-90% seat-fill band, so most table bars are grey and most rooms carry a too-big pill.
  • Harness behaviour during the critic rounds: round 11 returned an empty model response (13 s, no change). Round 12 ran the same grep 71 times and was killed. Round 13 fixed 5 of 12 items, then looped appending duplicate CSV rows until killed (the 26 duplicates were deleted in round 14). Round 14 completed on its own and fixed all 12 remaining items.