U of I × MindRouter App Gallery

Travel Expense Reconciler

Administration Built with Codex CLI v0.154.0 Critic status: shipped

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.

Travel Expense Reconciler running on a desktop screen, per-diem math and receipt checks for university travel Desktop · click to enlarge
Travel Expense Reconciler running on a phone, per-diem math and receipt checks for university travel
Phone · 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

  1. 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
  2. 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
  3. 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):
Full data provenance ✎ written by the AI reviewer

Raw data sources: Travel Expense Reconciler

Retrieval date for every file: 2026-09-13 (sequential HTTP GET, User-Agent UIdaho-gallery-data-prep/1.0). Every GSA file (sections 1–6) is the unmodified response body. Nothing was filtered, reshaped or aggregated. The policy files (sections 7–10, added later on 2026-09-13 for the University of Idaho per-diem rule) are also unmodified downloads (.html, .pdf, .xml). Each also has a plain-text extract (.txt) made from that exact download (HTML tags or PDF layout removed, wording unchanged), so a coding agent without HTML/PDF tools can read the policy text.

Fiscal year covered: FY2026 = October 1, 2025 through September 30, 2026 (GSA FTR Bulletin 26-01). GSA kept the FY2025 rates for FY2026: no new non-standard areas (NSAs), standard CONUS lodging $110, standard M&IE $68, and M&IE tiers $68 / $74 / $80 / $86 / $92.

Total size of data/raw: about 2.6 MB (21 files including this one): 13 GSA/data files from the first download plus 8 policy files (sections 7–10).

