Commit graph

36 commits

Author SHA1 Message Date
5f4ca3e7ef
fix(menu): address review feedback
Use the form deadline in the Monday Slack post, and compare that message exactly so CodeQL does not treat the test as URL sanitization.
2026-09-25 17:47:39 -04:00
265d12b890
feat(menu): publish the weekly menu from the job worker
Monday publish parses the catalog embedded in the Redefine menu page and runs on the Fargate worker, so the GitHub Actions scrape cron can go away.
2026-09-25 17:43:34 -04:00
renovate[bot]
aa15277112
chore(deps): update npm minor and patch (#210)
* chore(deps): update npm minor and patch

* test(openapi): stop pinning the Redocly patch version

Renovate minor bumps fail CI when the contract test hardcodes @redocly/cli.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Adam Moussa <adam@seahavenind.com>
2026-09-25 20:20:05 +00:00
Adam Moussa
f7957436bd
fix(iam): drop githubdeploy workflow_ref OIDC condition (PLAT-222) (#215)
Some checks failed
Deploy API / Deploy API to dev (push) Has been cancelled
Deploy API / Deploy API to prod (push) Has been cancelled
* fix(iam): drop githubdeploy workflow_ref OIDC condition

AWS STS does not evaluate GitHub workflow_ref, so that trust condition fail-closes AssumeRoleWithWebIdentity.

* chore(security): retarget githubdeploy Checkov suppression

Dropping the workflow_ref trust condition shifted CKV_AWS_111 from line 49 to 42. Permissions are unchanged.

* style: apply formatter

---------

Co-authored-by: sea-haven-auto-fix[bot] <5037331+sea-haven-auto-fix[bot]@users.noreply.github.com>
2026-09-22 21:30:25 +00:00
Adam Moussa
f632020b20
ci: convert onto org HCP reusables (PLAT-222) (#214)
Some checks are pending
Deploy API / Deploy API to dev (push) Waiting to run
Deploy API / Deploy API to prod (push) Waiting to run
* ci: convert onto org HCP reusables

Switch Fargate CD and CI to the v1.0.13 org workflows, emit ci-complete, and retarget githubdeploy OIDC to the reusable plus the thin caller.

* chore(security): retarget githubdeploy Checkov suppression

The OIDC dual-claim edit shifted CKV_AWS_111 from line 40 to 49. Permissions are unchanged.

* style: apply formatter

* ci: pin org reusables to v1.0.14

Drop collect-only and requirements from the python lint caller now that ci-python-app is lint-only.

* test(ci): probe autofix with a ruff format violation

* style: apply formatter

---------

Co-authored-by: sea-haven-auto-fix[bot] <5037331+sea-haven-auto-fix[bot]@users.noreply.github.com>
2026-09-22 20:01:17 +00:00
Adam Moussa
d7ad49d00f
feat(api): add OpenAPI Redocly contract and VPC outputs (DEV-289) (#206)
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
* feat(infra): export attached VPC ids and lock prod to afterhours (DEV-289)

Prod must keep existing_vpc_id pointed at the afterhours VPC. Outputs
expose the resolved vpc_id and public subnet IDs.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* feat(api): add OpenAPI 3.1 and Redocly lint in CI (DEV-289)

Same extends: recommended ruleset and @redocly/cli 2.52.1 as
internal-portal. Documents current { error: string } JSON errors.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix(api): document 4xx and reject invalid form-status weeks (DEV-289)

Health, form-status, and roster document 400. form-status now maps
current and returns 400 for a week that is not current or YYYY-WNN.
Redocly treats 302 as a success response, matching the portal.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* style(test): format VPC contract assertions for ruff (DEV-289)

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix(api): fail Redocly on missing 4xx and 2xx/3xx (DEV-289)

Promote operation-4xx-response and the 2xx-or-3xx success rule to error.
Replace unused health and roster 400s with 403, matching portal health.
Form-status keeps its real 400 for invalid week.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix(api): split week params and allow live menu nulls (DEV-289)

Menu and form-status take current or YYYY-WNN. Orders take YYYY-WNN or a
calendar date and reject current. Menu payloads may emit null menu_url,
calories, protein, and image_url.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix(infra): fail prod apply without the afterhours VPC (DEV-289)

Prod never creates the 10.60 fallback VPC. A terraform_data precondition
fails plan and apply when existing_vpc_id is empty, instead of a check
block that only warns.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-09-22 00:33:48 +00:00
Adam Moussa
fb3a181e0c
feat(api): add flask sentry sdk (#205)
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
Initialize the SDK on gunicorn and the SQS worker with afterhours-style scrubbing. Store the DSN in SSM and inject only the parameter name onto the live task.
2026-09-21 23:48:09 +00:00
Adam Moussa
596e949eef
fix(form): keep Google sign-in across page refresh (#204)
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
2026-09-21 22:50:07 +00:00
Adam Moussa
3efff5e784
fix(infra): share the afterhours VPC in prod (PLAT-215) (#203)
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
* fix(infra): share the afterhours VPC when existing_vpc_id is set

Prod is at the account VPC quota, so the v1.0.0 apply destroyed Lambda/API Gateway then failed on CreateVpc. Skip creating a sixth VPC when the workspace supplies afterhours subnets.

* style(test): format the VPC terraform assertion for ruff
2026-09-21 21:15:47 +00:00
Adam Moussa
697deb94fd
fix(iam): omit checkcomponents from the ECS boundary in non-prod (#202)
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
2026-09-21 16:26:17 -04:00
Adam Moussa
f48a82c476
feat(api): serve meals on ECS Fargate instead of Lambda (PLAT-215) (#199)
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
* feat(api): serve meals on ECS Fargate instead of Lambda

Keep the Flask app always-on with in-process jobs so CloudFront no longer fronts a cold-start API Gateway.

* fix(jobs): run delayed close and reminder deliveries

Wall-clock skip windows dropped the only weekly SQS attempt when Scheduler already fired in Eastern time. Dev schedules stay disabled.

* fix(api): return JSON objects and stop logging job payloads

Flask now jsonify-s handler dicts so API responses are not HTML, and the worker logs only event and status.

* fix(ci): restore the reusable workflow so the required check is named ci / ci

Inlining the job reported `ci` instead of the org ruleset's `ci / ci`.

* fix(secrets): drop unused os import so ruff check passes

* style: apply ruff format so ci-python-app lint passes

* fix(infra): give meals its own VPC because prod has none

* chore(security): re-key ALB SG checkov suppression after vpc.tf
2026-09-21 19:34:24 +00:00
Adam Moussa
12f1eb881f
fix(auth): accept federated portal Cognito tokens for meals admin (DEV-283) (#194)
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
* fix(auth): accept federated portal Cognito tokens for meals admin

Google Workspace federation stores email_verified=false, which 403'd the
portal Admin probe while the public menu still loaded.

* fix(iam): grant plan role CloudFront DescribeFunction
2026-09-18 15:31:17 +00:00
Adam Moussa
85f36fcfff
feat(edge): proxy portal meals API paths on orders.seahaven.com (DEV-282) (#193)
Some checks are pending
Build Lambda Layer / build (push) Waiting to run
* feat(edge): proxy portal meals API paths on orders.seahaven.com (DEV-282)

Keep the static form on / while CloudFront forwards submit, form-status, admin, and caller-only order lookup so the portal can use the public meals host without a form redirect.

* fix(style): apply ruff format
2026-09-17 23:52:51 +00:00
Adam Moussa
26a92a2f62
feat(auth): accept portal Cognito ID tokens (DEV-238) (#192)
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
* feat(auth): accept portal Cognito ID tokens

* fix(auth): distinguish portal verification outages

* docs(auth): document Cognito workspace variables
2026-09-15 22:12:01 +00:00
Adam Moussa
86bb476e27
feat(menu): expose production menu API (#191) 2026-09-15 21:41:10 +00:00
Adam Moussa
c1552f0bd3
chore(meals): remove email_report payroll SES path (PLAT-135) (#187)
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
* chore(meals): remove email_report payroll SES path (PLAT-135)

Stop Monday SES deduction emails now that Flex checkcomponents owns payroll posting.

* chore(meals): delete email_report handler and SAM resources

Remove the leftover SES Lambda source so it cannot be redeployed from template.yaml.
2026-09-10 19:29:58 +00:00
Adam Moussa
62f61f61d1
feat(meals): send weekly deductions to paychex checkcomponents (PLAT-154) (#184)
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
* feat(meals): send weekly deductions to paychex checkcomponents (PLAT-154)

* fix(meals): round checkcomponents amounts half-up

Keep SQS deduction amounts on the same rounding path as submit_order so extra-precision totals cannot diverge by a cent.
2026-09-03 22:04:02 +00:00
Adam Moussa
8489dc3986
feat(meals): dual-read week keys and meal-to-Flex join (PLAT-134) (#182)
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
* feat(meals): dual-read week keys and meal-to-Flex join (PLAT-134)

* style(meals): apply ruff format (PLAT-134)

* docs(meals): clarify week-key dual-read is same-instant (PLAT-134)
2026-09-02 00:01:44 +00:00
Adam Moussa
58b9d4f83b
fix(pdf): fit weekly summaries on one page without a broken title (#138)
Some checks failed
Build Lambda Layer / build (push) Has been cancelled
Helvetica cannot encode an em dash, so the header rendered a question mark. Tighter single-column spacing keeps a 12-person week on page 1 while still paginating large weeks.
2026-08-15 00:17:27 +00:00
Adam Moussa
a69e6d0a7c
fix(form): restore description toggles after sign-in (#107)
Some checks are pending
Deploy / deploy (push) Waiting to run
* fix(form): restore description toggles after sign-in

* fix(form): reset expanded descriptions on re-auth
2026-08-03 16:19:46 -04:00
Adam Moussa
88399fe153
refactor(weekly-menu): isolate menu writes behind API (#94)
* feat(api): add IAM-authenticated menu publication

Keep weekly menu writes behind Lambda so the GitHub runtime role cannot access the shared DynamoDB table directly.

* refactor(workflow): publish weekly menus through API

Use SigV4 requests for settings and menu publication so the scheduled workflow no longer needs direct DynamoDB access.

* fix: address review comments

* style(python): apply Ruff formatting
2026-08-03 14:27:59 -04:00
Adam Moussa
7e73bd2bfe
test(form): stabilize mobile admin assertions (#93)
Some checks are pending
Deploy / deploy (push) Waiting to run
2026-08-03 14:21:10 -04:00
Adam Moussa
311eab35c0
fix(auth): require Google authentication in cloud mode (#90)
* fix(auth): require Google authentication in cloud mode

Remove the public shared-key mechanism and fail closed on Google auth while adding submit-route throttling.

* fix(auth): address review follow-ups

Fail closed on whitespace-only Google configuration and centralize shared authentication behavior.

* test(auth): use non-secret Google client fixture

Make the public test identifier explicit so secret scanning does not misclassify it as an API key.

* test(auth): avoid OAuth-shaped fixture

Use a format-neutral audience value so secret scanning can distinguish the fixture from a real client identifier.

* chore(security): suppress public OAuth fixture

Document the scanner false positive without suppressing any runtime credential flow.
2026-08-03 13:51:12 -04:00
Adam Moussa
adf175daef
feat(form): render admin orders as mobile cards (#86)
Some checks failed
Deploy / deploy (push) Has been cancelled
* feat(form): render admin orders as mobile cards

* fix(form): address mobile admin review findings

* fix(tests): remove unused mobile fixture state
2026-07-31 10:15:43 -04:00
Adam Moussa
602b0c7fd0
fix(form): accessibility for qty, status, and descriptions (#81)
* fix(form): add status regions, live total, and a11y CSS

Dual alert/status slots for form and admin, aria-live on the sticky
total, success heading focus target, More/Less affordance styles, and
44px coarse-pointer chip padding.

* fix(form): wire a11y labels, status helper, and desc expand

Meal-scoped qty labels at card create time, aria-pressed filter chips,
dual-node showStatus replacing all alert() calls (confirm retained),
overflow-gated More/Less, and success-heading focus after submit.

* test(form): cover a11y labels, status region, and desc toggle

Structural checks for dual status nodes and no alert(); Playwright for
init-time qty labels, aria-pressed chips, overflow More/Less, and
failed-submit text landing in role=alert.

* fix: address review comments
2026-07-31 10:15:43 -04:00
Adam Moussa
83077f7aa9
test(form): cover sticky footer at narrow widths (#85)
* test(form): cover sticky footer at narrow widths

* fix(form): address review findings

* fix(form): guard stale admin edit responses

* test(form): stub admin lookup in browser tests
2026-07-31 10:15:42 -04:00
Adam Moussa
30fa7fe352
fix(form): wrap Google user bar at phone widths (#84)
* fix(form): wrap Google user bar at phone widths

Allow the signed-in Google identity and admin controls to wrap below 480px while preserving the desktop row, with generated-form Playwright coverage for phone widths.

* fix(form): preserve 480px user bar boundary

Keep the mobile layout below 480px and lock both sides of the breakpoint with browser coverage.

* style(tests): apply ruff formatting

* test(form): guarantee Playwright browser cleanup

* test(form): strengthen phone-width coverage

* fix: add @classmethod and use cls in tests/test_generate_form.py

Co-authored-by: seahaven-openswe[bot] <296972425+seahaven-openswe[bot]@users.noreply.github.com>

* fix(tests): restore class fixture discovery

---------

Co-authored-by: seahaven-openswe[bot] <296972425+seahaven-openswe[bot]@users.noreply.github.com>
2026-07-31 10:15:42 -04:00
Adam Moussa
216618a862
refactor(form): extract Jinja templates and lock form JS in CI (#77)
Some checks are pending
Deploy / deploy (push) Waiting to run
* refactor(form): extract Jinja templates and lock form JS in CI

Split the monolithic generate_form f-string into form.html.j2/css/js
plus admin.js, inject a single window.CONFIG blob, and add structural
plus Playwright coverage so qty delegation and clamp stay green in CI.

* Update src/server/generate_form.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(form): isolate admin script bindings

* fix(form): address admin and form review findings

* fix(form): resolve remaining review nitpicks

* ci(workflow): restore required check context

Keep the reusable workflow caller job compatible with the organization-required ci / ci status check.

* fix(form): address remaining review findings

* fix: apply CodeRabbit auto-fixes

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
2026-07-30 19:19:51 -04:00
Adam Moussa
44b21f4033
fix(test): fix aggregated notifier tests + enable CI test suite (INFRA-72) (#39)
Some checks failed
Deploy / deploy (push) Has been cancelled
* fix(test): mock get_settings/get_roster in aggregated tests + enable CI tests (INFRA-72)

handle_orders_aggregated now delivers the summary via admin DMs (PR #15),
adding get_settings/get_roster calls the aggregated tests never mocked, so
they hit live DynamoDB. Mock both and assert the message content on the
send_dm path. Set run-tests: true so the suite actually runs in CI.

* fix(test): default AWS region in conftest so CI collection doesn't hit NoRegionError (INFRA-72)

Handlers build boto3 clients at module load; CI runners have no AWS config,
so test collection raised NoRegionError once the suite actually ran. Set a
region default before imports (offline client construction; calls are mocked).

* fix(test): add repo root to sys.path so CI's bare pytest collects functions.* (INFRA-72)

test_aggregate_orders imports functions.aggregate_orders.handler, which needs
the repo root on sys.path. python -m pytest injects CWD automatically but CI
runs pytest directly, so these 11 tests errored at collection in CI only.
2026-07-08 16:33:42 -04:00
Adam Moussa
75fb3280f5
Add gateway-level authorizer to admin API (INFRA-100) (#24)
Some checks failed
Deploy / deploy (push) Has been cancelled
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.

- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
  token (aud + allowed Workspace domain) and the admin_emails allow-list from
  DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
  on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
  (AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
  served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
  public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.

No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.

Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
f2a0692577 Harden summary-PDF endpoint per cross-review
Address GPT-4.1 cross-review of the IAM change:

- Validate the week param (YYYY-WNN) and the DynamoDB-sourced PDF key
  shape before presigning, so a tampered SUMMARY record can't mint URLs
  for other report files (payroll CSVs).
- Narrow the IAM resource from reports/* to
  reports/*/weekly-summary-*.pdf — least privilege over the bucket.
- Reuse a module-level S3 client; name the URL TTL constant.
- Distinguish "week not found" from "PDF key missing" 404s.
- Tests: malformed-week 400 and tampered-key 500 (asserts no presign).
2026-06-05 18:44:25 -04:00
b6733703ab Add admin summary-PDF download endpoint and button
The weekly summary PDF generated at Thursday close was stored in the
reports bucket with no way to reach it from the UI — admins had to pull
it from S3 manually. Surface it in the admin panel:

- submit_order: GET /api/admin/summary-pdf?week= (admin-gated) returns
  a 5-minute presigned URL from the SUMMARY item's stamped PDF key;
  404 for weeks that haven't closed.
- template.yaml: REPORTS_BUCKET env var + read-only s3:GetObject on
  reports/* for SubmitOrderFunction (needed so the presigned URL is
  signed with sufficient permissions).
- generate_form.py: "Download summary PDF" button in the admin header;
  explains Thursday-close timing on 404.
- Tests: presign happy path, 404 open week, 400 missing week, 401
  unauthenticated.
- README updated.
2026-06-05 18:41:55 -04:00
Adam Moussa
10d135e32e
Add weekly summary PDF and admin order-list download (#16) (#17)
Some checks are pending
Deploy / deploy (push) Waiting to run
Generate a per-person weekly summary PDF at Thursday close and store it
alongside the CSV reports, plus a client-side admin download that rolls
orders up into item -> total quantity for bulk ordering.

- shared/pdf.py: build_weekly_summary_pdf() via fpdf2 (pure-Python,
  ARM64-safe; first non-boto3 layer dep). Per-person employee -> item ->
  quantity, no pricing.
- aggregate_orders: write reports/{week}/weekly-summary-{week}.pdf
  (application/pdf) and stamp weekly_summary_pdf_s3_key on the SUMMARY.
  No new IAM (existing S3CrudPolicy). No email/Slack delivery.
- generate_form.py: "Download order list" admin button aggregates the
  loaded week's orders into an item->qty CSV (no per-employee breakdown,
  no prices) via a Blob download. Works for open weeks too.
- Tests: tests/test_pdf.py; aggregate happy-path now asserts 3 S3
  uploads + the pdf key.
- README updated.
2026-06-01 18:49:58 -04:00
Adam Moussa
1aa28b38fd
Add admin form bypass and send order summary to admin DMs (#15)
Admins (by email in settings) can now view and submit orders even
after the form closes. The orders-aggregated Slack summary is sent
as a DM to each admin instead of posting to the channel.
2026-05-22 12:24:21 -04:00
Adam Moussa
5db95ce9d1
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
Some checks failed
Deploy / deploy (push) Has been cancelled
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule

Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.

* Rename Secrets Manager env vars to avoid CI false positive

The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
2026-05-19 16:32:19 -04:00
Adam Moussa
a752c24e0f
Add discount pricing, Google auth, and order hardening (#10)
Some checks failed
Deploy / deploy (push) Has been cancelled
* Add discount settings and two-tier pricing to order aggregation

Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).

* Add Google OAuth, server-side discounts, and Slack order confirmations

Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.

* Update SAM template for Google auth, Slack invocation, and deadline change

Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.

* Update order form UI and CI workflow for new features

Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.

* Add SSM GetParameter permission to submit order Lambda

Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.

* Harden auth, pricing, and reliability in order handlers

Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.

* Fix XSS risks and add closed-form UX to order page

Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.

* Document CORS, cron idempotency, and SSM config in template

Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.

* Add unit tests for submit, notify, and aggregate handlers

50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.

* Use full email as order slug for defense-in-depth

Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.

* Remove unused imports flagged by ruff

* Apply ruff formatting

* Fix PR review findings: auth, rounding, and close-form guard

- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)

* Fix close-form weekday guard and SSM auth fail-open

- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
  crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
  _get_google_client_id() (fetches value). If auth is configured but the SSM
  fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed

* Harden Flask dev server auth and escaping

- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
  bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json

* fix: Email order filenames, SSM param TTL, DST-safe reopen_at

- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* Apply ruff formatting to submit_order handler

* fix(server): retry SSM for Google client id after TTL on failure

Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(server): ruff-format Google client id cache helper

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron

EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix(submit-order): bill from Dynamo menu retail, not client JSON

Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.

Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>

* fix: use single braces in loadRoster JS nested string

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Eastern fallback countdown

* Fix pricing validation and JWT display decoding

* Fix optional Google auth detection

* Format app.py line length for ruff compliance

* Fix auth config check and URL escaping in form

- _google_auth_configured() now checks env var presence (intent), not
  the fetched SSM value — prevents silent auth bypass if SSM param is
  deleted
- Add </script> escaping to URL values in generate_form.py for
  consistency with other injected values

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00