feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(roles): project roles are Admin, Director, Student - #311

Merged
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student
Aug 12, 2026
Merged

feat(roles): project roles are Admin, Director, Student#311
nourshoreibah merged 1 commit into
mainfrom
worktree-project-roles-director-student

Conversation

@nourshoreibah

Copy link
Copy Markdown
Collaborator

Project membership roles become Admin, Director, Student.

Mapping

OldNew
AdminAdmin
PIDirector
AccountantDirector
StaffStudent

Permissions are unchanged

PI and Accountant could both edit; Staff could not. So editableRoles collapses from ['PI', 'Accountant', 'Admin'] to ['Director', 'Admin'] and every existing authorization outcome is preserved. Student is read-only on expenditures, matching Staff today. The global users.is_admin flag and the frontend nav roles (admin/standard/limited) are a separate mechanism and are untouched.

The migration is expand-only

20260812011405_rename_project_roles.sql widens the CHECK to old ∪ new before backfilling, so the currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the window between the migration and the lambda deploy. A follow-up migration drops the three old names once this is live — it is the contract half and needs -- allow-destructive:.

One transient effect during that window: the still-deployed code checks ['PI','Accountant','Admin'], so a backfilled Director row is denied edit (read is membership-only, so reads are unaffected) until the new lambdas land. Self-healing, no data impact.

Verification

  • expenditures — 107 tests pass (unit + e2e)
  • projectsprojects.e2e 28 pass, delete-authz.unit + dashboard.unit 13 pass
  • donors — 50 pass; health test 🌞 fails, but it fails identically on unmodified main (it fetches localhost:3000/donors/health, which needs the lambda container up)
  • apps/frontendtsc --noEmit clean
  • Post-migration DB state: constraint is old ∪ new, rows are 2 Director / 1 Student

shared/types/db-types.d.ts is unchanged — role is still VARCHAR(30)string, so the generated types can't drift.

Left alone deliberately

The Staff headings in ProjectCard.tsx and ProjectDetailClient.tsx are generic labels for the whole member list, not the role — renaming them to "Students" would mislabel Directors.

🤖 Generated with Claude Code

PI and Accountant both become Director, Staff becomes Student, Admin is
unchanged. Edit rights are unaffected: PI and Accountant could both edit,
Staff could not, so editableRoles collapses from ['PI','Accountant','Admin']
to ['Director','Admin'].
The migration is expand-only. It widens the CHECK to old ∪ new so the
currently deployed code can keep writing 'PI'/'Accountant'/'Staff' during the
window between the migration and the lambda deploy, then backfills the rows.
A follow-up migration drops the three old names once this is live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

This PR contains a database migration

  • apps/backend/db/migrations/20260812011405_rename_project_roles.sql

It will be applied to the production database automatically when this PR merges, before the new lambda code is deployed. Please confirm before requesting review:

  • Applied and tested locally.cd apps/backend && make migrate, then run the affected lambda's tests (cd apps/backend/lambdas/<name> && npm test). make show-migrations shows what applied.
  • Safe for the code that is live right now. During the deploy window -- and indefinitely if the deploy fails -- the currently deployed lambdas run against your new schema. Additive changes (CREATE TABLE, nullable ADD COLUMN, CREATE INDEX) are fine in one PR. DROP COLUMN, renames, ADD COLUMN NOT NULL with no default, and new UNIQUE/CHECK/FOREIGN KEY constraints need two merged PRs -- see the expand/contract rules in apps/backend/db/README.md.
  • Kept separate from unrelated changes. A migration PR should ideally contain the migration, the code that needs it, and nothing else. It changes production state, it is the one thing here that redeploying cannot roll back, and a reviewer should be able to see the whole schema change without scrolling past unrelated work.
  • No already-merged migration was edited. Fix an old migration by adding a new one; there is no down.

shared/types/db-types.d.ts is regenerated and pushed to this branch automatically -- don't hand-edit it. Expect one red migrations-fresh check before that commit lands.

