From 46b7995ce6444582bf697ac713c8ae409e10f96c Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Sat, 14 Mar 2026 00:01:34 -0500 Subject: [PATCH 01/38] clean up compose file env vars --- config/infra/docker-compose.override.yml | 4 ---- config/infra/docker-compose.yml | 26 +++++++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/config/infra/docker-compose.override.yml b/config/infra/docker-compose.override.yml index 3ffa947..1f17fa7 100644 --- a/config/infra/docker-compose.override.yml +++ b/config/infra/docker-compose.override.yml @@ -53,10 +53,6 @@ services: volumes: - ../../server/data:/src/data - ../../server/logs:/src/logs - environment: - - EMAIL__BACKEND=local - - EMAIL__SMTP_PORT=${EMAIL__SMTP_PORT?} - - GH__BACKEND=console develop: watch: diff --git a/config/infra/docker-compose.yml b/config/infra/docker-compose.yml index 1e82841..3e50967 100644 --- a/config/infra/docker-compose.yml +++ b/config/infra/docker-compose.yml @@ -92,15 +92,18 @@ services: - EMAIL__BACKEND=${EMAIL__BACKEND?} - EMAIL__EMAIL_FROM=${EMAIL__EMAIL_FROM?} + # internal host - EMAIL__SMTP_HOST=host.docker.internal - EMAIL__SMTP_PORT=${EMAIL__SMTP_PORT?} - - EMAIL__SMTP_USERNAME=${EMAIL__SMTP_USERNAME?} - - EMAIL__SMTP_PASSWORD=${EMAIL__SMTP_PASSWORD?} + # not required for non-smtp backends + - EMAIL__SMTP_USERNAME=${EMAIL__SMTP_USERNAME} + - EMAIL__SMTP_PASSWORD=${EMAIL__SMTP_PASSWORD} - GH__BACKEND=${GH__BACKEND?} - - GH__REPO_OWNER=${GH__REPO_OWNER?} - - GH__TOKEN=${GH__TOKEN?} - - GH__ISSUE_ASSIGNEE=${GH__ISSUE_ASSIGNEE?} + # not required for console backend + - GH__REPO_OWNER=${GH__REPO_OWNER} + - GH__TOKEN=${GH__TOKEN} + - GH__ISSUE_ASSIGNEE=${GH__ISSUE_ASSIGNEE} command: alembic upgrade head @@ -149,15 +152,18 @@ services: - EMAIL__BACKEND=${EMAIL__BACKEND?} - EMAIL__EMAIL_FROM=${EMAIL__EMAIL_FROM?} + # internal host - EMAIL__SMTP_HOST=host.docker.internal - EMAIL__SMTP_PORT=${EMAIL__SMTP_PORT?} - - EMAIL__SMTP_USERNAME=${EMAIL__SMTP_USERNAME?} - - EMAIL__SMTP_PASSWORD=${EMAIL__SMTP_PASSWORD?} + # not required for non-smtp backends + - EMAIL__SMTP_USERNAME=${EMAIL__SMTP_USERNAME} + - EMAIL__SMTP_PASSWORD=${EMAIL__SMTP_PASSWORD} - GH__BACKEND=${GH__BACKEND?} - - GH__REPO_OWNER=${GH__REPO_OWNER?} - - GH__TOKEN=${GH__TOKEN?} - - GH__ISSUE_ASSIGNEE=${GH__ISSUE_ASSIGNEE?} + # not required for console backend + - GH__REPO_OWNER=${GH__REPO_OWNER} + - GH__TOKEN=${GH__TOKEN} + - GH__ISSUE_ASSIGNEE=${GH__ISSUE_ASSIGNEE} healthcheck: test: From 90f24ab396b466c725f47226a9f4bea10ca6e0da Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Sat, 14 Mar 2026 00:15:10 -0500 Subject: [PATCH 02/38] add actrc, update readme --- .actrc | 2 ++ README.md | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .actrc diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..61dbaa5 --- /dev/null +++ b/.actrc @@ -0,0 +1,2 @@ +--container-architecture linux/amd64 +-P self-hosted=catthehacker/ubuntu:act-latest diff --git a/README.md b/README.md index a7caf45..d0df295 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ [![CI / Build Images](https://github.com/arcot-labs/RepTrack/actions/workflows/build.yml/badge.svg)](https://github.com/arcot-labs/RepTrack/actions/workflows/build.yml) [![CD / Deploy Application](https://github.com/arcot-labs/RepTrack/actions/workflows/deploy.yml/badge.svg)](https://github.com/arcot-labs/RepTrack/actions/workflows/deploy.yml) -### Local Development +# RepTrack + +## Local Development Copy `.env.example` to `.env` & populate variables @@ -25,7 +27,23 @@ Start containers: ./scripts/dev.sh ``` -### Database +## Local GitHub Actions Testing + +Use `act` to run workflows locally for quick validation. + +List all workflows: + +```bash +act -l +``` + +Run a specific job: + +```bash +act -j {job-id} +``` + +## Database All writes should go through SQLAlchemy From 011ac680a172210ae9e93f0321ba3d6cff753404 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Sat, 14 Mar 2026 01:37:31 -0500 Subject: [PATCH 03/38] client - add custom view label support for data table --- .../data-table/DataTableViewOptions.tsx | 17 +++++++++++++++-- client/src/components/data-table/README.md | 4 ++++ client/src/lib/text.ts | 12 ++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 client/src/lib/text.ts diff --git a/client/src/components/data-table/DataTableViewOptions.tsx b/client/src/components/data-table/DataTableViewOptions.tsx index b34503e..978f504 100644 --- a/client/src/components/data-table/DataTableViewOptions.tsx +++ b/client/src/components/data-table/DataTableViewOptions.tsx @@ -10,6 +10,16 @@ import { DropdownMenuLabel, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu' +import { formatIdentifier } from '@/lib/text' + +interface DataTableColumnMeta { + viewLabel?: string +} + +function getColumnViewLabel(columnId: string, meta?: unknown): string { + const resolvedMeta = meta as DataTableColumnMeta | undefined + return resolvedMeta?.viewLabel ?? formatIdentifier(columnId) +} export function DataTableViewOptions({ table, @@ -39,16 +49,19 @@ export function DataTableViewOptions({ column.getCanHide() ) .map((column) => { + const columnLabel = getColumnViewLabel( + column.id, + column.columnDef.meta + ) return ( { column.toggleVisibility(value) }} > - {column.id} + {columnLabel} ) })} diff --git a/client/src/components/data-table/README.md b/client/src/components/data-table/README.md index 2c600fb..0175cdf 100644 --- a/client/src/components/data-table/README.md +++ b/client/src/components/data-table/README.md @@ -101,6 +101,7 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'status', + meta: { viewLabel: 'Status' }, header: ({ column }) => ( ), @@ -114,6 +115,9 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => , }, ] + +// To customize labels shown in the "View" column toggle menu, +// set `meta.viewLabel` on each column definition. ``` ### 3. Configure Toolbar diff --git a/client/src/lib/text.ts b/client/src/lib/text.ts new file mode 100644 index 0000000..96ada11 --- /dev/null +++ b/client/src/lib/text.ts @@ -0,0 +1,12 @@ +export const capitalizeWords = (str: string) => + str.replace(/\b\w/g, (char) => char.toUpperCase()) + +export const formatIdentifier = (str: string) => + capitalizeWords( + str + // split camelCase + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + // replace underscores & hyphens with spaces + .replace(/[_-]+/g, ' ') + .trim() + ) From d1cfc463cda8a4790b47306fb19d72b41de61887 Mon Sep 17 00:00:00 2001 From: aditya-arcot Date: Sat, 14 Mar 2026 01:51:41 -0500 Subject: [PATCH 04/38] client - add initial exercises page --- client/src/AppRoutes.tsx | 2 + client/src/components/ExercisesTable.tsx | 221 +++++++++++++++++++++++ client/src/layout/AppLayout.tsx | 1 + client/src/pages/Exercises.tsx | 77 ++++++++ 4 files changed, 301 insertions(+) create mode 100644 client/src/components/ExercisesTable.tsx create mode 100644 client/src/pages/Exercises.tsx diff --git a/client/src/AppRoutes.tsx b/client/src/AppRoutes.tsx index ad74c38..c7119d2 100644 --- a/client/src/AppRoutes.tsx +++ b/client/src/AppRoutes.tsx @@ -6,6 +6,7 @@ import { AppLayout } from '@/layout/AppLayout' import { Admin } from '@/pages/Admin' import { Dashboard } from '@/pages/Dashboard' import { Docs } from '@/pages/Docs' +import { Exercises } from '@/pages/Exercises' import { ForgotPassword } from '@/pages/ForgotPassword' import { Login } from '@/pages/Login' import { Register } from '@/pages/Register' @@ -25,6 +26,7 @@ export function AppRoutes() { } > } /> + } /> }> } /> } /> diff --git a/client/src/components/ExercisesTable.tsx b/client/src/components/ExercisesTable.tsx new file mode 100644 index 0000000..71e0604 --- /dev/null +++ b/client/src/components/ExercisesTable.tsx @@ -0,0 +1,221 @@ +import { type ExercisePublic, type MuscleGroupPublic } from '@/api/generated' +import { zExercisePublic } from '@/api/generated/zod.gen' +import { DataTable } from '@/components/data-table/DataTable' +import { DataTableColumnHeader } from '@/components/data-table/DataTableColumnHeader' +import { DataTableInlineRowActions } from '@/components/data-table/DataTableInlineRowActions' +import { createSelectColumn } from '@/components/data-table/DataTableSelectColumn' +import { Badge } from '@/components/ui/badge' +import { + blueText, + greenText, + lightBlueBackground, + lightGreenBackground, + redText, +} from '@/lib/styles' +import { capitalizeWords } from '@/lib/text' +import type { + DataTableRowActionsConfig, + DataTableToolbarConfig, + FilterOption, +} from '@/models/data-table' +import type { ColumnDef } from '@tanstack/react-table' +import { Pencil, Plus, Trash2 } from 'lucide-react' +import { useState } from 'react' + +const blueBadgeClassName = `${lightBlueBackground} ${blueText}` +const greenBadgeClassName = `${lightGreenBackground} ${greenText}` + +function TypeBadge({ userId }: { userId: number | null }) { + if (userId === null) { + return System + } + return Custom +} + +function getTypeFilterOptions(): FilterOption[] { + return [ + { label: 'System', value: 'system' }, + { label: 'Custom', value: 'custom' }, + ] +} + +interface ExercisesTableProps { + exercises: ExercisePublic[] + muscleGroups: MuscleGroupPublic[] + isLoading: boolean + onReloadExercises: () => Promise +} + +export function ExercisesTable({ + exercises, + // muscleGroups, + isLoading, + // onReloadExercises, +}: ExercisesTableProps) { + const [loadingExerciseIds] = useState>(new Set()) + + const rowActionsConfig: DataTableRowActionsConfig = { + schema: zExercisePublic, + menuItems: (row) => { + if (row.user_id === null) return [] + + const isRowLoading = loadingExerciseIds.has(row.id) + return [ + { + type: 'action', + label: 'Edit', + icon: Pencil, + onSelect: () => { + // TODO implement + // eslint-disable-next-line no-console + console.log('Edit', row) + }, + disabled: isRowLoading, + }, + { + type: 'action', + className: redText, + label: 'Delete', + icon: Trash2, + onSelect: () => { + // TODO implement + // eslint-disable-next-line no-console + console.log('Delete', row) + }, + }, + ] + }, + } + + const columns: ColumnDef[] = [ + createSelectColumn(), + { + accessorKey: 'name', + header: ({ column }) => ( + + ), + enableHiding: false, + }, + { + accessorKey: 'description', + header: ({ column }) => ( + + ), + cell: ({ row }) => row.original.description ?? '—', + enableHiding: true, + }, + { + id: 'muscle_groups', + meta: { viewLabel: 'Muscle Groups' }, + accessorFn: (row) => + row.muscle_groups + .map((group) => capitalizeWords(group.name)) + .join(', '), + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const names = row.original.muscle_groups.map((group) => + capitalizeWords(group.name) + ) + return names.length ? names.join(', ') : '—' + }, + enableHiding: true, + }, + { + id: 'type', + accessorFn: (row) => (row.user_id === null ? 'system' : 'custom'), + header: ({ column }) => ( + + ), + cell: ({ row }) => ( +
+ +
+ ), + filterFn: (row, id, filterValues: string[]) => + filterValues.includes(row.getValue(id)), + enableHiding: false, + }, + { + id: 'actions', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const menuItems = rowActionsConfig.menuItems(row.original) + return menuItems.length > 0 ? ( + + ) : ( +
+ ) + }, + enableHiding: false, + }, + { + accessorKey: 'updated_at', + meta: { viewLabel: 'Updated At' }, + header: ({ column }) => ( + + ), + cell: ({ row }) => + row.original.user_id !== null ? ( +
+ {new Date(row.original.updated_at).toLocaleString()} +
+ ) : ( +
+ ), + enableHiding: true, + }, + ] + + const toolbarConfig: DataTableToolbarConfig = { + search: { + columnId: 'name', + placeholder: 'Filter by name...', + }, + filters: [ + { + columnId: 'type', + title: 'Type', + options: getTypeFilterOptions(), + }, + ], + actions: [ + { + label: 'Add Exercise', + icon: Plus, + onClick: () => { + // TODO implement + // eslint-disable-next-line no-console + console.log('Add Exercise') + }, + }, + ], + showViewOptions: true, + } + + return ( + <> + + + ) +} diff --git a/client/src/layout/AppLayout.tsx b/client/src/layout/AppLayout.tsx index 6de9386..7b873ce 100644 --- a/client/src/layout/AppLayout.tsx +++ b/client/src/layout/AppLayout.tsx @@ -32,6 +32,7 @@ export function AppLayout() {