Travel Expense Reconciler
Per-diem math and receipt checks for university travel
Enter a trip once and get the per-diem math done right — partial first and last days, provided meals deducted, lodging caps checked — with every receipt reconciled and a clean summary for your travel reimbursement.
Desktop · click to enlarge
What it is ✎ blurb written by the AI critic
Enter a university trip (dates, travelers, where you sleep each night and which meals a conference provides) and the app works out each day's meal and lodging allowance from real GSA FY2026 rates. It uses the University of Idaho rule by default (noon / 6 p.m. timing, $58.00 Idaho in-state rate), or the federal 75% rule for federally sponsored travel. Add receipts, with image thumbnails kept in your browser, to see over-cap lodging, missing or duplicate receipts, per-traveler totals and charts, then export a CSV or print a summary.
Try this
- Click 'Load example trip', then set Departure to 'Trip begins after 12 p.m. (noon)' and watch Jun 9 drop to 50% (allowance $220.00) and Per diem (all) fall from $1,088.80 to $1,030.80
- Switch to the Federal rule: the Boise daily rate changes from the $58.00 Idaho in-state rate to GSA's $86.00, with 75% on the first and last day
- Change A. Faculty's Jun 9 lodging receipt from 204.50 to 190.00 and watch the over-cap flag and the $13.50 'Not reimbursable' clear
Highlights ✎ written by the AI critic
- University of Idaho rule by default (APM 70.02 noon / 6 p.m. timing, State of Idaho $58.00/day in-state rate), with the federal 75% first/last-day rule as a labelled alternative and the policy text quoted in the app
- Real GSA FY2026 lodging caps by month and M&IE rates for Moscow, Boise, Idaho Falls, Coeur d'Alene, Seattle, Washington DC and Denver
- Tick conference-provided meals or personal days and every allowance, flag, chart and total updates immediately
- Per-traveler reconciliation: each person's hotel receipt is checked against the nightly cap, and missing lodging receipts are grouped by stay
- Drag-and-drop receipt images, CSV export, printable summary and saved trips, all kept in the browser
Who it's for
Faculty and staff who travel, department business officers
Features
- Trip builder: travelers, legs, dates, destination per night, conference-provided meals per day
- Automatic M&IE per day with 75% first/last-day rule and meal deductions; lodging cap per night by month
- Receipt table (category, date, amount, notes) with drag-and-drop of receipt images kept locally as thumbnails
- Reconciliation panel: over-cap flags, missing receipts, duplicates, personal days
- Charts: spending by category and by day vs allowance
- Printable summary and CSV export; save/load trips in the browser
Data & sources
- api.gsa.gov — GSA Per Diem API v2 — FY2026 rates by city (Moscow ID, Boise ID, Idaho Falls ID, Coeur d'Alene ID, Seattle WA, Washington DC, Denver CO):
- api.gsa.gov — GSA Per Diem API v2 — FY2026 CONUS M&IE breakdown:
- api.gsa.gov — GSA Per Diem API v2 — FY2026 CONUS lodging, all localities:
- api.gsa.gov — GSA Per Diem API v2 — FY2026 ZIP code to locality:
- www.gsa.gov — GSA FY2026 Per Diem master rates file (XLSX):
- www.gsa.gov — GSA M&IE breakdowns FY2025–present (DOCX):
- www.uidaho.edu — University of Idaho APM Chapter 70 Travel Management (policy reference):
- www.uidaho.edu — University of Idaho APM 70.02 Travel (§I proration, saved page):
- www.uidaho.edu — University of Idaho APM 70.04 Travel Entitlements (§E, outdated $45 in-state maximum, saved page):
- www.sco.idaho.gov — State of Idaho State Travel Policy and Procedures, SBEX Policy No. 442-50, amended Aug 20, 2026 (App. B in-state $58.00/day; §6.C meal percentages):
- www.ecfr.gov — Federal Travel Regulation 41 CFR Part 301-11 as of 2026-09-01 (§301-11.20 75% rule; §301-11.21 meal deductions):
How it was built
Written by Codex CLI 0.154.0 (github.com) on qwen/qwen3.8-27b, served by MindRouter, the University of Idaho's on-premises AI platform.
Rounds by kind
| build | 1 |
| feedback | 9 |
| critic | 5 |
| continue | 1 |
Lines of code per file
| body html | 247 |
| app css | 501 |
| app js | 1,803 |
| scripts | 199 |
Critic scores ✎ scored by the independent AI critic, 1–10
Independent audits ✎ by the AI critic
Automated checks
Checked by critic-verify.
Data spot checks ✎ written by the AI critic
- Boise Jun 2026 lodging cap: app $191.00 = raw gsa_perdiem_city_boise_id_2026.json Jun value 191
- Washington DC Mar 30-Apr 1 caps $276.00 and M&IE $92.00 = raw gsa_perdiem_city_washington_dc_2026.json Mar 276, Apr 276, meals 92
- Seattle Jul cap $248.00 and Coeur d'Alene Jul cap $217.00 = raw Seattle and Coeur d'Alene city files, Jul 248 / Jul 217
- U of I Idaho in-state rate $58.00/day = raw idaho_sbex_442-50_state_travel_policy.txt '(a) In State $58.00 day'; meal shares 25/35/55% = SBEX 442-50 section 6.C lines 455-457
- Federal Boise first/last day $64.50 = raw gsa_perdiem_conus_mie_2026.json tier 86 FirstLastDay 64.5; DC deductions breakfast 23 / lunch 26 / dinner 38 = tier 92
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.
-
Every lodging cap shows
$undefinedand every allowance shows$NaN. In "2 · Itinerary & lodging by night" the Lodging cap cell is$undefinedfor every night; the Allowance column is$NaNfor every day except the last; the Summary shows "Per diem computed $NaN", "lodging caps $NaN", "Receipts − per diem −$NaN"; the CSV hasNaNin the Amount column. The embedded data stores monthly lodging under the keys"Jan"…"Dec", but the page looks a night's cap up by the two-digit month from the date (e.g."06"), which does not exist. Expected for the test trip (federal rule): lodging caps Jun 8 $215 (Denver/Aurora, June), Jun 9 $215, Jun 10 $248 (Seattle, June); M&IE Jun 8 $69.00 (75% of $92), Jun 9 $66.00 ($92 − lunch $26), Jun 10 $31.00 ($92 − breakfast $23 − dinner $38), Jun 11 $69.00; M&IE total $235.00 (this part is already right), lodging caps total $678, per diem $913.00. Make sure every place that uses a cap (table, allowance, reconciliation, by-day chart, summary, CSV, print) gets a real number. -
1,333 console errors while using the app, all from the by-day chart:
Error: <rect> attribute y: Expected length, "NaN".,Error: <rect> attribute height: Expected length, "NaN".,Error: <line> attribute y1: Expected length, "NaN".,Error: <text> attribute y: Expected length, "NaN".The "By day — spend vs. per-diem allowance" chart is an empty white area with only x-axis dates and a legend. Expected: no console errors; paired bars per day (allowance vs receipts) with a y-axis in dollars. -
Over-cap lodging is never flagged. The $239.40 lodging receipt on Jun 8 is above Denver's June cap ($215), but "4 · Reconciliation" shows no over-cap flag (it only says "No lodging receipt for the night of Jun 10 in Seattle (cap $undefined)"). Expected a red flag like "Lodging Jun 8: $239.40 exceeds the Denver / Aurora cap of $215 by $24.40", and the over-cap amount highlighted on that receipt row and in the itinerary row.
-
Destination names are wrong. The destination dropdown lists "Standard Rate, WY" twice (Moscow and Idaho Falls are indistinguishable and labelled Wyoming) and "District of Columbia, VA". Expected: "Moscow, ID", "Idaho Falls, ID", "Boise, ID", "Coeur d'Alene, ID", "Seattle, WA", "Washington, DC", "Denver, CO" — with the GSA locality shown as a secondary detail (e.g. "GSA standard CONUS rate", "Denver / Aurora NSA"). New trips start with every night on "Standard Rate, WY"; the default should be a real, named destination.
-
Itinerary table columns are misaligned with their headers (desktop and phone). The header has 8 columns (Day, Date, Destination, Lodging cap, M&IE, Provided meals, Personal, Allowance) but each row has 7 cells, so the destination select sits under "DATE", the lodging cap under "DESTINATION (NIGHT)", M&IE under "LODGING CAP", the B/L/D checkboxes under "M&IE", the personal checkbox under "PROVIDED MEALS" and the allowance under "PERSONAL"; the ALLOWANCE header has nothing under it. Also show the day type (Departure / Full day / Return, 75% badge) that the code computes but never displays.
-
Chart empty-state box stays visible under real charts. With dates and receipts entered, the charts render but the dashed box "Charts appear once the trip has dates and at least one receipt." is still shown below them. The by-category chart also leaves a large blank area below its three bars at desktop; its labels are tiny at 390 px.
-
Two "+ Add traveler" buttons are stacked under the traveler list; the lower one does nothing when clicked. Expected a single working button.
-
Personal days do not change the math. Ticking "Personal" on the Jun 10 night only adds an info note; the allowance, lodging total, charts and summary are unchanged. Expected: a personal night/day is excluded from M&IE and lodging allowance (shown struck-through or greyed with a "personal" badge), any receipts dated on it are flagged as personal/non-reimbursable, and the summary lists personal days. The per-traveler "Personal" checkbox next to each traveler name is confusing (a traveler is not a day) — replace it with something meaningful (e.g. traveler role/department) or remove it.
-
"Legs" are missing from the trip builder. SPEC asks for travelers, legs, dates and destination per night. There is no way to enter legs (e.g. "Jun 8 · Moscow → Denver · Air", "Jun 10 · Denver → Seattle · Air", "Jun 11 · Seattle → Moscow · Air"). Expected: an add/remove list of legs (date, from, to, mode) that pre-fills the destination for each night (nights after a leg arriving in Denver default to Denver) and appears in the summary, CSV and print view.
-
Multiple travelers are ignored in the totals. With two travelers the summary still shows a single per diem figure. Expected: per-traveler allowance plus a trip total for all travelers (or receipts assignable to a traveler), stated clearly in the summary, CSV and print view.
-
Receipts table looks unfinished. The Amount and Notes inputs are plain browser boxes with thin black borders (unlike the styled selects/date inputs), the AMOUNT header is not above its column, amounts re-display as "189.0" and "38.5" instead of "189.00"/"38.50". At 390 px the receipts table scrolls sideways with the category column cut off ("ry", "ng") and the amount cut off ("AMOU", "250"). Expected: consistent styled inputs, right-aligned money, and at phone width each receipt shown as a stacked card (thumbnail, category, date, amount, notes, delete) with no sideways scrolling. The itinerary table at 390 px has the same sideways-scroll problem (only DAY and DATE visible).
-
Drag-and-drop receipts need a real drop zone. The "Drop images here or browse…" control is a small pill button. Dropping an image onto the table silently appends a new blank "Lodging" row with no date and no amount. Expected: a large, obvious dashed drop zone (highlighted gold while dragging over the card), each dropped image creating a receipt row whose date defaults to the trip start and whose amount field receives focus, with a short "1 receipt added — enter the amount" toast. Clicking a thumbnail should open a larger preview with a close button and Escape-to-close.
-
Print summary is not a clean printable document. In print media the page hides the itinerary and the receipt list but still prints the editable trip form (inputs, "+ Add traveler" buttons), the empty by-day chart and the chart empty-state box. Expected: a one-to-two page reimbursement summary — trip title, dates, travelers, legs, per-day per-diem table (date, destination, lodging cap, M&IE, deductions, allowance), receipt list with small thumbnails, reconciliation flags, totals, GSA FY2026 source line and the "U of I APM governs — verify rates" note — with all buttons and form controls hidden.
-
Save/load is a single hidden slot. "Save trip" overwrites one saved trip and reloading the page shows an empty form until "Open saved trip" is clicked. Expected: saved trips listed by title and date (open, delete), plus automatic restore of the current draft after a page reload.
-
First-visit experience and polish. The intro card leaves the right half empty on desktop. Add a "Load example trip" button (clearly labelled as a sample, e.g. a Boise conference trip) so a first-time visitor immediately sees filled tables, flags and charts; add a live totals strip (per diem, receipts, difference, number of flags) that stays visible near the top; make reconciliation items clickable to scroll to and briefly highlight the related night or receipt row; show a green "All checks passed" state when there are no issues. Keep transitions smooth (150–250 ms) and keep the U of I brand rules in BRAND.md.
-
Typing into a receipt's Amount or Notes field is broken at normal typing speed. Click the Amount input of a new receipt row and type
123.45at about 4 keys per second: the field ends up containing1.00and keyboard focus jumps to the page body, so the remaining keys are lost. TypingTaxi to hotelin Notes leaves justT. Clicking into an existing amount189.00and typing produced201189.00, and the reconciliation then flagged "$201,189.00 exceeds the Denver / Aurora cap". Cause visible from the outside: shortly after every keystroke the whole receipts table is rebuilt (and amounts are re-formatted to two decimals), destroying the input being typed in. Expected: the user can type freely in every receipt field (category, date, amount, notes) and in the itinerary selects/checkboxes without losing focus or characters; totals, flags, charts and summary still update live; amounts are formatted to two decimals only when the field loses focus. Traveler and leg text fields already keep focus — receipts must behave the same. -
Chart empty-state box is still shown below real charts. With a trip and receipts, "5 · Spending vs. allowance" shows both charts AND the dashed box "Charts appear once the trip has dates and at least one receipt." underneath. Expected: the box only when there is nothing to chart.
-
By-category chart bars are all dark grey/black (Lodging, Airfare, Registration, Meals all the same near-black colour). In round 1 each category had its own colour. Expected: one consistent brand series colour per category (the same colour for a category everywhere, with a small legend or coloured label), hover state, tooltip kept. The by-day chart's y-axis labels are raw numbers like
$225418,$169063— format as$225k/$1,200with thousands separators and round tick values. -
Travel legs editor looks unfinished on desktop. Each leg is stacked as five full-width lines (date, From text, → arrow at the far right, destination select, mode select, then a lone × on its own line). Expected at desktop: one compact row per leg —
[date] [From] → [To destination] [mode] [×]— with a small leg number, and at 390 px a neat stacked card. Same polish as the traveler rows. -
Phone (390 px): the per-day summary table is cut off. In "6 · Summary for reimbursement" the Date / Destination / Lodging cap / M&IE / Deductions / Allowance table runs past the card edge — "DEDU…" is clipped and the Allowance column is not visible; with a filled trip the page scrolled 127 px sideways. Expected: no sideways page scroll at 390 px; show that table as stacked day cards (like the itinerary cards) or fit it.
-
Phone (390 px): receipt cards and drop zone need layout polish.
-
The drop zone squeezes its text into two narrow side-by-side columns ("Drop receipt images here — or press Enter / click to browse." next to "Stored only in this browser as small thumbnails — nothing is uploaded."). Stack them.
-
In each receipt card the Amount input is narrower and starts further left than Category and Date; Notes sits on a separate line with its label above; the delete × floats alone at the bottom right; the "IMG" label sits far from its thumbnail slot. Expected: a tidy card — thumbnail + category/date on top, amount prominent, notes full width, delete as a clear small button in the card corner.
-
The receipts total row reads "TOTAL Total" and "AMOUNT $908.65" (duplicated labels). Expected: "Total $908.65".
-
-
"Receipts − per diem" compares the wrong things. It subtracts the per-diem allowance (M&IE + lodging caps) from ALL receipts including Airfare and Registration, which per diem does not cover — e.g. the example trip shows "−$239.35 within allowance" even though a $204.50 hotel night is over cap. A business officer needs: lodging receipts vs lodging cap (per night, over-cap amount non-reimbursable), meals vs M&IE, and transport / registration / other reimbursed at cost. Expected: an "Estimated reimbursement" = M&IE allowance + lodging (actual, capped per night) + other actual costs, with "not reimbursable" (over-cap + personal) shown separately — in the totals strip, summary, CSV and print document. With two travelers, let each receipt be assigned to a traveler (default "shared / all") so per-traveler totals make sense.
-
Sticky totals strip covers content. When scrolling, the strip sits over form fields (it hid the Home ZIP / Departure row and the "+ Add row / Export CSV / Print summary" buttons in screenshots). Clicking a reconciliation flag ("→ jump") scrolls the target row to a position that can be under the strip. Expected: sections scroll so targets land below the strip (scroll margin), and the strip is compact enough not to hide controls (on phone, consider a slim one-line version).
-
Dropping an image does not focus the new row's Amount field. After dropping
hotel-receipt.pngthe toast "1 receipt added — enter the amount" appears but no field has focus. Expected: the new row scrolls into view, briefly highlights, and its Amount input is focused (this likely depends on item 1). -
Receipts table header alignment (desktop). The AMOUNT header sits ~90 px left of the amount inputs; the Notes inputs are narrow (text like "Moscow–Boise round trip (each)" is cut off) while a wide empty gap sits before the ×. Expected: headers aligned with their columns, notes column taking the spare width.
-
Itinerary return row text runs together: "last day — no overnightrate basis: Seattle, WA" (no space / line break). Expected "last day — no overnight" with "rate basis: Seattle, WA" on its own small line.
-
Summary stat cards leave an orphan. Six stat cards in a five-column grid put "Receipts − per diem (all)" alone on a second row with a large empty area beside it. Expected a balanced grid (e.g. 3 × 2 at desktop, 2 × 3 or 1 column on phone).
-
Toast position on phone — "Example trip loaded — sample data, not a real U of I trip" appears in the middle of the screen over the itinerary/summary text. Expected: bottom of the viewport, not covering content, short fade.
-
CRITICAL — the itinerary never appears. On a fresh visit (empty browser storage) set Start date 2026-06-08 and End date 2026-06-11 (by typing or with the date picker): "2 · Itinerary & lodging by night" still shows the empty state "Set the start and end dates above…" and has no rows, so there is no way to choose a destination per night, tick provided meals or mark personal days. The same happens after pressing Reset and entering dates, after editing the trip title, and after changing the end date. Meanwhile the summary silently computes 4 days with every night at Moscow, ID ($110 cap), so a Denver trip gets wrong flags ("Lodging Jun 8: $229.10 exceeds the Standard Rate cap of $110.00"). This worked in round 2. Expected: as soon as both dates are valid, one itinerary row per day appears (and rows are added/removed when dates change), and the empty state is hidden.
-
CRITICAL — loaded trips show empty tables. Click "Load example trip", or open a saved trip, or reload the page (draft restore): the trip title, dates, travelers, legs, totals strip, flags and summary all show the loaded trip, but the itinerary table has 0 rows (empty state visible) and the receipts table has 0 rows with "No receipts yet." shown under a "Total $908.65" row. On phone the charts card also shows only "Charts appear once the trip has dates and at least one receipt." Pressing "+ Add row" makes all receipts reappear, but the itinerary stays empty. Expected: loading the example, opening a saved trip and restoring the draft render every section (itinerary rows, receipt rows with thumbnails, charts) exactly as if the user had entered the trip by hand.
-
Desktop receipts table layout is collapsed. The header has six columns (Img, Category, Date, Amount, Notes, actions) but each row now has only three cells: the thumbnail, one wide cell under "CATEGORY" with the category select, date, amount and traveler select stacked vertically, and a cell with Notes and the × button. The Date, Amount and Notes columns are empty, and each receipt is ~150 px tall. Expected at desktop: one compact line per receipt — thumbnail | category | date | amount (right-aligned) | traveler | notes | × — each under its own header (add a "Traveler" header). Keep the stacked card layout at phone width only.
-
Category chart bars ignore their colours. The legend under "By category" shows Lodging = teal, Airfare = leather/brown, Registration = gold, Meals = purple, but every bar is drawn teal. Something gives every bar the first series colour and overrides the per-category colour. Expected: each bar in its legend colour.
-
"Estimated reimbursement" double-counts meals and does not add up. For the example trip (2 travelers, all receipts "shared") the app shows Estimated reimbursement $1,087.15 = "M&IE $192.00 + lodging $369.00 (capped) + meals $64.05 + other $462.10", per-traveler cards A. Faculty $830.83 and B. Staff $830.83 (these sum to $1,661.66, not $1,087.15), and "Reimbursement vs. receipts +$178.50 — reimbursement exceeds receipts — check the figures". Problems: meal receipts are added on top of the M&IE allowance (M&IE per diem already covers meals, so meal receipts are not reimbursed separately); M&IE is counted for one traveler although there are two; the per-traveler cards use a different formula from the total. Expected model and numbers for the example trip:
-
M&IE allowance = $192.00 per traveler × 2 = $384.00
-
Lodging = actual lodging receipts capped per night: Jun 9 $204.50 → $191.00, Jun 10 $178.00 → $369.00
-
Other costs at actual (airfare, registration, mileage, parking, …): $342.10 + $120.00 = $462.10
-
Estimated reimbursement = $1,215.10; Not reimbursable = $13.50 (over-cap lodging, plus any personal-day receipts)
-
Meal receipts ($64.05) are listed as "covered by M&IE — not added"
-
Per traveler (shared receipts split equally): $192.00 + $184.50 + $231.05 = $607.55 each; the per-traveler amounts must add up to the estimated reimbursement. Replace the confusing "Reimbursement vs. receipts" card with something meaningful (e.g. "Out of pocket" = receipts not reimbursed). Keep the totals strip, summary, CSV and print document consistent with these numbers.
-
-
Travel legs card at 390 px is jumbled. The → arrow sits on the far left outside the fields, the × delete floats under the From field, the leg number badge sits at the right edge halfway down, and the destination select text is cut off ("Moscow, ID · GSA standard CON"). Expected: a tidy card — "Leg 1" label with × on the same top line, then date, "From", "To" destination (full text visible, wrap or shorter label), and mode, each full width.
-
Phone summary is very long. Nine stat cards stack one per row (about 2.5 screens) before the useful tables. Expected: a compact two-column grid of small stat tiles on phone, with the headline figures (estimated reimbursement, not reimbursable, flags) first.
-
CRITICAL (regressed again) — typing in receipt fields loses focus after the first key. Load the example trip, clear the first receipt's Amount (342.10) and type
350.25at any speed (tested at 40 ms, 150 ms and 300 ms per key): after the first key the field loses focus and the value becomes3.00; the rest of the keys are lost. In Notes, pressing End and typingextrainserts a stray space in the middle ("Moscow–Boi se round trip (each)"). Clicking "+ Add row" no longer puts focus in the new row's Amount field, so typing immediately goes nowhere. Pasting a whole value works; typing does not. Round 3 had this working. Expected: while a receipt input has focus, nothing rebuilds or re-formats that input; each keystroke only updates state and refreshes the other panels (totals strip, flags, charts, summary, allowance column); amounts are formatted to 2 decimals only on blur; "+ Add row" focuses the new Amount input. The same must hold for date, category and traveler controls. -
Itinerary "Allowance" column is always empty. In "2 · Itinerary & lodging by night" the ALLOWANCE header has no values in any row — for the example trip, for a hand-entered Denver trip, and after ticking provided meals. (The summary's "Per diem by day" cards do show them.) Expected for the example trip: $255.50, $254.00, $64.50; after ticking B on Jun 10 it should update to $232.00 immediately.
-
Per-traveler cards are still wrong and do not add up. The example trip shows A. Faculty $742.60 and B. Staff $742.60 = "M&IE $96.00 + lodging $184.50 + other $462.10" (sum $1,485.20, not the $1,215.10 total). Each traveler receives the full M&IE allowance, not half, and shared "other" costs must be split, not given in full to each. Expected: $607.55 each = M&IE $192.00 + lodging $184.50 + other $231.05; the per-traveler amounts must always sum to the Estimated reimbursement (also in the print document and CSV).
-
"Out of pocket" counts meals that M&IE already covers. It shows $77.55 (= $13.50 over-cap lodging + $64.05 meal receipts). Meal spending is covered by the $384.00 M&IE allowance, so it is not out of pocket. Expected for the example trip: $13.50 (over-cap lodging + personal-day receipts only).
-
Small polish:
-
The receipts table footer row lost its "Total" label (desktop shows just "$908.65"; phone card shows "AMOUNT $908.65"). Expected "Total $908.65".
-
On phone the stat tiles cut their explanations off with "…" ("provided-meal deductions $23.00 ·…", "above the cap) — the rest…"). Let the text wrap instead of truncating.
-
Desktop summary: with 10 tiles in a 3-column grid, "B. Staff" sits alone on the last row. Put the per-traveler tiles in their own row (e.g. a small "Per traveler" table or equal-width tiles spanning the row).
-
On phone, the receipt card's Category select is half width while Traveler and Notes are full width; make the fields a consistent width.
-
-
Keyboard flow between receipt fields loses focus.
-
Click into a receipt's Amount, type
229.10, press Tab: focus drops to the page body instead of moving to the next field (Traveler select), so anything typed next is lost. It looks like the row is rebuilt when the Amount field loses focus (to re-format it), which destroys the element Tab just moved to. -
Click into the Notes field of the first example receipt and immediately press End then type
x: the End key is lost and " x" lands in the middle ("Moscow–Boi xse round trip (each)") — the same rebuild-on-blur happening right after the click. -
Clicking "+ Add row" does not put focus in the new row's Amount field (focus is on the page body). Expected: re-format the amount in place (just set that input's value to 2 decimals) without rebuilding the row or table, so Tab / Shift+Tab move normally through Category → Date → Amount → Traveler → Notes → ×, and "+ Add row" focuses the new Amount input. Rebuild the receipts table only when rows are added, removed or loaded.
-
-
Allowance column does not update when a provided-meal box is ticked. Load the example trip, tick B on the Jun 10 row: the row's Allowance stays $254.00; expected $232.00 immediately ($86 − lunch $23 − breakfast $22 + $191 lodging cap). Untick → back to $254.00. Also make sure the column updates immediately when a destination is changed or Personal is ticked. The totals strip and summary already update — the column must too.
-
Per-traveler reimbursement is still wrong (asked twice). The example trip shows A. Faculty $511.55 and B. Staff $511.55 = "M&IE $96.00 + lodging $184.50 + other $231.05". These sum to $1,023.10, but the Estimated reimbursement right above is $1,215.10 = "M&IE $384.00 (2 travelers) + lodging $369.00 + other $462.10". The M&IE allowance is a per-person entitlement: each traveler gets the full per-traveler M&IE ($192.00 — the same figure shown in the "Per diem (per traveler)" tile). The $384.00 in the total is 2 × $192.00; it must not be halved again. Lodging and shared "other" receipts are split equally (they are one set of receipts). Expected: A. Faculty $607.55 = M&IE $192.00 + lodging $184.50 + other $231.05, B. Staff $607.55; $607.55 + $607.55 = $1,215.10. Apply the same numbers in the print document and CSV.
-
Phone receipts total card is empty. At 390 px the last card under the receipt cards is an empty bordered box. Expected "Total $908.65" in it (desktop already shows "TOTAL $908.65").
-
Focus is stolen ~60–100 ms after it lands — a delayed refresh rebuilds the receipts rows. Measured with the example trip loaded:
-
Click the first receipt's Amount (342.10) and press Tab. At 0 ms focus is correctly on that row's Traveler select; 100 ms later focus is on the page body. Pressing Tab from the Category select (no amount change) keeps focus on Date — so the problem is whatever runs shortly after the Amount field's change/blur.
-
Click "+ Add row". At 0 ms focus is correctly in the new row's Amount input; 60 ms later focus is on the page body, so anything typed right away is lost. The same happens after choosing an image with the drop zone's file picker (the new row's Amount is not focused afterwards). This is the signature of a debounced/queued re-render (a timer of roughly 100 ms) that rebuilds the receipts table after the event. Expected: no scheduled refresh may rebuild the receipts table rows; after adding a row or editing a field, the delayed refresh must only update other panels (totals strip, flags, charts, summary, allowance column) — or it must restore focus and caret to the same field in the same row. Verify by checking which element is focused 300 ms after each action.
-
-
Allowance column ignores provided-meal checkboxes. Load the example trip, tick B on the Jun 10 row: the totals strip updates (Per diem (all) $1,148.00 → $1,104.00) and the summary's "Jun 10 · Boise, ID" card shows Allowance $232.00, but the itinerary row's Allowance cell stays $254.00 — even after clicking elsewhere. Destination and Personal changes already update the cell; the B / L / D checkboxes must do the same.
-
Phone receipts total card is still empty. At 390 px the last element in "3 · Receipts" is an empty rounded box with no text. Expected "Total $908.65" inside it (desktop shows "TOTAL $908.65" correctly).
-
Phone (≤ 560 px): the receipts total card shows no amount. Load the example trip at 390 px wide and scroll to the end of "3 · Receipts". The last card contains two thin horizontal rules and a small centred "TOTAL" label, but the amount $908.65 is not visible (at desktop the footer correctly reads "TOTAL $908.65"). Expected at phone width: one tidy card, no stray rules, reading Total on the left and $908.65 (bold, tabular numbers) on the right.
-
Repro: Reset. Dates 2026-06-09 to 2026-06-10, night of Jun 9 = Boise. Two travelers "Ann" and "Bo". Add two Lodging receipts dated Jun 9 for $180.00 each, Traveler = Ann on one and Traveler = Bo on the other. Actual: "Lodging Jun 9: $360.00 exceeds the Boise cap of $191.00 by $169.00", plus "Possible duplicate: two lodging receipts of $180.00 on Jun 9". Not reimbursable = $169.00, and lodging reimbursed = $191.00. Expected: no flags. Lodging reimbursed = $360.00, not reimbursable = $0.00, estimated reimbursement = M&IE $258.00 + lodging $360.00 = $618.00. Each traveler's card shows their own lodging ($180.00 each). Rule to implement everywhere (lodgingReimb, reconcile, perTravelerAlloc, summary day cards, CSV, print): a Lodging receipt assigned to one traveler is checked against that traveler's nightly cap;
-
a "shared / all" Lodging receipt covers every traveler, so check it against cap x number of travelers and split the reimbursed amount equally;
-
over-cap = per traveler, then summed.
-
The duplicate check must NOT flag two receipts with the same category, date and amount when they are assigned to different travelers.
-
Print CSS styles
.print-doc__hand.print-doc__sub, but the markup usesprintdoc__handprintdoc__sub. Align the names. -
On desktop the receipts Notes input is only about 180 px wide, with empty space before the x button. Let Notes fill its column.
-
Now: 2 travelers (Ann, Bo), night of Jun 9 2026 in Boise (cap $191). Two Lodging receipts of $180.00 dated Jun 9, one with Traveler = Ann and one with Traveler = Bo. The app says "$360.00 exceeds the Boise cap of $191.00 by $169.00" and "Possible duplicate". Not reimbursable shows $169.00. Wanted: no flags, lodging reimbursed $360.00, not reimbursable $0.00, estimated reimbursement $618.00. Rule (use it in lodgingReimb, reconcile, perTravelerAlloc, the summary, CSV and print): a Lodging receipt with a traveler set is compared with that traveler's cap for that night;
-
a "shared / all" Lodging receipt is compared with cap x number of travelers and split equally;
-
the duplicate check ignores receipts that are assigned to different travelers.
-
reconcile(): one "bad" flag per traveler whose over_t > 0 (show the traveler name only when n > 1).
-
lodging reimbursed = sum of all reimb_t; over-cap lodging = sum of all over_t; each traveler card uses its own reimb_t.
-
summary day card: "Lodging cap $191.00 x 2", "Lodging actual" = sum of the amounts, "over" = sum of over_t.
-
uidaho_apm_70-02_travel.txt: APM 70.02 §I, the noon / 6 p.m. proration -
uidaho_apm_70-04_travel_entitlements.txt: APM 70.04 §E, which prints an outdated $45 in-state maximum -
idaho_sbex_442-50_state_travel_policy.txt: State Travel Policy, Appendix B, In State $58.00 day; §6.C, meal percentages -
ecfr_41cfr_part_301-11_2026-09-01.txt: 41 CFR §301-11.20 (75%) and §301-11.21 (meal deductions) -
A reviewer checked the current app in real Chrome. It has these problems against the decision: The federal rule is the default (
input[name=proration]:checkedisfederalon a fresh load). -
The APM option uses the GSA locality M&IE even for Idaho trips (Boise $86.00 instead of the Idaho maximum).
-
Under the APM option the day badge still says "75% (APM: by time)".
-
The CSV never says which rule was used.
-
Provided meals are deducted with GSA amounts under both rules.
-
The federal rule can drop to $0.00 when all meals are provided on the first or last day.
-
The federal citation "FTR §301-11.101" is out of date: the FTR was renumbered on Dec 8, 2025, and the rule is now 41 CFR §301-11.20.
-
Trip bar radio group, in this order, with these exact visible labels:
-
value
uofi: "University of Idaho rule (APM 70.02)" plus a small line "Default · 100% or 50% by noon departure / 6 p.m. return · Idaho in-state $58.00/day" -
value
federal: "Federal rule (GSA: 75% of M&IE on first and last day) for federally sponsored travel"
-
-
On a fresh load with empty localStorage, after Reset, and after Load example trip,
uofiis checked. A snapshot or saved trip with norule, or with the old value"apm", loads asuofi. A saved"federal"stays federal. -
Departure and return selects: use the policy wording in the option text.
-
Departure: "Trip begins before 12 p.m. (noon) — 100%" / "Trip begins after 12 p.m. (noon) — 50%".
-
Return: "Trip concludes after 6 p.m. — 100%" / "Trip concludes before 6 p.m. — 50%".
-
Hint under both: "APM 70.02 does not define exactly 12:00 or 6:00 — check with Travel Services".
-
Under the federal rule, the hints say "Not used by the federal rule (75% on first and last day at any time)".
-
-
The day badge shows the real percentage for the active rule:
100%or50%under U of I,75%or100%under federal. Remove "(APM: by time)". -
The M&IE column shows the rate actually used plus a tiny note. Under U of I: "$58.00 · Idaho in-state max" on Idaho days and "$92.00 · GSA" out of state. Under federal: the GSA M&IE.
-
The Allowance cell and the chart use the new amounts.
-
Real-browser condition to verify: switching the radio immediately updates every badge, M&IE cell and Allowance cell without reloading, and keeps focus behaviour as it is now.
-
Add a card "Per-diem rules" directly below the trip bar and above "How to use". Use
<details>for the long quotes so the page stays short; it must fit a 390 px phone with no horizontal overflow. The active rule gets a gold "Active" badge. Clicking a rule's heading or the radio selects it. -
University of Idaho rule (default).
-
Quote the APM 70.02 §I sentences.
-
Explain the rate: Idaho destinations use the State of Idaho in-state maximum, $58.00/day (State Travel Policy SBEX 442-50, App. B, amended Aug 20, 2026).
-
Explain the outdated figure: "APM 70.04 §E still prints $45 (last updated Aug 2017, under revision); $45 was the State rate from Oct 1, 2015".
-
Out-of-state destinations use the GSA M&IE of the locality.
-
Provided meals: allowance for the meals not provided, 25% breakfast / 35% lunch / 55% dinner of the daily rate (SBEX 442-50 §6.C).
-
Links:
-
https://www.uidaho.edu/policies/apm/70/02
-
https://www.uidaho.edu/policies/apm/70/04
-
https://www.sco.idaho.gov/ViewerFiles/StateTravelPolicy/docs/State-Travel-Policies-and-Procedures.pdf
-
-
-
Federal rule, for federally sponsored travel.
-
75% of GSA M&IE on the first and last day, 100% on full days (quote 41 CFR §301-11.20(a)).
-
Provided meals: deduct the GSA breakdown amount (from the 75% on first and last day), never below incidentals (quote §301-11.21(b)).
-
Links:
-
https://www.ecfr.gov/current/title-41/section-301-11.20
-
https://www.ecfr.gov/current/title-41/section-301-11.21
-
https://www.gsa.gov/travel/plan-a-trip/per-diem-rates/mie-breakdowns
-
-
-
Replace every "§301-11.101" in the app with "41 CFR §301-11.20".
-
Update the intro bullet "Per diem = M&IE (75% first & last day) + lodging cap per night" so it describes the active rule.
-
In "Data & sources", add the four policy documents with the retrieval date 2026-09-13.
-
Sticky totals strip: under "Per diem (all)" show a small line: "U of I rule" or "Federal rule".
-
Summary (section 6):
-
The first block is a prominent rule banner, for example: "Per-diem rule: University of Idaho (APM 70.02) · Idaho in-state $58.00/day · departure before noon → 100% · return after 6 p.m. → 100%".
-
For the federal rule: "Per-diem rule: Federal (GSA 75% first & last day) — federally sponsored travel".
-
"Per diem by day" shows each day's percentage and rate basis.
-
-
Print view (
buildPrintDoc):-
Add a "Per-diem rule" row near the top of the meta table with the full rule name, the citation (APM 70.02 §I + SBEX 442-50 App. B / §6.C, or 41 CFR §301-11.20–.21) and the departure/return choices.
-
The per-day table gets "%" and "Rate basis" columns.
-
The footer names the active rule. Do not say "APM 70.02 governs" when the federal rule is selected; say instead that it was selected for federally sponsored travel.
-
-
CSV:
-
Row 2 must be
Rule,,"<full rule name> — <citations>; departure: <choice>; return: <choice>",. -
Each "Per diem" row's detail includes the percentage and rate basis, for example
Boise, ID — first day, 100% of 58.00 (Idaho in-state max), provided: none. -
The M&IE total row names the rule:
M&IE allowance (2 travelers, University of Idaho rule).
-
-
python3 build.py --checksucceeds. -
All 7 table rows in item 3 match under both rules; check them in Node.
-
A fresh load shows
uofichecked. -
No text in the app says "75% (APM: by time)" or "§301-11.101".
-
Everything listed at the top still works.
-
Round 12 did most of the owner's decision correctly, and all of it must keep working: U of I rule is the default. The federal label is exact. Old
"apm"drafts load as U of I. Saved"federal"stays federal. -
The rules card with quotes, links and the $58 / $45 note is in place, and the totals strip shows the rule.
-
Summary banner and CSV row 2 are correct.
-
All 7 reference trips match to the cent under both rules. Example trip: $1,088.80 / $1,865.00 / $933.50 / $931.50 (U of I) and $1,148.00 / $1,924.20 (federal).
-
No phone overflow.
-
Checkable condition: load the example trip, then run
buildPrintDoc()in the console. It must return a string containing "Per-diem rule" and "University of Idaho", without throwing. -
Clicking Print summary must leave
#print-docwith non-emptyinnerHTMLand no page error. -
After switching to the federal rule, the string must contain "Federal" and must not contain "APM 70.02 governs".
-
Choose the federal rule, then click Reset and confirm. The federal radio stays checked (
state.rule === "federal").clearTrip()never resets the rule or the time selects. Reset must also do: setstate.rule = "uofi"and check theuofiradio; -
move the "Active" badge to the U of I card;
-
set
#trip-dep-timetoamand#trip-ret-timetoevening; -
update the hints, then render.
-
Add a heading "Per-diem rules" at the top of the rules card, with one line under it: "Choose the rule for this trip — University of Idaho policy is the default."
-
Data & sources, University policy entry: replace "pick the rule in the trip bar" with "pick the rule in the Per-diem rules card".
-
Data & sources footer note: replace "All figures are GSA published rates for FY2026 … No University of Idaho financial figures are included" with text saying the rates are GSA FY2026 published rates plus the State of Idaho in-state maximum of $58.00/day (SBEX 442-50, App. B). Traveler and receipt entries remain your own data.
-
On a fresh 1440×900 load the Per-diem rules card fills the whole first screen and the "Enter a trip once" intro starts below the fold; on a 390 px phone the rules card is about two screens tall before the user sees anything else. Move the intro card (How to use + "New here? Start from a sample") directly under the trip bar, above the rules card.
-
Make the rules card compact: keep the heading, the two radio options side by side (stacked on phone), each option's one-line subtitle, the Active badge and the citation links visible. Put each option's bullet list together with its "Full policy text" quotes inside that option's collapsed details element, whose summary reads "How this rule works — full policy text". Keep every existing sentence, number and link; nothing is deleted, only collapsed.
1 build 18.9 min · 6 files changed, 2487 insertions(+), 3 deletions(-) Show prompt
Read AGENTS.md, SPEC.md and BRAND.md in this directory. Then build the complete app they describe, step by step: first write scripts in scripts/ that turn the raw GSA FY2026 per-diem data in data/raw/ (see data/raw/SOURCES.md, which documents every file's format, quirks and the U of I APM policy note) into compact JSON in data/embed/ and run them; 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.
2 feedback 35.0 min · 7 files changed, 1904 insertions(+), 590 deletions(-) Show prompt
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.
Reviewer findings — round 1 (tested index.html in Chromium at 1440 px and 390 px)
Test trip used: Start 2026-06-08, End 2026-06-11, two travelers, nights = Denver, Denver, Seattle, lunch provided Jun 9, breakfast + dinner provided Jun 10, receipts: Lodging Jun 8 $239.40, Lodging Jun 9 $189.00, Airfare Jun 8 $412.18, Meals Jun 9 $38.50 twice.
Most important first.
3 feedback 12.4 min · 5 files changed, 874 insertions(+), 304 deletions(-) Show prompt
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.
Reviewer findings — round 2 (tested index.html in Chromium at 1440 px and 390 px)
Big improvement. These now WORK and must be kept: correct lodging caps and allowances (Denver/Seattle test trip gives $284 / $281 / $279 / $69 federal, APM noon/6 p.m. rule correct), over-cap flags, destination names, aligned itinerary columns, personal days excluded, legs pre-filling nights, example trip, sticky totals strip, drop zone highlight, lightbox with Escape, saved-trip list + draft restore after reload, the dedicated print document, CSV export, itinerary cards at 390 px, zero console errors, no external requests.
Most important first.
Keep the U of I brand rules (BRAND.md). Re-run python3 build.py --check when done.
4 feedback 21.5 min · 5 files changed, 372 insertions(+), 434 deletions(-) Show prompt
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.
Reviewer findings — round 3 (tested index.html in Chromium at 1440 px and 390 px)
Now WORKING — keep: typing in receipt amount/notes keeps focus (fixed), chart empty-state box hides correctly, by-day chart y-axis ticks ($0 … $700) and bars look good, legs are one compact row on desktop, dropped image focuses the new Amount field, toast sits at the bottom on phone, print document, CSV, saved-trip list, over-cap flags, zero console errors, no external requests, no sideways scroll at 390 px.
Two changes in round 3 broke the core of the app. Fix them first.
Before finishing, walk through these flows and make sure each one really works: (a) fresh page with no saved data → enter dates → itinerary rows appear → pick Denver for each night → tick lunch on day 2; (b) add receipts and type amounts; (c) Load example trip → itinerary has 3 rows, receipts table has 6 rows, charts drawn; (d) reload the page → the same trip is fully shown; (e) Save trip, Reset, open the saved trip → fully shown. Then run python3 build.py --check.
5 feedback 4.2 min · 4 files changed, 145 insertions(+), 90 deletions(-) Show prompt
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.
Reviewer findings — round 4 (tested index.html in Chromium at 1440 px and 390 px)
Now WORKING — keep all of this: itinerary appears on fresh dates and grows/shrinks with date changes; example trip, saved trips and page reload render every section; compact one-line receipt rows with a Traveler column; category bars in their legend colours; estimated reimbursement $1,215.10 and not reimbursable $13.50 for the example trip; "Meals — covered by M&IE" tile; tidy legs cards; print document; CSV; zero console errors; no sideways scroll at 390 px.
Short list this time — please fix exactly these and re-test each one before finishing.
Regression checklist to run before finishing (each must pass): (a) fresh page → type dates → 4 itinerary rows with Allowance values; (b) "+ Add row" → type 229.10 key by key → field shows 229.10 with focus still in it → Tab → notes typed key by key; (c) Load example trip → 3 itinerary rows with $255.50 / $254.00 / $64.50, 6 receipt rows, both charts, estimated reimbursement $1,215.10, per traveler $607.55 each, out of pocket $13.50; (d) reload the page → same; (e) Save trip → Reset → open saved trip → same. Then run python3 build.py --check.
6 feedback 2.2 min · 4 files changed, 128 insertions(+), 83 deletions(-) Show prompt
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.
Reviewer findings — round 5 (tested index.html in Chromium at 1440 px and 390 px)
Now WORKING — keep all of this: typing key by key in an existing receipt's Amount keeps focus (350.25 typed fine on desktop and phone); itinerary Allowance column shows $255.50 / $254.00 / $64.50 for the example trip; out of pocket $13.50; desktop receipts footer "TOTAL $908.65"; per-traveler tiles in their own row; example trip, reload and saved trips render every section; print document; CSV; zero console errors; no sideways scroll at 390 px.
Only four items remain. Please fix exactly these and re-test each one.
Regression checklist before finishing: example trip → (a) first receipt Amount: select all, type 350.25, Tab → focus is on the Traveler select and Amount shows 350.25; (b) "+ Add row" → type 77.70 immediately → Amount shows 77.70; (c) tick B on Jun 10 → Allowance $232.00; (d) per traveler $607.55 each, sum $1,215.10; (e) phone total card shows "Total $908.65"; (f) fresh page → dates → itinerary rows; reload → trip restored. Then run python3 build.py --check.
7 feedback 27.3 min · 4 files changed, 327 insertions(+), 58 deletions(-) Show prompt
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.
Reviewer findings — round 6 (tested index.html in Chromium at 1440 px and 390 px)
Now WORKING — keep all of this: per-traveler $607.55 + $607.55 = $1,215.10 (summary, print, CSV); Allowance column updates when a destination changes (Jun 9 → Seattle gives $317.00) and when Personal is ticked; fresh-page dates build the itinerary with allowances; Denver trip $284 / $307 / $307 / $69; notes typing; reload and saved trips; zero console errors; no sideways scroll at 390 px.
Three items remain. The first two have the same cause, measured precisely below.
Checklist before finishing (example trip loaded): (a) click first Amount → select all → type 350.25 → Tab → wait 300 ms → focus is on the Traveler select and Amount shows 350.25; (b) "+ Add row" → wait 300 ms → focus is in the new Amount → type 77.70 → it shows 77.70; (c) tick B on Jun 10 → the row's Allowance cell reads $232.00; untick → $254.00; (d) phone 390 px: receipts total card reads "Total $908.65". Then run python3 build.py --check.
8 feedback 4.1 min · 3 files changed, 23 insertions(+), 43 deletions(-) Show prompt
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.
Reviewer findings — round 7 (tested index.html in Chromium at 1440 px and 390 px)
Excellent — everything from the last list now works in a real browser: Tab / Shift+Tab through receipt fields keeps focus (checked at 0, 300 and 1000 ms), "+ Add row" and image upload focus the new Amount and typing sticks, B / L / D checkboxes update the Allowance cell ($254.00 → $232.00 → $254.00), per traveler $607.55 × 2 = $1,215.10, reload and saved trips, print document, CSV, zero console errors, no sideways scroll at 390 px. Do not change any of that.
One small item remains. It is a phone-only CSS problem — please fix it in src/app.css only and do not modify src/app.js or src/body.html behaviour.
Before finishing, check at 390 px that the card shows both "Total" and "$908.65", check at 1440 px that the desktop footer still reads "TOTAL $908.65" under the Amount column, then run python3 build.py --check.
9 critic 0.5 min · 1 file changed, 97 insertions(+) Show prompt
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.
Reviewer feedback (critic pass, tested in real Chromium at 1440x900 and 390x844)
KEEP WHAT WORKS. These were verified correct against the raw GSA files and must not change: the single-traveler per-diem math (75% first/last day, APM noon/6 p.m. rule, provided-meal deductions using the M&IE tier breakdown, lodging cap by month, personal days), the example trip's per-diem total of $1,148.00, typing in receipt/itinerary fields keeping focus, drag-and-drop thumbnails, lightbox, CSV, print, save/open/reset, and draft restore after reload. You cannot run a browser, so for every item below check the exact numbers with node.
1. Lodging cap is per traveler, but the app checks all travelers' rooms against one cap (HIGH)
2. Meal check compares every traveler's meals with one traveler's M&IE (HIGH)
reconcile() flags "Meal receipts total ... over the M&IE allowance" using mealTotal > al.mie, where al.mie is one traveler's allowance. Repro: load the example trip (2 travelers) and add a Meals receipt of $150.00 on Jun 10 (shared). Meals total $214.05 is flagged as over $192.00, but the allowance for two travelers is $384.00. Expected: no flag. Compare shared meal receipts with M&IE x travelers, and meal receipts assigned to a traveler with that traveler's own M&IE.
3. Make the example trip consistent with items 1 and 2, and keep one real flag
After item 1, the shared $204.50 room no longer exceeds 2 x $191. Rebuild the sample so it still demonstrates one over-cap flag honestly: give each traveler their own room receipts (Traveler column set), e.g. A. Faculty Jun 9 $204.50 (over the $191 cap by $13.50), B. Staff Jun 9 $189.00, and both travelers Jun 10 $178.00. The airfare note says "Moscow-Boise round trip (each)", but only one $342.10 receipt is entered and it is split across both people. Enter one airfare receipt per traveler, each assigned to that traveler, or change the note. Check the new example totals by hand in node and make the top bar, summary, per-traveler cards, CSV and print all agree.
4. "By day - spend vs. per-diem allowance" chart is misleading (MEDIUM-HIGH)
It compares ONE traveler's allowance (Jun 9: $255.50) with ALL receipts for ALL travelers, including airfare and registration ($588.35), which per diem never covers. Every trip therefore looks overspent. Fix: allowance bar = (M&IE + lodging cap) x number of travelers for that day. Spend = only per-diem items (Lodging + Meals) for that day. Show other costs (airfare, registration, parking, ...) as a separate, clearly labelled segment stacked on top in a muted colour, or leave them out. Update the legend to match. Add the same custom hover tooltip the category chart has (right now the day chart only has native <title> tooltips), e.g. "Jun 9 - allowance $511.00 (2 travelers) - lodging + meals $246.25 - other $342.10".
5. Tab out of a receipt field drops keyboard focus to <body> (MEDIUM)
Repro: click into receipt row 2's Notes field, type a few characters, press Tab. Actual: document.activeElement becomes <body>. Expected: focus moves to that row's x (delete) button, then on to the next row's fields. Cause: the blur handler calls touch(), which does a full renderAll(). focusKey()/findFocusTarget() only restore controls that have data-role, and the delete button has none. Fix it without regressing typing focus: skip the rebuild on blur (patch in place), and/or give the delete button and the image slot a data-role so focus can be restored. The same applies to Tab inside itinerary rows.
6. Home ZIP 20001 shows the wrong state (MEDIUM, data label)
Typing 20001 shows "GSA NSA: District of Columbia, VA (DID 75)". DID 75 appears three times in gsa_perdiem_conus_lodging_2026.json (State DC, MD and VA), and scripts/build_perdiem.py keeps the last row. Keep the DC row for allLocalities (or use the ZIP row's "ST" field), so the hint reads "District of Columbia, DC". Re-run the script so data/embed/perdiem.json is regenerated.
7. Empty Home ZIP shows a hint as if a ZIP had been typed (LOW-MEDIUM)
After Reset, with the ZIP field empty, the hint still says "Standard CONUS rate applies at home (no GSA NSA)." because "".padStart(5,"0") becomes "00000". Show no hint until exactly 5 digits are typed, and do not pad partial input (for example "8384" must not become "08384").
8. Top bar and summary disagree when there are no receipts (LOW-MEDIUM)
Repro: Reset, dates 2026-02-27 to 2026-03-02, add a leg to Washington, DC, one traveler, no receipts. The top bar shows Estimated reimbursement "—", but the Summary shows $322.00. Show the same figure in both. The "Out of pocket" tile always equals "Not reimbursable" (same formula), so remove that duplicate tile (summary, print and CSV), or give it a genuinely different meaning.
9. Reconciliation floods on long trips (LOW-MEDIUM)
A trip of many nights with no lodging receipts produces one "No lodging receipt for the night of ..." line per night (hundreds for a long range). Group consecutive missing nights in the same destination into one line ("No lodging receipts for 12 nights, Feb 27 - Mar 10, in Washington, DC (caps $196-$276)"), with jump to the first of them.
10. Nights default to Moscow, ID (LOW-MEDIUM)
With dates set but no legs, every night defaults to "Moscow, ID". Moscow is the U of I home campus, so a Moscow-based traveler would not claim lodging there. Default each night to an explicit "— choose destination —" option (no lodging cap and no allowance until one is chosen, with one reconciliation line such as "Pick a destination for 3 nights"). A leg keeps pre-filling nights as now.
11. Phone (390 px) receipts Total card (cosmetic)
The Total card at the bottom of the receipts cards still shows two short stray horizontal rules at its top and an "AMOUNT" caption. Render one clean row: "Total" on the left and "$908.65" on the right, with no stray borders.
12. Small polish
Then run python3 build.py --check.
10 critic 3.3 min · 4 files changed, 222 insertions(+), 171 deletions(-) Show prompt
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.
Reviewer feedback: 5 fixes (tested in real Chromium)
Keep everything else exactly as it is. The single-traveler per-diem math is verified correct. Work in src/app.js, then run python3 build.py --check. Check the numbers below with node.
1. Lodging cap must be checked per traveler
2. Meal check must use every traveler's M&IE
reconcile() uses mealTotal > al.mie, but al.mie belongs to one traveler. With the example trip (2 travelers), add a shared Meals receipt of $150.00 on Jun 10. The app flags $214.05 as over $192.00. Wanted: compare with al.mie x number of travelers ($384.00), so there is no flag.
3. Example trip: give each traveler their own receipts
In loadExampleTrip, set trv on the receipts to the traveler ids: A. Faculty: Airfare Jun 9 $342.10, Lodging Jun 9 $204.50, Lodging Jun 10 $178.00. B. Staff: Airfare Jun 9 $342.10, Lodging Jun 9 $189.00, Lodging Jun 10 $178.00. Keep the shared meals and registration receipts. Result: exactly one over-cap flag (A. Faculty, Jun 9, $13.50). The top bar, summary, per-traveler cards, CSV and print must all show the same totals.
4. Day chart: compare like with like
"By day - spend vs. per-diem allowance" compares one traveler's allowance with all receipts, including airfare. Change it so the allowance bar = (M&IE + lodging cap) x number of travelers, and the spend bar = only the Lodging + Meals receipts for that day. Draw other costs (airfare, registration, parking...) as a separate grey segment stacked on the spend bar, and name all three in the legend. Give the bars the same hover tooltip as the category chart (showTip/hideTip), e.g. "Jun 9 · allowance $511.00 (2 travelers) · lodging + meals $435.25 · other $684.20".
5. Tab out of a receipt field loses focus
Click in a receipt's Notes field, type, then press Tab: focus ends up on <body>. It should land on the row's × button. Cause: the blur handler calls touch(), touch() then runs renderAll(), and focus is restored only to elements that have a data-role. Fix: give the × delete button data-role="del" (and the image slot data-role="img") so focus restore finds them. Do not rebuild the table on blur.
11 critic 7.8 min · 5 files changed, 210 insertions(+), 174 deletions(-) Show prompt
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.
Reviewer feedback: lodging totals and 5 small fixes (tested in real Chromium)
Keep everything else as it is. Last round's meal check, duplicate check, day chart and Tab focus fixes work, so do not undo them. Check every number below with node before finishing, then run python3 build.py --check.
1. Use ONE lodging rule everywhere (HIGH: the example trip's totals are wrong right now)
The flags and the money disagree. The example trip shows one flag (A. Faculty, Jun 9, over by $13.50), but the top bar shows Not reimbursable $367.50 and Estimated reimbursement $1,570.20, both per-traveler cards show $614.05, and the Jun 9 day card says "Lodging actual $393.50 over $202.50". The cause: lodgingReimb() still compares all rooms with one cap, and reconcile() checks receipts one at a time instead of summing them per traveler.
Write ONE helper and use it in reconcile(), lodgingReimb(), perTravelerAlloc(), reimbursement(), the summary day cards, CSV and print:
lodgingNight(date) -> one entry per traveler: { t, amount, cap, reimb, over }
n = number of travelers
cap = lodgingCap(night.destId, date) // cap is per traveler
use Lodging receipts with this date, skip personal nights, skip receipts with trv === TRV_OVER
a receipt whose trv is a current traveler's id belongs 100% to that traveler
every other receipt (shared, empty, or an id that no longer exists) gives amount / n to each traveler
amount_t = sum of that traveler's shares
reimb_t = Math.min(amount_t, cap)
over_t = Math.max(0, amount_t - cap)
Expected results (all must match): a) Example trip: Estimated reimbursement $1,924.20 = M&IE $384.00 + lodging $736.00 + other $804.20. Not reimbursable $13.50, Out of pocket $13.50, 1 flag. A. Faculty $963.10 = M&IE $192.00 + lodging $369.00 + other $402.10. B. Staff $961.10 = M&IE $192.00 + lodging $367.00 + other $402.10. The CSV and print show the same numbers. b) Reset. Jun 9 to Jun 10 2026, night = Boise, 1 traveler, Lodging receipts $150.00 and $60.00 on Jun 9: flag over by $19.00 (round 10 broke this, and there is no flag now). Lodging reimbursed $191.00, Not reimbursable $19.00, Estimated reimbursement $320.00. c) Same dates with 2 travelers and ONE shared Lodging receipt of $300.00 on Jun 9: no flag ($150.00 each), Estimated reimbursement $558.00, Not reimbursable $0.00. Change it to $400.00: both travelers are $9.00 over, Not reimbursable $18.00, Estimated reimbursement $640.00. d) 2 travelers Ann and Bo, Lodging $180.00 (Ann) and $180.00 (Bo) on Jun 9: no flag, Estimated reimbursement $618.00. Add Lodging $20.00 (Ann) on Jun 9: flag "Lodging Jun 9 (Ann): $200.00 exceeds the Boise cap of $191.00 by $9.00", Estimated reimbursement $629.00, Not reimbursable $9.00.
2. Per-traveler costs assigned to one traveler are halved
In perTravelerAlloc(), a receipt with a traveler set adds amt / n to that traveler. It must add the full amt. Only shared receipts are split as amt / n. The same applies to the personal-day amount: shared receipts must add amt / n, not amt, to each traveler. The per-traveler cards must add up exactly to the Estimated reimbursement ($963.10 + $961.10 = $1,924.20).
3. Home ZIP 20001 shows "District of Columbia, VA"
DID 75 has three rows in data/raw/gsa_perdiem_conus_lodging_2026.json (State DC first, then MD, then VA), and scripts/build_perdiem.py keeps the last one. Keep the FIRST row for each DID (skip a DID already seen), then re-run python3 scripts/build_perdiem.py. Expected hint for 20001: "GSA NSA: District of Columbia, DC (DID 75) applies at home."
4. Home ZIP hint for an empty or partial ZIP
With the field empty, the hint says "Standard CONUS rate applies at home (no GSA NSA)." because "" is padded to "00000", and "8384" is treated as 08384. Show no hint unless exactly 5 digits are typed. Do not pad the input.
5. Top bar shows "—" for Estimated reimbursement when there are no receipts
Repro: Reset, dates 2026-02-27 to 2026-03-02, a leg to Washington, DC, no receipts. The Summary shows $322.00 but the top bar shows "—". Whenever the trip has dates, show the same figure in the top bar, with Not reimbursable $0.00.
6. Day chart: allowance bars need the tooltip too
The teal allowance bars only have a native <title>. Give them the same showTip / hideTip tooltip as the lodging + meals bars.
12 feedback 11.4 min · 7 files changed, 815 insertions(+), 126 deletions(-) Show prompt
Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
FEEDBACK — per-diem rule change (project owner decision, 2026-09-13)
The project owner decided that the app must default to University of Idaho policy and offer the federal 75% rule only as a clearly labelled alternative. SPEC.md ("Per-diem rule — product decision") records the decision. The official policy texts are now in data/raw/, quoted word for word in data/raw/SOURCES.md sections 7–10:
Everything else in the app works and must keep working: itinerary, receipts, typing focus, drag-and-drop, reconciliation, per-traveler split, charts, saved trips, draft restore, phone layout, and zero console errors. Change only what is listed below.
1. Policy data: embed it reproducibly (do this first)
Write scripts/build_policy.py (stdlib only) to produce data/embed/policy.json, embedded as data-policy. The script must read the numbers and quotes from the data/raw/*.txt files above and assert that each one is found, so that a changed source file makes the build fail loudly. Minimum content:
uofi: {
inStateMax: 58.00, # SBEX 442-50 Appendix B "(a) In State $58.00 day"
apm7004PrintedInState: 45, # APM 70.04 §E "For in-state travel -- $45 a day" (outdated)
mealPct: { b: 0.25, l: 0.35, d: 0.55 }, # SBEX 442-50 §6.C
quotes: [ the 4 APM 70.02 bullet sentences + "For the departure day, the per diem rate is based upon ..." sentence,
the APM 70.02 sentence "The meal allowance is based on the federal per diem for out of state destinations and for the maximum amount allowed within the state of Idaho.",
SBEX §6.C first two sentences ]
}
federal: { quotes: [ §301-11.20(a) second and third sentence, §301-11.21(b) both sentences ] }
2. Rule selector: University of Idaho is the default
3. Per-diem math: implement exactly this
For each itinerary day, dest = that date's night destination. On the return day (no night), use the last night's destination. provided = the ticked B/L/D for the day. Personal days stay $0.00, as now. Round every amount to cents.
University of Idaho (uofi)
rate = dest.state === "ID" ? 58.00 : dest.mie // Idaho in-state maximum vs GSA M&IE
pct = first day: (departure before noon ? 1 : 0.5); last day: (return after 6 p.m. ? 1 : 0.5); other days: 1
base = rate * pct
allow = provided.length === 0 ? base
: Math.min(base, rate * sum(mealPct[m] for m in [b,l,d] NOT provided)) // 25/35/55 of the daily rate
deduction shown = base - allow
Federal (federal)
rate = dest.mie (GSA, Idaho included); base = first or last day ? tier.fl (75%) : rate
allow = provided.length === 0 ? base : Math.max(base - sum(tier.b/l/d of provided), tier.i) // never below incidentals ($5)
Expected M&IE per traveler. Check every row with Node against allowance() and in the itinerary table. Lodging caps do not change.
| Trip (dates 2026 unless noted) | Times | Provided meals | U of I per day = total | Federal per day = total |
|---|---|---|---|---|
| Example trip, Boise Jun 9–11 | before noon / after 6 p.m. | L Jun 10 | 58.00 + 46.40 + 58.00 = 162.40 | 64.50 + 63.00 + 64.50 = 192.00 |
| Boise Jun 9–11 (in-state) | before noon / after 6 p.m. | none | 58.00 + 58.00 + 58.00 = 174.00 | 64.50 + 86.00 + 64.50 = 215.00 |
| Seattle Mar 2–5 (out-of-state) | after noon / before 6 p.m. | none | 46.00 + 92.00 + 92.00 + 46.00 = 276.00 | 69.00 + 92.00 + 92.00 + 69.00 = 322.00 |
| Denver Apr 13–16 | before noon / after 6 p.m. | D Apr 13, L Apr 14, L Apr 15, B Apr 16 | 55.20 + 73.60 + 73.60 + 82.80 = 285.20 | 31.00 + 66.00 + 66.00 + 46.00 = 209.00 |
| Coeur d'Alene Jul 20–22 | after noon / after 6 p.m. | D Jul 20, L Jul 21 | 29.00 + 46.40 + 58.00 = 133.40 | 24.50 + 54.00 + 55.50 = 134.00 |
| Night Jun 1 Boise, night Jun 2 Seattle, return Jun 3 | before noon / before 6 p.m. | none | 58.00 + 92.00 + 46.00 = 196.00 | 64.50 + 92.00 + 69.00 = 225.50 |
| Idaho Falls Oct 5–6, 2025 | before noon / before 6 p.m. | B+L+D Oct 5 | 0.00 + 29.00 = 29.00 | 5.00 + 51.00 = 56.00 |
Example trip totals (2 travelers):
| U of I (default) | Federal | |
|---|---|---|
| Per diem (all) | $1,088.80 | $1,148.00 (unchanged) |
| Estimated reimbursement | $1,865.00 (M&IE $324.80 + lodging $736.00 + other $804.20) | $1,924.20 |
| Per traveler | A. Faculty $933.50, B. Staff $931.50 | as before |
4. Itinerary shows the rule actually applied
5. Explain both rules in the UI, with citations and links
6. Make the active rule obvious in the summary, the print view and the CSV
7. Done means
13 feedback 1.8 min · 5 files changed, 155 insertions(+), 187 deletions(-) Show prompt
Read FEEDBACK.md in this directory — it lists changes the project owner decided on and problems a reviewer found when testing index.html in a real browser. Make every change, keeping everything that already works, then run python3 build.py --check.
FEEDBACK — round 13 (reviewer tested round 12 in real Chrome)
Fix only the problems below. Do not change the per-diem math.
1. "Print summary" is broken: ReferenceError rm is not defined (highest priority)
In real Chrome, clicking Print summary throws ReferenceError: rm is not defined at buildPrintDoc. #print-doc stays empty, so the printed page has no summary. buildPrintDoc() uses rm.short, rm.name, rm.citations and rm.footer but never declares rm. Only renderSummary() and exportCSV() have const rm = ruleMeta();.
2. Reset does not restore the default rule
Checkable condition: after Reset, document.querySelector('input[name=proration]:checked').value === "uofi".
3. Federal rule: do not show noon / 6 p.m. percentages
Under the federal rule, three places still say "departure: Trip begins before 12 p.m. (noon) — 100% · return: Trip concludes after 6 p.m. — 100%": the summary banner's second line, the CSV Rule row, and the print "Per-diem rule" row. That is wrong for the federal rule: first and last day are always 75%. Under the federal rule, write "departure / return times: not used (75% on first and last day)" instead. Keep the current text under the U of I rule.
In the CSV "Per diem" rows, spell out provided meals: provided: lunch (less 11.60), and provided: breakfast + dinner (…) for several meals, not provided: l.
4. Itinerary rows go stale when a trip-level control changes while an itinerary checkbox has focus
Real-browser mechanism: touch() decides from document.activeElement. When the focused element is still an itinerary input (for example the B/L/D checkbox just ticked), typingInEditable() is true and only refreshPanels() runs. The model and totals update, but the itinerary badge and Allowance cells keep their old values, even after focus moves away.
Reproduction: tick L on a day, then change #trip-dep-time to pm without moving focus (set .value and dispatch change). Day 1 stays "100%" / "$275.00" while allowance().days[0].amount is 29.
Fix: changes from #trip-dep-time, #trip-ret-time and the rule radios must always update the itinerary cells. Call patchItinerary() or re-render the itinerary for them, whatever element has focus. Checkable condition: after that reproduction, the day-1 badge reads "50%" and its Allowance is $29.00 + lodging cap.
5. Small text fixes
Then run python3 build.py --check.
14 critic 20.9 min · 4 files changed, 282 insertions(+), 238 deletions(-) Show prompt
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.
FEEDBACK — critic re-review in real Chrome (1440×900 and 390×844)
The per-diem math is right: every day allowance and total I checked matches an independent calculation from data/raw under both rules (example trip, a Washington DC trip with provided meals, a Coeur d'Alene + Seattle trip with a personal day). Keep all of that working, plus typing focus, drag-and-drop, thumbnails and lightbox, reconciliation, per-traveler split, charts, CSV, print, saved trips, draft restore, zero console errors and no phone overflow.
The problems below are about what the page SAYS, which in several places contradicts what it computes. Fix every item, in this order, then run python3 build.py --check.
1. Return-day row: the % badge and M&IE cell go stale (wrong on screen)
Mechanism: patchItinerary() updates the day badge and the M&IE cell only inside the branch for rows that have a night. The return-day row has no night, so it is skipped. patchItinerary() is what runs when the Departure/Return selects change, and when the rule radio changes while an itinerary field has focus. Repro: Load example trip, then set Return (last day) to "Trip concludes before 6 p.m. — 50%". The Jun 11 row still shows the badge "100%" while its allowance correctly drops to $29.00. Same on any trip: the Return row badge never follows the select. Checkable condition: after that change, the Jun 11 badge reads 50% (tooltip too) and the allowance reads $29.00, with no other action. Switching back to "after 6 p.m." shows 100% and $58.00. The badge and M&IE cell of every row (including the return day) must always match the allowance, whichever path refreshed the table.
2. Destination notes, return-day rate basis and personal badge go stale after changing a night
Mechanism: changing a night's destination select (or Personal checkbox) runs refreshItineraryPanels(), which updates the cap, badge and allowance cells but not the small note under each destination select (.destnote), not the return row's "rate basis: City, ST" text, and not the "personal" badge in the Day cell. Repro: Reset, set dates Mar 30 – Apr 2, 2026, then use the three night selects to pick Washington, DC. Each note still says "GSA standard CONUS rate" (should be "District of Columbia NSA") and the return row says "rate basis: Moscow, ID" while its M&IE is already the DC $92.00 rate. Checkable condition: immediately after each select change, the note under that select names the new locality (Standard rate for Moscow/Idaho Falls, "District of Columbia NSA", "Seattle NSA", etc.), and the return row's rate basis names the last overnight destination. Ticking Personal on a night shows the "personal" badge in that row's Day cell at once; unticking removes it.
3. Summary banner contradicts the user's choice
ruleMeta() builds the U of I banner with fixed text "departure before noon → 100% · return after 6 p.m. → 100%". With Departure set to after noon, section 6 shows that fixed line AND, right under it, "departure: Trip begins after 12 p.m. (noon) — 50%". Checkable condition: Load example trip, set Departure to after noon and Return to before 6 p.m. The summary banner must not contain "before noon → 100%" or "after 6 p.m. → 100%" anywhere; it names the rule, the $58.00 in-state rate, and the ACTUAL choices (after noon → 50%, before 6 p.m. → 50%), once. With the defaults it shows before noon → 100% and after 6 p.m. → 100%, once.
4. Federal rule: hints and time selects never change (wrong element ids)
updateRuleHints() looks up the ids hint-dep, hint-ret and intro-perdiem, but the page's elements are #dep-hint, #ret-hint and #intro-rule. So under the federal rule the Departure/Return hints still say "APM 70.02 does not define exactly 12:00 or 6:00", the options still promise "— 100%" and "— 50%", and the intro bullet never changes. Checkable condition: select the Federal rule. Both hints read "Not used by the federal rule (75% on first and last day at any time)", both time selects are disabled (still showing the saved choice, restored when switching back), and the intro bullet reads "Per diem = GSA M&IE (75% first & last day) + lodging cap per night". Select the U of I rule: hints, enabled selects and intro bullet return to the U of I wording. This must also hold after Load example trip, Reset, opening a saved trip, and reload with a federal draft.
5. First screen: the policy card buries the "How to use" intro
Checkable condition: fresh load at 1440×900, the top of the "Enter a trip once" heading is less than 450 px from the top of the page and the whole rules card (collapsed) is under 260 px tall; at 390×844 the collapsed rules card is under 700 px tall. Opening a details element still shows the $58.00 / $45 / 25-35-55% bullets and the quotes, with no horizontal overflow on phone.
6. Empty totals strip shows a stray gold dash
Before any dates are set, the "Per diem (all)" tile shows two dashes, the second one small and gold (#tb-rule). Checkable condition: on a fresh load the small line under Per diem (all) shows the active rule name ("U of I rule", or "Federal rule"), and it updates immediately when the rule radio changes, with or without trip dates.
7. Phone receipts: stray rules and caption in the Total card
At 390 px with the example loaded, the receipts Total card shows two short horizontal rules and an "AMOUNT" caption above "TOTAL $1,617.75". Checkable condition: the phone Total card shows only the word Total and the amount, on one line, with no extra rules or captions.
8. "Out of pocket" is always identical to "Not reimbursable"
reimbursement() computes both from the same three terms, so the two summary tiles (and the two print and CSV lines) always match. Remove the "Out of pocket" tile, print row and CSV row; keep "Not reimbursable" with its breakdown. Checkable condition: the summary, print view and CSV no longer contain "Out of pocket"; all totals are otherwise unchanged (example trip: Estimated reimbursement $1,865.00, Not reimbursable $13.50).
9. Group missing-lodging warnings for consecutive nights
A trip with no lodging receipts lists one "No lodging receipt" flag per night, so a 6-night trip shows 6 near-identical warnings. Group consecutive nights at the same destination into one warning, for example "No lodging receipts for 3 nights, Sep 29 – Oct 1 in Moscow, ID (caps $110.00/night)", which jumps to the first of those nights. Each group counts as one flag in the Flags tile. Checkable condition: Reset, dates Jul 6 – Jul 10 2026, all four nights Boise: exactly one missing-lodging warning (4 nights, Jul 6 – Jul 9). Setting the Jul 8 night to Seattle gives three warnings (Boise Jul 6–7, Seattle Jul 8, Boise Jul 9).
15 continue 1.5 min · 3 files changed, 42 insertions(+), 26 deletions(-) Show prompt
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.
FEEDBACK — follow-up after the last round (checked in real Chrome, time zone America/Los_Angeles)
The last round fixed most of the previous list in real Chrome: return-day badge, rule-switch refresh, federal hints and disabled time selects, the compact rules card below the intro, the rule name in the empty totals strip, destination notes, the personal badge, and the removal of "Out of pocket". All per-diem math still matches an independent calculation. Keep all of that working.
Six problems remain. Fix each one, then run python3 build.py --check. Your sandbox runs in UTC; real users (and the reviewer) are in US time zones. Test date logic with the environment variable TZ set to America/Los_Angeles as well as UTC.
1. Missing-lodging warnings are still one per night in a US time zone
Mechanism: in reconcile() the grouping test compares a night's date with the day after the group's last night, computed by passing the ISO date string straight to addDays(). A date-only ISO string is parsed as UTC midnight, while iso() reads local fields, so west of UTC "the day after 2026-07-06" comes back as "2026-07-06" and consecutive nights never join a group. In UTC it happens to work, which is why your sandbox test passed parts of this. Build the date with the app's own parseDate() helper before adding a day (the same way the rest of the app handles dates), or compare day numbers. Checkable condition, in Chrome or in Node with TZ America/Los_Angeles: Reset, dates Jul 6 – Jul 10 2026, all four nights Boise, no receipts: exactly one missing-lodging warning reading "No lodging receipts for 4 nights, Jul 6 – Jul 9 in Boise, ID (cap $191.00/night)" and the Flags tile shows 1. Setting the Jul 8 night to Seattle gives exactly three warnings (Boise Jul 6 – Jul 7, Seattle Jul 8, Boise Jul 9) and Flags shows 3.
2. Single-night warning wording
A one-night warning now reads "No lodging receipt for the night of Jul 6 in Boise, ID (caps $191.00/night)". For a single night say "(cap $191.00)"; for a group say "(cap $191.00/night)". Never "caps".
3. Return-day "rate basis" still goes stale
Mechanism: syncItinRow() sets its destination only when the row has a night, and updates the note only when that destination exists. The return-day row has no night, so its "rate basis: City, ST" note is never refreshed. The allowance row for that date already carries the last overnight destination; use it for the return row. Checkable condition: Reset, dates Mar 30 – Apr 2 2026, use the three night selects to pick Washington, DC. Without any other action, the Apr 2 row reads "rate basis: Washington, DC". Then set the Apr 1 night to Seattle: the Apr 2 row reads "rate basis: Seattle, WA".
4. Phone: the receipts Total amount is pushed outside its card and cut off (new in the last round)
At 390×844 with the example trip loaded, the Total card shows "Total" on the left, and the amount "$1,617.75" sits to the right of the card's border and is clipped by the screen edge (only "$1,6" is visible). The previous stray rules and caption are gone; keep them gone. Checkable condition: at 390 px the amount is fully visible inside the Total card, on the same line as the word Total, right-aligned with at least 12 px between the amount and the card's right border; no "AMOUNT" caption and no extra rules. Desktop Total row unchanged.
5. Summary banner repeats the choices and ends with a stray comma
With Departure after noon and Return before 6 p.m., section 6 now shows the choices twice, first in the banner line ("departure Trip begins after 12 p.m. (noon) — 50% · return Trip concludes before 6 p.m. — 50%,") and again in the small line under it, and the banner line ends with a comma. Checkable condition: the banner names the rule and "Idaho in-state $58.00/day", and states each actual choice exactly once, in readable wording such as "departure after 12 p.m. (noon) → 50% · return before 6 p.m. → 50%". The text "after 12 p.m." appears once in the banner element, and no line ends with a comma. With the defaults it reads "departure before 12 p.m. (noon) → 100% · return after 6 p.m. → 100%", once. Under the federal rule it keeps "departure / return times: not used (75% on first and last day)".
6. A receipt image named "dinner.jpg" is guessed as Lodging
guessCat() tests the lodging words first and "inn" matches inside "dinner". Match lodging words only as whole words or word starts (hotel, lodging, inn, suites, and the chain names), so dinner, dinnerware, etc. are guessed as Meals. Checkable condition: dropping files named dinner-grill.png and lunch.jpg creates Meals rows; hampton-inn.png, marriott.jpg and hotel.png create Lodging rows.
16 critic 0.5 min · 3 files changed, 12 insertions(+), 12 deletions(-) Show prompt
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.
FEEDBACK — two problems left (checked in real Chrome after the last round)
The last round fixed the grouped missing-lodging warnings (in a US time zone too), the "cap" wording, the summary banner and the receipt category guess. The full regression check passes everything else. Keep all of that working.
Two problems remain. Fix both, then run python3 build.py --check.
1. Return-day "rate basis" is still never refreshed (the last fix reads a field that does not exist)
Mechanism: in syncItinRow() the return-day branch looks up the destination with the allowance row's destId field. The day rows returned by allowance() have no destId field: they carry dest, which is already the destination object (see the object returned inside allowance(), next to date, kind, base and deduction). So the lookup is always undefined, the note is never updated, and the return row keeps whatever destination it had when the table was last fully rebuilt. Use the day row's dest object for the return row's rate basis. Checkable condition (real browser, no page reload): Reset, set dates Mar 30 – Apr 2 2026, then use the three night selects to pick Washington, DC. The Apr 2 row's small note reads "rate basis: Washington, DC" immediately. Then set the Apr 1 night to Seattle: the Apr 2 row reads "rate basis: Seattle, WA", and its M&IE cell shows $92.00. Loading the example trip still shows "rate basis: Boise, ID" on Jun 11.
2. Phone: the receipts Total amount is still outside its card
Measured at 390×844 (mobile emulation) with the example trip loaded: the footer card is 328 px wide; the "Total" label cell is 298 px wide and the Amount cell is 0 px wide, starting at x 356, so "$1,617.75" is drawn past the card's right border and the page's layout viewport grows to 444 px (window.innerWidth 444 while the document is 390 wide). Mechanism: the phone rule that turns the receipts table into cards gives every table cell display block and width 100%. That width also applies to the two footer cells, so the label cell (flex 0 0 auto) takes the whole row and cannot shrink, and the amount cell is squeezed to zero. In the phone footer, the label and amount cells must have automatic width (not 100%): the label may grow to fill the space, the amount keeps its natural width, right-aligned. Checkable condition at 390×844 on a mobile viewport: window.innerWidth stays 390; the text "$1,617.75" is fully inside the Total card, on the same line as "Total", with at least 12 px between the amount and the card's right border; the Amount cell's width is at least the width of its text. Desktop receipts table and its Total row are unchanged.
What the agent was asked
The full specification the harness received, rendered from the original Markdown.
Known limitations
Reported by the independent critic. ✎ written by the AI critic
- Phone (390 px): the receipts Total card still shows a small 'AMOUNT' caption above $1,617.75 (the amount now fits inside the card)
- Summary banner wording is clumsy: 'departure Trip begins before 12 p.m. (noon) -> 100%' (capital T mid-sentence)
- One-day trips are not handled specially: APM 70.02's one-day meal paragraph and the 41 CFR 301-11.20 12-hour threshold are not applied; a same-day trip is prorated like a departure day
- Judgement calls not confirmed by U of I Accounts Payable: $58.00 State rate used instead of APM 70.04's outdated $45; SBEX 442-50 section 6.C percentages used for provided meals; on a 50% day a provided meal does not lower the allowance below the 50% amount
- Destination picker covers only the 7 SPEC destinations; dates on/after 2026-10-01 are flagged but priced at FY2026 levels
- With no travel leg set, every night defaults to Moscow, ID
- With the example loaded the phone page is about 14,000 px tall
- Desktop receipts Notes input is narrower than its column (cosmetic)
- Critic round 14 stalled (no file change or log growth for 15 minutes) and was killed with its partial work committed; round 15 (continue) finished it