@github-actions

Copy link
Copy Markdown
Contributor

Database Types Check Complete

The database schema files were modified, but the regenerated TypeScript types are identical to the existing ones.

No changes were needed and the type definitions are already up to date.

@nourshoreibahnourshoreibah added the no-review The PR review bot won't run label Aug 12, 2026
@nourshoreibah
nourshoreibah marked this pull request as ready for review August 12, 2026 01:38
@nourshoreibah
nourshoreibah merged commit c8dfcfb into mainAug 12, 2026
22 checks passed
@nourshoreibah
nourshoreibah deleted the worktree-project-roles-director-student branch August 12, 2026 01:38
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
…load route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
)
* feat(expenses): admin approve/deny review flow with receipt upload
Implements the Figma "Expenses Page" section (node 3545:34605).
Frontend
- Status column and pill: Approved / Pending / Needs Info, using the
design's exact fills. `needs_more_info` was already permitted by the
DB check constraint, so no enum change was needed.
- New ReviewExpenseModal. Everyone sees the expense read-only; the
Admin Decision pills and Admin Notes are rendered only for admins,
and Save Changes is admin-only.
- Table now matches the design: Expense ID, Date, Type of Expense,
Project, Amount, Receipt, Status. Description was dropped; the
project detail page hides Project via `showProject`.
- Filters consolidated into one "Filter By" nested menu (Month /
Project / Type / Status) plus "Clear Filters (n)".
- The receipt is now actually uploaded. FileUpload previously ran a
fake setInterval progress bar and the File was never sent anywhere,
so receipt_url was always null. It now presigns, PUTs to S3 with
real XHR progress, and passes the object URL through to the POST.
- resetForm did not clear the selected file, so a cancelled modal
reopened holding the previous receipt.
Backend
- GET /expenditures/upload-url presigns a PDF PUT under receipts/.
- GET /expenditures/{id}/receipt presigns a short-lived GET, so the
receipt does not depend on the bucket being publicly readable.
- PATCH /expenditures/{id}/status accepts and persists adminNotes.
- GET /expenditures/{id} returns the submitter and project names for
the modal's "Submitted By".
- validateExpenditureInput read body.receipt_url while every other
field was camelCase; it now accepts receiptUrl and keeps the old
key working.
Infra
- The shared lambda role had no S3 permissions, so a presigned PUT
would have failed AccessDenied. Adds PutObject/GetObject.
Also fixes a pre-existing `next build` failure: page modules may not
have non-page exports, and both accounts/page.tsx and
expenses/page.tsx did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): size the status pill and review modal from content
The pill was pinned to Figma's 81x29, which clips longer labels such as
Needs Info and the legacy denied fallback. 81px is now a min-width and
the label drives the real width.
Review modal now shrinks below its 485px Figma width, the field labels
flex instead of sitting at a fixed 120px, and a long receipt filename
truncates rather than pushing the actions off the row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(expenses): use the Director/Admin project roles on the receipt upload route
The presigned upload route was written against the old PI/Accountant/Admin
allow-list and git merged it cleanly over #311, so it silently kept roles
that no longer exist. Any non-global-admin would have been refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: regenerate lambda READMEs
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
nourshoreibah added a commit that referenced this pull request Aug 12, 2026
Resolves conflicts between the admin-only dashboard and main's expense
approval flow (#315), project role rename (#311) and audit fixes (#310):
- routes: /dashboard is admin-gated, /expenses is not. Main opened
/expenses to non-admins because they submit and read their own
expenses there; only the review modal's approve/deny is admin-gated.
- accounts: both sides moved the staff roster out of page.tsx to satisfy
the Next.js page-export rule. Kept main's mockUsers.ts and dropped the
duplicate staff.ts.
- Navbar/routes tests follow the same split.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-reviewThe PR review bot won't run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@nourshoreibah