FY boundary: these files cover travel dates 2025-10-01 through 2026-09-30 only. Nights and days on or after 2026-10-01 fall in FY2027, which this data does not include. GSA has posted FY2027 files ("Validated 09/01/26": https://www.gsa.gov/system/files/FY2027_PerDiemRates_Validated090126.xlsx), but they were not downloaded because SPEC.md asks for FY2026. The app should flag or block dates outside FY2026 rather than silently apply FY2026 rates.


Publisher, license and API notes (apply to all GSA files)

  • Publisher: U.S. General Services Administration (GSA), Office of Government-wide Policy.

  • License: U.S. federal government work, not subject to U.S. copyright (17 U.S.C. §105); public domain. API use falls under the api.data.gov terms of service (https://api.data.gov/). GSA asks users to cite GSA and to verify rates at https://www.gsa.gov/perdiem before relying on them.

  • API key: the URLs use api_key=DEMO_KEY, the public api.data.gov demo key (not a secret). On this retrieval the DEMO_KEY limit was x-ratelimit-limit: 10 requests per hour, and all 10 were used.

  • API docs: https://open.gsa.gov/api/perdiem/ . Per those docs, apostrophes, hyphens and periods in city names must be replaced with a space or removed. That is why Coeur d'Alene was requested as Coeur%20d%20Alene.

  • Month convention (important): GSA lodging months are calendar months 1..12 with no year. For FY2026, Oct, Nov and Dec mean 2025, and Jan through Sep mean 2026. Lodging applies per night, using the month of the night's date.

  • Standard-rate detection: in the city endpoint the field standardRate is the string "false" even on the record whose city is "Standard Rate" (a GSA quirk). Detect the standard rate with city == "Standard Rate" (city endpoint) or DID == "0" (conus/lodging and zipcodes).

  • Units: all money values are whole US dollars per day (lodging per night, excluding taxes) except FirstLastDay, which can have cents (e.g. 55.5).


1. Per diem by city (GSA Per Diem API v2, endpoint 1): 7 files

Common URL pattern (HTTP GET): https://api.gsa.gov/travel/perdiem/v2/rates/city/{city}/state/{ST}/year/2026?api_key=DEMO_KEY

Common JSON shape:

{ "request": null, "errors": null, "version": null,
  "rates": [ { "state": "ID", "year": 2026, "isOconus": "false", "oconusInfo": null,
               "rate": [ { "city": "...", "county": "..." | null, "zip": null,
                           "standardRate": "false", "meals": <int M&IE $/day>,
                           "months": { "month": [ { "value": <int lodging $/night>,
                                                    "number": 1..12, "short": "Jan", "long": "January" }, ... 12 ] } } ] } ] }

Path to lodging: rates[0].rate[i].months.month[k].value (match number to the calendar month). Path to M&IE: rates[0].rate[i].meals. No missing-value codes: county is null only on the standard rate. Numbers are JSON numbers in this endpoint (they are strings in endpoint 4, below). year is a number (2026), while standardRate and isOconus are the strings "false". The month array is ordered Jan..Dec by number, not in fiscal order. The raw bytes use JSON escapes \u0027 (apostrophe, as in "Coeur d\u0027Alene") and \/ (as in "Denver \/ Aurora"). A JSON parser decodes them, but a plain-text grep for Coeur d'Alene or Denver / Aurora will not match the raw file.

FileExact URL (GET)BytesRecordsResult (FY2026)
gsa_perdiem_city_moscow_id_2026.jsonhttps://api.gsa.gov/travel/perdiem/v2/rates/city/Moscow/state/ID/year/2026?api_key=DEMO_KEY8981 rate record"Standard Rate": lodging $110 all months; M&IE $68. Latah County is not an NSA.
gsa_perdiem_city_boise_id_2026.jsonhttps://api.gsa.gov/travel/perdiem/v2/rates/city/Boise/state/ID/year/2026?api_key=DEMO_KEY8911Boise (Ada County), DID 495: M&IE $86; lodging $191 Oct (2025), $167 Nov–May, $191 Jun–Sep.
gsa_perdiem_city_idaho_falls_id_2026.jsonhttps://api.gsa.gov/travel/perdiem/v2/rates/city/Idaho%20Falls/state/ID/year/2026?api_key=DEMO_KEY8981"Standard Rate": lodging $110 all months; M&IE $68. Bonneville County is not an NSA. Byte-identical to the Moscow file.
gsa_perdiem_city_coeur_d_alene_id_2026.jsonhttps://api.gsa.gov/travel/perdiem/v2/rates/city/Coeur%20d%20Alene/state/ID/year/2026?api_key=DEMO_KEY9091Coeur d'Alene (Kootenai County), DID 120: M&IE $74; lodging $142 Oct–May, $217 Jun–Aug, $142 Sep.
gsa_perdiem_city_seattle_wa_2026.jsonhttps://api.gsa.gov/travel/perdiem/v2/rates/city/Seattle/state/WA/year/2026?api_key=DEMO_KEY8941Seattle (King County), DID 389: M&IE $92; lodging $188 Oct–May, $248 Jun–Sep.
gsa_perdiem_city_washington_dc_2026.jsonhttps://api.gsa.gov/travel/perdiem/v2/rates/city/Washington/state/DC/year/2026?api_key=DEMO_KEY10991"District of Columbia", DID 75. It also covers Alexandria, Falls Church and Fairfax cities and Arlington and Fairfax counties (VA), plus Montgomery and Prince George's counties (MD). M&IE $92; lodging $275 Oct, $196 Nov–Feb, $276 Mar–Jun, $183 Jul–Aug, $275 Sep.
gsa_perdiem_city_denver_co_2026.jsonhttps://api.gsa.gov/travel/perdiem/v2/rates/city/Denver/state/CO/year/2026?api_key=DEMO_KEY17362 rate recordsThe API returned two localities: rate[0] "Boulder / Broomfield" (DID 52, M&IE $80, $173 Oct, $125 Nov–Apr, $173 May–Sep) and rate[1] "Denver / Aurora" (Denver/Adams/Arapahoe/Jefferson counties, DID 56, M&IE $92, $215 Oct, $165 Nov–Mar, $215 Apr–Sep). The Denver destination is "Denver / Aurora" (downtown Denver ZIP 80202 maps to DID 56). Select by city == "Denver / Aurora"; do not take rate[0].

(DIDs are not present in endpoint 1 responses. They come from files 3 and 4 and are listed here for reference.)

Suggested citation: U.S. General Services Administration, Per Diem API v2, FY2026 CONUS rates by city (api.gsa.gov/travel/perdiem/v2), retrieved 2026-09-13.


2. gsa_perdiem_conus_mie_2026.json: M&IE breakdown tiers (endpoint 5)

  • URL (GET): https://api.gsa.gov/travel/perdiem/v2/rates/conus/mie/2026?api_key=DEMO_KEY

  • Bytes: 425. Records: 5 (one per M&IE tier). Date range: FY2026.

  • Format: top-level JSON array of objects { "total", "breakfast", "lunch", "dinner", "incidental", "FirstLastDay" }, all JSON numbers in US dollars. total = breakfast + lunch + dinner + incidental. FirstLastDay = 75% of total (the federal first/last day of travel amount; FTR §301-11.101 in older documents, 41 CFR §301-11.20 in the current regulation, see section 10). Join to a locality by total == meals (city endpoint) or total == int(Meals) (conus/lodging).

  • Contents (verbatim values):

totalbreakfastlunchdinnerincidentalFirstLastDay
68161928551
74182031555.5
80202233560
86222336564.5
92232638569
  • Deductions for government-furnished or registration-fee meals follow FTR §301-11.18 (now 41 CFR §301-11.21, see section 10). Complimentary hotel meals and meals from a common carrier do not reduce per diem (§301-11.17), per the GSA M&IE breakdown document (file 6).

  • Suggested citation: U.S. General Services Administration, FY2026 CONUS M&IE breakdown (Per Diem API v2, conus/mie/2026), retrieved 2026-09-13.


3. gsa_perdiem_conus_lodging_2026.json: all CONUS localities (endpoint 4)

  • URL (GET): https://api.gsa.gov/travel/perdiem/v2/rates/conus/lodging/2026?api_key=DEMO_KEY

  • Bytes: 79,776. Records: 346 rows = 48 "Standard Rate" rows (one per state, DID "0", County null, lodging "110", Meals "68") + 298 NSA rows. The NSA rows cover 296 distinct DIDs (DID 75 "District of Columbia" appears three times, with State DC, MD and VA). 49 state codes (48 contiguous states + DC). Date range: FY2026.

  • Format: top-level JSON array of flat objects. All values are strings: Jan..Dec (lodging $/night for that calendar month; Oct–Dec = 2025, Jan–Sep = 2026), Meals (M&IE $/day), City (destination name, or "Standard Rate"), State (2-letter), County (county or counties, null on standard rows), DID (destination ID; "0" = standard rate). 13 City values have trailing spaces (e.g. "Douglas " CO, "Santa Monica " CA, "State College " PA), so trim before matching. County has no stray whitespace. The only null values are County on the 48 standard rows. M&IE distribution across rows: 68 (65 rows), 74 (71), 80 (104), 86 (71), 92 (35). Rows are ordered by state, not by DID.

  • Idaho rows: Standard Rate; Sun Valley / Ketchum (DID 122); Boise (495); Coeur d'Alene (120). These values match the city files.

  • Suggested citation: U.S. General Services Administration, FY2026 CONUS per diem lodging rates, all localities (Per Diem API v2, conus/lodging/2026), retrieved 2026-09-13.


4. gsa_perdiem_conus_zipcodes_2026.json: ZIP code to per diem locality (endpoint 6)

  • URL (GET): https://api.gsa.gov/travel/perdiem/v2/rates/conus/zipcodes/2026?api_key=DEMO_KEY

  • Bytes: 1,548,073. Records: 42,358 rows across 49 state codes. Date range: FY2026.

  • Format: top-level JSON array of { "Zip": "83843", "DID": "0", "ST": "ID" }, all strings (digits only). Leading zeros are STRIPPED in this API file: 3,089 rows have 4-digit Zip values (e.g. CT "6018" = 06018) and 2 rows have 3-digit values (NY "501" = 00501, "544" = 00544); they are in NJ, MA, ME, CT, VT, NH, RI and NY. Left-pad Zip to 5 digits (zip.padStart(5,"0")) before matching user input. (GSA's own FY2026_ZipCodeFile.xlsx keeps the zeros, e.g. "00501".) DID "0" = standard rate. Join DID to file 3's DID (every zipcode DID exists in file 3, and every file 3 DID has at least one ZIP). 40,426 distinct ZIPs. 1,839 ZIPs appear in more than one row (1,751 twice, 83 three times, 5 four times; 1,479 of them include DID "0" next to an NSA; 16 appear under two ST codes). No row is an exact duplicate. A ZIP lookup must handle multiple matches (e.g. 81623 CO → DIDs 0, 50, 55, 66), such as by asking the user to pick.

  • Checked mapping for the app's destinations (each of these ZIPs has exactly one row): Moscow 83843, 83844 → DID 0 (standard). Idaho Falls 83401, 83402, 83404 → DID 0 (standard). Boise 83702, 83706 → 495. Coeur d'Alene 83814, 83815 → 120. Seattle 98101, 98104 → 389. Washington DC 20001, 20004 → 75. Denver 80202, 80206 → 56 (Denver / Aurora).

  • Suggested citation: U.S. General Services Administration, FY2026 per diem ZIP code file (Per Diem API v2, conus/zipcodes/2026), retrieved 2026-09-13.


5. FY2026_PerDiemMasterRatesFile.xlsx: GSA official downloadable FY2026 rates file

  • URL (GET): https://www.gsa.gov/system/files/FY2026_PerDiemMasterRatesFile.xlsx (linked as "FY 26 Per Diem Rates [XLSX - 41 KB]" from https://www.gsa.gov/travel/plan-a-trip/per-diem-rates/per-diem-files)

  • Bytes: 42,276. HTTP Last-Modified: Fri, 15 Aug 2025 00:24:04 GMT. Office Open XML spreadsheet.

  • Structure: one sheet, Master, 652 rows, columns A–H (I–K empty):

    • Row 1: title FY2026 Per Diem Rates - Effective October 1, 2025

    • Row 2: header ID | STATE | DESTINATION | COUNTY/LOCATION DEFINED | SEASON BEGIN | SEASON END | FY26 Lodging Rate | FY26 M&IE

    • Row 3: standard CONUS rate (ID and STATE blank; DESTINATION = "Standard CONUS rate applies to all counties not specifically listed. Cities not listed may be located in a listed county."), lodging 110, M&IE 68

    • Rows 4–652: 649 season rows for 296 NSAs (ID = the API's DID, but here an integer, e.g. 495, while the API uses the string "495"). SEASON BEGIN / SEASON END are strings like "October 1" / "May 31"; empty strings ('', not null) mean the rate applies all year (139 NSAs have a single all-year row; the other 157 are seasonal and each starts with an "October 1" row). Every season begins on day 1 of a month and ends on that month's last day ("February 28" appears as an end date). Seasons run within the fiscal year in Oct→Sep order and never cross Sep 30 (a row that begins "October 1" starts in Oct 2025). Lodging and M&IE are integers.

    • Whitespace quirks: DESTINATION has leading/trailing spaces in 25 rows and COUNTY/LOCATION DEFINED in 18 rows, so trim them. Cell K4 holds a single space ' '; columns I–K are otherwise empty.

  • Derived check (not a file): each NSA's season rows were expanded to 12 calendar months and compared with file 3 (DID, all 12 months, and Meals). Result: 0 mismatches across all 296 DIDs, and every DID appears in both sources. The XLSX and the API are the same FY2026 rate table.

  • Suggested citation: U.S. General Services Administration, FY2026 Per Diem Rates master file (gsa.gov/travel/plan-a-trip/per-diem-rates/per-diem-files), retrieved 2026-09-13.


6. FY2025-present_MIE_Breakdown.docx: GSA M&IE breakdown document

  • URL (GET): https://www.gsa.gov/system/files/FY%202025%20MIE%20Breakdown.docx (linked as "FY 25-Present M&IE Breakdown [DOCX - 18 KB]" on the per diem files page)

  • Bytes: 18,668. HTTP Last-Modified: Fri, 16 Aug 2024 01:28:53 GMT. Word (Office Open XML); text lives in word/document.xml.

  • Contents: heading "M&IE breakdowns for continental U.S. (CONUS), FY 2025", the FTR §301-11.18 and §301-11.17 notes, and a 5-row table with columns M&IE Total, Breakfast, Lunch, Dinner, Incidental Expenses, First & Last Day of Travel. The in-document heading says FY 2025, but GSA lists the file as applying from FY25 to the present. Its values are identical to file 2 (conus/mie/2026), which confirms they apply to FY2026.

  • Suggested citation: U.S. General Services Administration, M&IE breakdowns for CONUS, FY 2025–present (DOCX), retrieved 2026-09-13.


Reference links (checked 2026-09-13; the APM 70.02 and 70.04 pages were later saved, see sections 7–8)

LinkWhat it is
https://www.gsa.gov/perdiemGSA per diem rate lookup (tell users to verify rates here)
https://www.gsa.gov/travel/plan-a-trip/per-diem-rates/per-diem-filesGSA page listing downloadable files (source of files 5–6)
https://www.gsa.gov/travel/plan-a-trip/per-diem-rates/mie-breakdownsGSA M&IE breakdowns page
https://www.gsa.gov/policy-regulations/regulations/federal-travel-regulation/ftr-and-related-files/gsa-per-diem-bulletin-ftr-2601GSA Per Diem Bulletin FTR 26-01 (July 31, 2025), which sets FY2026 CONUS rates (FY2025 levels kept; standard lodging $110; M&IE $68–$92)
https://open.gsa.gov/api/perdiem/Per Diem API v2 documentation
https://www.uidaho.edu/policies/apm/70University of Idaho Administrative Procedures Manual, Chapter 70: Travel Management (the policy citation for the app)
https://www.uidaho.edu/policies/apm/70/02APM 70.02 – Travel (page shows "Last updated: January 1, 2022"). Section I covers meals, incidental expenses and per diem proration.
https://www.uidaho.edu/policies/apm/70/04APM 70.04 – Travel Entitlements (page shows "Last updated: August 2017" and "APM 70.04 is under revision")

Suggested citation for the policy note: University of Idaho, Administrative Procedures Manual Chapter 70: Travel Management (APM 70.02 Travel), https://www.uidaho.edu/policies/apm/70, accessed 2026-09-13.

Policy files for the per-diem rule (added 2026-09-13; see sections 7–10 below)

An earlier version of this file gave only a paraphrase of APM 70.02 and did not save the policy pages. The official pages are now saved in data/raw and quoted word for word in sections 7–10. Use the quotes, not the old paraphrase.


7. University of Idaho APM 70.02 – Travel (default proration rule)

FileBytesWhat
uidaho_apm_70-02_travel.html185,725unmodified page, GET https://www.uidaho.edu/policies/apm/70/02 (HTTP 200)
uidaho_apm_70-02_travel.txt28,413policy body only, from the title "70.02 - Travel" to "Version History", HTML tags removed

Page header: "Owner: Position: Travel Services Manager", "Last updated: January 1, 2022".

§A. General (exact): "Departmental administrators, along with the Division of Finance and Administration, are responsible for ensuring that travel expenditures comply with the State of Idaho travel policies."

§B. Definitions (exact): "In-State Travel. Travel between locations in Idaho." "Out-of-State Travel. Travel from within Idaho to a destination outside of Idaho, but within the United States, and U.S Territories."

§I. Meals and Incidental Expenses (exact):

General. Generally, meals and incidental expenses are reimbursed at a per diem rate. The per diem allowance is a fixed amount for a full day of official travel and is not a reimbursement for actual costs incurred. No receipt is required. Per Diem for Partial Days of Travel. Proration of per diem for partial days is required when travel does not involve an overnight stay, when meals are furnished by others or when meals are furnished as part of a business-related meeting or conference. The following percentages shall be applied when prorated per diem is required. 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. - On the day the trip begins, the traveler is eligible for 100% of the per diem if the trip begins before 12 p.m. (noon). - On the day the trip begins, the traveler is eligible for 50% if the trip begins after 12 p.m. (noon). - On the day the trip concludes, the traveler is eligible for 100% of the per diem if the trip concludes after 6 p.m. - On the day the trip concludes, the traveler is eligible for 50% if the trip concludes before 6 p.m. For the departure day, the per diem rate is based upon the location of overnight lodging. For the day the traveler returns from a trip, the per diem rate is based upon the last location of overnight lodging. For more information view the Accounts Payable web site.
  • Notes: APM 70.02 lists no per-meal percentages for meals furnished by others. The State policy in section 9 does (§6.C: 25% / 35% / 55%), and §A above requires compliance with State of Idaho travel policies.

  • The wording does not say what happens at exactly 12:00 p.m. or exactly 6:00 p.m.

  • One-day trips (no overnight stay) have their own paragraph, "Meals on One-Day Trips", in the same section.

  • The "Accounts Payable web site" link (https://uidaho.edu/dfa/finance/controller/accounts-payable/travel-services) redirected on 2026-09-13 to the Controller's Office landing page (https://www.uidaho.edu/leadership/finance-administration/controllers-office), which has no per-diem or meal-rate table. The Accounts Payable page (https://www.uidaho.edu/leadership/finance-administration/accounts-payable) has none either. Neither page was saved. The rate table APM 70.02 relies on for "the maximum amount allowed within the state of Idaho" is the State Board of Examiners table in section 9.


8. University of Idaho APM 70.04 – Travel Entitlements (in-state meal maximum as printed)

FileBytesWhat
uidaho_apm_70-04_travel_entitlements.html154,025unmodified page, GET https://www.uidaho.edu/policies/apm/70/04 (HTTP 200)
uidaho_apm_70-04_travel_entitlements.txt15,505policy body only, from the title "70.04 - Travel Entitlements" to the page footer, HTML tags removed

Page header: "Last updated: August 2017" and "APM 70.04 is under revision."

§E (exact): "Per Diem Allowances: The amount reimbursed is limited to the lesser of (a) actual expenditures or (b) whichever of the following maximum applies: For in-state travel -- $45 a day; for out-of-state travel -- $51 a day, unless travel includes designated cities as identified on the IRS' Publication 1542, Table 2, Localities Eligible for $59 (M&IE) Per Diem Amount Under the High-Low Substantiation Method, [excluding cities listed in Idaho]. See http://www.irs.gov . [rev. 10-10, ed. 8-17]"

§E-2 table (exact, "applied when travel does not involve overnight stay. [rev. 8-17]"):

MealIn-State ($45/day Maximum)Out-of-State ($51/day Maximum or Allowable Federal Per Diem)
Breakfast$11.25$12.75 or 25% of Federal rate
Lunch$15.75$17.85 or 35% of Federal rate
Dinner$24.75$28.05 or 55% of Federal rate

Is $45 still the in-state maximum? No. APM 70.04's $45 in-state and $51 out-of-state are the State Board of Examiners rates that took effect 10-1-15. Section 9, Appendix A shows the row "10-1-15 … $45.00 … $51.00 or Federal Rate". The State in-state rate later became $49.00 (2-19-19), then $55.00 (1-1-22), then $58.00 (2-1-25). $58.00 is still the rate in the policy last amended August 20, 2026. APM 70.02 (last updated 2022, newer than APM 70.04) sets the in-state meal allowance at "the maximum amount allowed within the state of Idaho". The current in-state maximum is therefore $58.00 per day. The $45 figure is out of date, on a page marked as under revision. The E-2 percentages (25% / 35% / 55%) match the current State table ($58 × 25% = $14.50, × 35% = $20.30, × 55% = $31.90).


9. State of Idaho, State Travel Policy and Procedures (SBEX Policy No. 442-50)

FileBytesWhat
idaho_sbex_442-50_state_travel_policy.pdf356,549unmodified PDF, GET https://www.sco.idaho.gov/ViewerFiles/StateTravelPolicy/docs/State-Travel-Policies-and-Procedures.pdf (HTTP 200, Last-Modified: Fri, 21 Aug 2026 16:15:46 GMT; PDF created Aug 20 2026)
idaho_sbex_442-50_state_travel_policy.txt69,324pdftotext -layout of that PDF (page numbers and running layout kept)

Linked as "Travel Policy/PerDiem Rates" from the Idaho State Controller's Office home page (https://www.sco.idaho.gov/LivePages/scohome.aspx). Cover page (exact): "State Travel Policy and Procedures / SBEX Policy No. 442-50 / Adopted: July 1, 1996 / Last Amended: August 20, 2026 / Authority Idaho Code § 67-2004".

§6.A (exact, excerpt): "The Per diem allowance shall be based on the rate at the temporary work location, and on the final day of travel, the allowance shall be the rate for the location where the traveler last stayed the night prior to returning to their official primary work station."

§6.B (exact): "The amount of the Per diem allowance when the employee is in official travel status that does not involve an overnight stay, or for the first and last day of a multiple-day trip, shall be calculated based on the percentages of the daily Per diem allowance as established by the State Board of Examiners. 1) 25% for breakfast (leave at 7:00 am or earlier/return at 8:00 am or later) 2) 35% for lunch (leave at 11:00 am or earlier/return at 2:00 pm or later) 3) 55% for dinner (leave at 5:00 pm or earlier/return at 7:00 pm or later) Each entity of State government may adopt an internal policy regarding the time frames for allowing partial day Per diem allowance in order to consider the effects of swing shifts and other flexible work schedules that are usual and customary to the entity." (The University of Idaho's own time frames are the noon / 6 p.m. rule in APM 70.02, section 7.)

§6.C (exact): "When meals are furnished by others or as part of a meeting or conference and are identified on an official agenda, the Per diem allowance for the day shall be calculated for only those meals not provided. The allowable amount shall use the following percentages applied to the allowable Per diem allowance for each meal not provided by others or as part of a meeting or conference: 1) 25% for breakfast 2) 35% for lunch 3) 55% for dinner. Complimentary meals or beverages provided by lodging vendors, commercial airlines, or other commercial entities will not be considered when determining Per diem allowances."

Appendix B (exact): "Maximum Per Diem Allowance / Daily Per Diem Allowance (see definition of "per diem" in Section 11) / (a) In State $58.00 day / (b) Out-of-State Per Diem Allowance $68.00 per day is the base, but would allow the higher federal rate". Partial Day Per Diem Allowance table:

In-StateOut-of-State
Breakfast – 25%$14.50$17.00 or 25% of Federal Rate
Lunch – 35%$20.30$23.80 or 35% of Federal Rate
Dinner – 55%$31.90$37.40 or 55% of Federal Rate

"Agencies may adopt maximums of lesser amounts than those established by the Board of Examiners."

Appendix A, "History of Rate Changes" (MEALS In-State / MEALS Out-State), last rows (exact): 10-1-15 $45.00 / $51.00 or Federal Rate · 2-19-19 $49.00 / $55.00 or Federal Rate · 1-1-22 $55.00 / $59.00 or Federal Rate · 2-1-25 $58.00 / $68.00 or Federal Rate · 2-2-26 $58.00 / $68.00 or Federal Rate · 9-1-26 $58.00 / $68.00 or Federal Rate. Every GSA FY2026 M&IE tier ($68–$92) is at least $68, so out of state "the higher federal rate" is always the GSA M&IE of the locality.


10. Federal Travel Regulation, 41 CFR Part 301-11 "Subsistence Expenses" (federal alternative rule)

FileBytesWhat
ecfr_41cfr_part_301-11_2026-09-01.xml27,354unmodified eCFR XML, GET https://www.ecfr.gov/api/versioner/v1/full/2026-09-01/title-41.xml?part=301-11 (HTTP 200, gzip transfer)
ecfr_41cfr_part_301-11_2026-09-01.txt17,646the same text with XML tags removed (each heading prefixed with "## ")

Point in time: the regulation as in effect on 2026-09-01. Human-readable page: https://www.ecfr.gov/current/title-41/part-301-11 . The part's source note (exact): "FTR Case 2025-05, 90 FR 56893, Dec. 8, 2025, unless otherwise noted." Renumbered: the 75% rule is now §301-11.20 and the provided-meal deduction is §301-11.21. Older documents cite §301-11.101 and §301-11.18 / §301-11.17 instead: GSA's DOCX (file 6) and the GSA M&IE page still do, and so did this app. In the current text §301-11.18 is "Reimbursement for advance room deposit".

§301-11.20 Meals and incidental expenses (M&IE) reimbursement amounts, (a) (exact): "Except as provided in paragraph (b) of this section, when travel is more than 12 but less than 24 hours, employees receive a per diem allowance of 75 percent of the applicable M&IE rate for each calendar day they are in a travel status. If their travel is 24 hours or more, on the first day of departure and last day of travel, they receive 75 percent of the applicable M&IE rate. Full days of travel are reimbursed at 100 percent of the applicable M&IE rate."

§301-11.21 Allowable M&IE reimbursement when meals are provided (exact): "(a) Except as provided in paragraph (c) or (d) of this section, when M&IE per diem is authorized and meals are provided, either by the Government or included in the registration fee, including meals furnished under the authority of chapter 304 of this subtitle, employees must adjust the amount reimbursed by deducting the appropriate amount shown at https://www.gsa.gov/mie. (b) For meals provided on the day of departure and the last day of travel, employees must deduct the entire allocated meal cost from the decreased M&IE rate. The total amount of meal deductions made will not cause employees to receive less than the amount allowed for incidental expenses. (c) Employees do not need to deduct meals provided by a common carrier or a complimentary meal provided by a hotel/motel." The "appropriate amount" is the GSA breakdown in file 2 (conus/mie/2026: breakfast, lunch, dinner, incidental for each tier).


Independent verification (2026-09-13)

  • Every JSON file parses. errors is null and year is 2026 on all 7 city files. The record counts, DIDs, month values and M&IE above were re-derived from the files. Each of the 7 city records (plus the Boulder / Broomfield record) matches the XLSX season rows exactly.

  • Live re-fetch: the Per Diem API DEMO_KEY returned HTTP 429 OVER_RATE_LIMIT (retry-after about 5.7 h) because the 10/hour allowance was used by the download, so the API itself could not be re-queried. Instead, the same data was checked live against GSA's own files: FY2026_PerDiemMasterRatesFile.xlsx and FY 2025 MIE Breakdown.docx re-downloaded byte-identical. The live M&IE breakdowns page (https://www.gsa.gov/travel/plan-a-trip/per-diem-rates/mie-breakdowns) shows the same 5 tiers as file 2. GSA's https://www.gsa.gov/system/files/FY2026_ZipCodeFile.xlsx (3,338,820 bytes, sheet "Zip Code", 42,358 data rows; used only for checking, not saved) gives the same ZIP→DID sets as file 4 for all 40,426 ZIPs once leading zeros are normalized.

  • All reference links above returned HTTP 200 on 2026-09-13. The APM 70.02 proration wording and the APM 70.04 table were re-read and match the paraphrase above.

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.

16harness rounds
173.3minutes of harness work (2.9 h)
2,750lines of code
437KB, single-file app

Rounds by kind

build1
feedback9
critic5
continue1

Lines of code per file

body html247
app css501
app js1,803
scripts199

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

visual 8 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

  • 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
Show all 10 spot checks
  • ZIP hints: 20001 -> District of Columbia (DID 75), 83702 -> Boise (DID 495), 83843 -> standard rate = raw conus zipcodes file
  • Computed (independent Python from data/raw): example trip, U of I rule, 2 travelers: per diem $1,088.80 = 2 x (58 + 46.40 + 58 + 191 + 191); estimated reimbursement $1,865.00; A. Faculty $933.50, B. Staff $931.50; not reimbursable $13.50 (all match the app)
  • Computed: Washington DC Mar 30-Apr 2 2026, after noon / before 6 p.m., dinner Mar 30, breakfast Mar 31, lunch + dinner Apr 1: U of I M&IE $197.80 (46 + 82.80 + 23 + 46), federal $197.00 (31 + 69 + 28 + 69) = app
  • Computed: Coeur d'Alene Jul 6 + Seattle Jul 7-8 (Jul 8 personal), return before 6 p.m.: U of I $196.00 (58 + 92 + 0 + 46), federal $216.50 (55.5 + 92 + 0 + 69) = app
  • Computed: A. Faculty Jun 9 lodging edited 204.50 -> 190.00 gives reimbursement $1,864.00 (324.80 + 735 + 804.20) and $0.00 not reimbursable = app
Build notes ✎ written by the AI reviewer

FOLLOW-UP (2026-09-13): owner decision to default to U of I APM Ch. 70 with the federal 75% rule as a labelled alternative. 2 feedback rounds (12, 13), 794 harness seconds. Codex CLI 0.154.0 on MindRouter; no outages (network_error_lines 0), exit 0 both times.

POLICY RESEARCH (downloaded 2026-09-13; commit 35c6d39, data/raw only). Saved and quoted word for word in SOURCES.md sections 7-10: APM 70.02 page (last updated Jan 1, 2022), APM 70.04 page (Aug 2017, 'under revision'), the State of Idaho State Travel Policy SBEX 442-50 PDF (last amended Aug 20, 2026) and eCFR 41 CFR Part 301-11 as of 2026-09-01. Each has a plain-text extract, because Codex has no network and no PDF tools. (a) The APM 70.02 §I rule is confirmed exactly as the data verifier read it: 100% if the trip begins before 12 p.m. (noon), 50% after; 100% if it concludes after 6 p.m., 50% before; departure-day rate from that night's lodging location, return-day rate from the last overnight location. The page defines no per-meal deduction percentages. (b) APM 70.04 §E prints an in-state maximum of $45/day, but that figure is out of date. It equals the State Board of Examiners rate effective 10-1-15 (SBEX Appendix A). The State rate has since become $49 (2-19-19), $55 (1-1-22) and $58.00 (2-1-25), still current in the Aug 20, 2026 amendment. APM 70.02 (newer) sets the in-state allowance as 'the maximum amount allowed within the state of Idaho', so the app uses $58.00/day and explains the stale $45. This is a judgement call the owner should confirm; SPEC.md records it. The APM 70.02 'Accounts Payable web site' link now redirects to the Controller's Office landing page, which has no rate table, so the meal-rate table APM 70.02 relies on is SBEX 442-50 Appendix B. Provided meals under the U of I rule follow SBEX §6.C (25/35/55% for meals not provided). Also found: the FTR was renumbered by FTR Case 2025-05 (90 FR 56893, Dec 8, 2025). The 75% rule is now §301-11.20 and the meal deductions §301-11.21, and §301-11.21(b) floors deductions at the incidental amount. The app's old '§301-11.101' citation was stale, and its federal math could drop to $0.00.

SPEC.md updated with the decision (commit 4676505, SPEC.md only). Hand calculations in Python outside the project (handcalc_rules.py/.json), each under both rules: example Boise trip (U of I 162.40 / federal 192.00 per traveler); Boise in-state (174.00/215.00); Seattle, after noon / before 6 p.m. (276.00/322.00); Denver with provided meals (285.20/209.00); Coeur d'Alene, after noon / after 6 p.m. with meals (133.40/134.00); mixed Boise + Seattle (196.00/225.50); Idaho Falls with all meals provided (29.00/56.00).

ROUND 12 (686 s, 7 items). Codex added scripts/build_policy.py and data/embed/policy.json (asserts every number and quote against data/raw), the rules card, the U of I default, the rewritten math, itinerary badges and rate notes, and the summary/CSV rule display. In real Chrome all 12 trip/rule combinations already matched the Python values to the cent. It claimed everything worked, but my browser probe found these still wrong: Print summary threw 'ReferenceError: rm is not defined' (empty print view); Reset kept the federal rule; the federal summary/CSV still showed 'departure … — 100%'; the CSV printed meal codes ('provided: l'). I also found a real-browser focus bug: touch() skips the itinerary refresh when an itinerary checkbox still has focus, so a trip-level time change left badges and allowances stale. One of my own probe failures was an artefact (wrong Seattle destination id, and select_option not moving focus); I corrected the probe and did not report it.

ROUND 13 (108 s, 5 items, each with mechanism and checkable condition). Codex fixed all of them. Final real-Chrome probe: 34/34 checks and 12/12 trip/rule combinations, zero errors. A 15-check regression (typing focus, add row, image thumbnail, meal tick, personal day, flags, charts, save/reopen, phone) passed. verify_app is clean: 0 errors, 0 external requests, 0 phone overflow. Codex said it accidentally rewrote and restored FEEDBACK.md mid-round; the committed file matched mine, and it changed nothing in data/raw or SPEC.md.

I never edited src/, scripts/, data/embed/ or index.html. My only project writes: data/raw/* and SOURCES.md (commit 35c6d39), SPEC.md (4676505), and FEEDBACK.md, deleted at the end (345f830). The tamper guard never fired. No servers or browsers left running.

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 18.9 min · 6 files changed, 2487 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: 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.
    

    No feedback file was sent in this round.

    9 critic 0.5 min · 1 file changed, 97 insertions(+) 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.
    

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

    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)
    • 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.

    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
    • Print CSS styles .print-doc__h and .print-doc__sub, but the markup uses printdoc__h and printdoc__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.

    Then run python3 build.py --check.

    10 critic 3.3 min · 4 files changed, 222 insertions(+), 171 deletions(-) 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.
    

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

    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
    • 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.

    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

    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.
    

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

    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)
    • 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.

    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.

    14 critic 20.9 min · 4 files changed, 282 insertions(+), 238 deletions(-) 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.
    

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

    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
    • 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.

    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

    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.
    

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

    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

    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.
    

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

    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.

Read the specification

Travel Expense Reconciler — Per-diem math and receipt checks for university travel

Category: Administration Who it is for: Faculty and staff who travel, department business officers

What it should do

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.

Data

Real U.S. General Services Administration (GSA) FY2026 per-diem rates (lodging by month and M&IE) for a set of common destinations — include Moscow ID, Boise ID, Idaho Falls ID, Coeur d'Alene ID, Seattle WA, Washington DC, Denver CO — gathered at build time from the GSA per-diem site/API, with the source and fiscal year shown. State in the app that university policy (U of I Administrative Procedures Manual) governs and that rates must be verified.

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

  • Trip builder: travelers, legs, dates, destination per night, conference-provided meals per day

  • Automatic M&IE per day under the per-diem rule below (default: University of Idaho rule; alternative: federal 75% first/last-day rule), with provided-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

Per-diem rule — product decision by the project owner (2026-09-13)

The app defaults to University of Idaho policy (Administrative Procedures Manual, Chapter 70). It also offers the alternative "Federal rule (GSA: 75% of M&IE on first and last day) for federally sponsored travel". Both rules are explained in the UI, with citations and links. The active rule must be obvious in the summary, the print view and the CSV. The official policy texts are saved in data/raw/; SOURCES.md sections 7–10 quote them word for word.

  • Default: University of Idaho rule (APM 70.02, with the Idaho in-state maximum) Daily M&IE rate. For an Idaho (in-state) destination: the maximum allowed within the State of Idaho, $58.00 per day (State Travel Policy, SBEX Policy No. 442-50, Appendix B, last amended Aug 20, 2026). APM 70.02 §I sets the in-state allowance at "the maximum amount allowed within the state of Idaho". APM 70.04 §E still prints $45 a day (page dated August 2017, marked "under revision"). $45 was the State rate in effect from 10-1-15 and is no longer current, so the app uses $58.00 and explains why. For an out-of-state destination: the federal (GSA) M&IE rate for that locality (APM 70.02 §I).

  • Departure day: 100% if the trip begins before 12 p.m. (noon), 50% if it begins after noon. Return day: 100% if the trip concludes after 6 p.m., 50% if it concludes before 6 p.m. (APM 70.02 §I). Days in between are full days (100%). The departure-day rate is based on that night's lodging location; the return-day rate is based on the last overnight location.

  • Provided meals. The day's allowance counts only the meals not provided: 25% of the daily rate for breakfast, 35% for lunch, 55% for dinner (SBEX 442-50 §6.C; APM 70.02 §A requires compliance with State of Idaho travel policies). The allowance never exceeds the day's 100%/50% amount.

  • Alternative: Federal rule (GSA: 75% of M&IE on first and last day) for federally sponsored travel The GSA M&IE rate of the locality applies to every destination, Idaho included. The first and last day get 75%, full days 100% (41 CFR §301-11.20). Departure and return times do not change the amount.

  • Provided meals: deduct the full GSA breakdown amount for each meal (on the first and last day, from the 75% amount), but never go below the incidental-expenses amount (41 CFR §301-11.21).

  • Reference results (hand-calculated M&IE per traveler; lodging caps are unchanged): Boise, Jun 9–11 2026, departs before noon, returns after 6 p.m., no meals provided: U of I $174.00, Federal $215.00.

  • Seattle, Mar 2–5 2026, departs after noon, returns before 6 p.m.: U of I $276.00, Federal $322.00.

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

  • 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