diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1a64262..02e1479 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -96,7 +96,9 @@ "mcp__playwright__browser_close", "Bash(wc:*)", "mcp__brave-search__brave_web_search", - "mcp__firecrawl__firecrawl_search" + "mcp__firecrawl__firecrawl_search", + "Bash(ssh:*)", + "Bash(git checkout:*)" ], "deny": [] } diff --git a/backend/eslint.config.js b/backend/eslint.config.js new file mode 100644 index 0000000..72e5ee2 --- /dev/null +++ b/backend/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist', 'node_modules'] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.ts'], + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + args: 'after-used' + }], + '@typescript-eslint/no-explicit-any': 'warn', + }, + }, +); diff --git a/backend/package.json b/backend/package.json index e41670f..3551ba0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,26 +13,26 @@ "migrate:all": "ts-node src/_system/migrations/run-all.ts", "migrate:feature": "ts-node src/_system/migrations/run-feature.ts", "schema:generate": "ts-node src/_system/schema/generate.ts", - "lint": "eslint src --ext .ts" + "lint": "eslint src" }, "dependencies": { - "pg": "^8.11.3", - "ioredis": "^5.3.2", + "pg": "^8.13.1", + "ioredis": "^5.4.2", "minio": "^7.1.3", - "@fastify/multipart": "^8.1.0", - "axios": "^1.6.2", + "@fastify/multipart": "^9.0.1", + "axios": "^1.7.9", "opossum": "^8.0.0", - "winston": "^3.11.0", - "zod": "^3.22.4", + "winston": "^3.17.0", + "zod": "^3.24.1", "js-yaml": "^4.1.0", - "fastify": "^4.24.3", - "@fastify/cors": "^9.0.1", - "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^8.0.0", - "@fastify/type-provider-typebox": "^4.0.0", - "@sinclair/typebox": "^0.31.28", - "fastify-plugin": "^4.5.1", - "@fastify/autoload": "^5.8.0", + "fastify": "^5.2.0", + "@fastify/cors": "^10.0.1", + "@fastify/helmet": "^12.0.1", + "@fastify/jwt": "^9.0.1", + "@fastify/type-provider-typebox": "^5.0.0", + "@sinclair/typebox": "^0.34.0", + "fastify-plugin": "^5.0.1", + "@fastify/autoload": "^6.0.1", "get-jwks": "^9.0.0", "file-type": "^16.5.4" }, @@ -40,17 +40,17 @@ "@types/node": "^20.10.0", "@types/pg": "^8.10.9", "@types/js-yaml": "^4.0.9", - "typescript": "^5.6.3", + "typescript": "^5.7.2", "ts-node": "^10.9.1", - "nodemon": "^3.0.1", + "nodemon": "^3.1.9", "jest": "^29.7.0", "@types/jest": "^29.5.10", "ts-jest": "^29.1.1", "supertest": "^6.3.3", "@types/supertest": "^2.0.16", "@types/opossum": "^8.0.0", - "eslint": "^8.54.0", - "@typescript-eslint/eslint-plugin": "^6.12.0", - "@typescript-eslint/parser": "^6.12.0" + "eslint": "^9.17.0", + "@eslint/js": "^9.17.0", + "typescript-eslint": "^8.18.1" } } diff --git a/backend/src/core/plugins/error.plugin.ts b/backend/src/core/plugins/error.plugin.ts index b014b75..d2b1e0c 100644 --- a/backend/src/core/plugins/error.plugin.ts +++ b/backend/src/core/plugins/error.plugin.ts @@ -2,12 +2,12 @@ * @ai-summary Fastify global error handling plugin * @ai-context Handles uncaught errors with structured logging */ -import { FastifyPluginAsync } from 'fastify'; +import { FastifyPluginAsync, FastifyError } from 'fastify'; import fp from 'fastify-plugin'; import { logger } from '../logging/logger'; const errorPlugin: FastifyPluginAsync = async (fastify) => { - fastify.setErrorHandler((error, request, reply) => { + fastify.setErrorHandler((error: FastifyError, request, reply) => { logger.error('Unhandled error', { error: error.message, stack: error.stack, diff --git a/data/vehicle-etl/etl_generate_sql.py b/data/vehicle-etl/etl_generate_sql.py index f08f38a..0bbed5d 100644 --- a/data/vehicle-etl/etl_generate_sql.py +++ b/data/vehicle-etl/etl_generate_sql.py @@ -43,7 +43,10 @@ def load_pairs(snapshot_path: Path) -> List[sqlite3.Row]: if not snapshot_path.exists(): raise FileNotFoundError(f"Snapshot not found: {snapshot_path}") - conn = sqlite3.connect(snapshot_path) + # Open in immutable mode to prevent any write attempts on read-only filesystems + absolute_path = snapshot_path.resolve() + uri = f"file:{absolute_path}?immutable=1" + conn = sqlite3.connect(uri, uri=True) conn.row_factory = sqlite3.Row try: cursor = conn.execute( diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index 365a4d6..6a88b1c 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -20,14 +20,16 @@ Your task is to create a plan that can be dispatched to a seprate set of AI agen *** ROLE *** -You are a senior devops systems reliablity engineer specializing modern web applications. Expert in linux, docker compose and gitlab. -Read README.md CLAUDE.md and AI-INDEX.md to understand this code repository in the context of this change. +You are a senior web deveoper specializing in nodejs, typescript along with CSS and HTML. + *** ACTION *** -- You need to create a plan for the end user to implement to make this application deployable with GitLab runners. +- You need to create a plan to upgrade the node packages to the latest versions that are compatible with each other. Some of the packages are very old and while we might not get to the very latest we need to get as close as possible for security reasons. +- Use context7 mcp to find the latest versions and compatibilities. +- Read README.md CLAUDE.md and AI-INDEX.md to understand this code repository in the context of this change. *** CONTEXT *** -- This is a docker compose app that is functioning in the local dev environment. It was developed with the plan to move to Kubernetes eventually but right now it's staying in docker compose. There is a secrets architecture that mirrors k8s that needs to be replicated in gitlab deployment into the docker compose environment. The gitlab version is 18.6.2 and is using the shell runtime on the gitlab runners. +- This is a modern web app for managing a vehicle fleet. It has both a desktop and mobile versions of the site that both need to maintain feature parity. The UX will be different between mobile and desktop due to resolution differences but that's it. *** EXECUTE *** - Create a plan the user can execute to make this app deployable with gitlab. Use brave, context7 and firecrawl if needed. Make no assumptions if your data does not have version 18.6 of gitlab. Save the plan to @docs/CICD-DEPLOY.md. Ultrathink. \ No newline at end of file + Create a plan that can be tasked to sub agents to explore and find dependancies between all the packages. Make sure nothing breaks. Use context7, brave search and firecrawl MCP's extensively. Make no assumptions. \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index cff744d..5af2b58 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -2,32 +2,30 @@ import js from '@eslint/js'; import globals from 'globals'; import reactHooks from 'eslint-plugin-react-hooks'; import reactRefresh from 'eslint-plugin-react-refresh'; -import tseslint from '@typescript-eslint/eslint-plugin'; -import parser from '@typescript-eslint/parser'; +import tseslint from 'typescript-eslint'; -export default [ - { ignores: ['dist'] }, +export default tseslint.config( + { ignores: ['dist', 'node_modules'] }, + js.configs.recommended, + ...tseslint.configs.recommended, { files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, - parser, }, plugins: { - '@typescript-eslint': tseslint, 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { - ...js.configs.recommended.rules, ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['warn', { + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_', ignoreRestSiblings: true, @@ -36,4 +34,4 @@ export default [ '@typescript-eslint/no-explicit-any': 'warn', }, }, -]; \ No newline at end of file +); \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 6339b8f..e5a284e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,47 +12,48 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-router-dom": "^6.8.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^6.28.1", "@auth0/auth0-react": "^2.2.3", - "axios": "^1.6.2", - "zustand": "^4.4.6", - "@tanstack/react-query": "^5.8.4", - "react-hook-form": "^7.48.2", - "@hookform/resolvers": "^3.3.2", - "zod": "^3.22.4", - "date-fns": "^2.30.0", + "axios": "^1.7.9", + "zustand": "^4.5.6", + "@tanstack/react-query": "^5.84.1", + "react-hook-form": "^7.54.2", + "@hookform/resolvers": "^3.9.1", + "zod": "^3.24.1", + "dayjs": "^1.11.13", "clsx": "^2.0.0", "react-hot-toast": "^2.4.1", "react-slick": "^0.30.2", "slick-carousel": "^1.8.1", - "framer-motion": "^11.0.0", - "@mui/material": "^5.15.0", - "@mui/x-date-pickers": "^6.19.0", - "@mui/x-data-grid": "^6.19.1", - "@emotion/react": "^11.11.4", - "@emotion/styled": "^11.11.0", - "@emotion/cache": "^11.11.0", - "@mui/icons-material": "^5.15.0" + "framer-motion": "^11.15.0", + "@mui/material": "^6.3.0", + "@mui/x-date-pickers": "^7.23.0", + "@mui/x-data-grid": "^7.23.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@emotion/cache": "^11.14.0", + "@mui/icons-material": "^6.3.0" }, "devDependencies": { - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", "@types/react-slick": "^0.23.13", - "@typescript-eslint/eslint-plugin": "^6.12.0", - "@typescript-eslint/parser": "^6.12.0", - "@vitejs/plugin-react": "^4.2.0", - "autoprefixer": "^10.4.16", - "eslint": "^8.54.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.4", - "postcss": "^8.4.32", - "tailwindcss": "^3.3.6", + "typescript-eslint": "^8.18.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "eslint": "^9.17.0", + "@eslint/js": "^9.17.0", + "globals": "^15.14.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.16", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", "terser": "^5.24.0", "@emotion/babel-plugin": "^11.11.0", - "typescript": "^5.6.3", - "vite": "^5.0.6", + "typescript": "^5.7.2", + "vite": "^5.4.11", "jest": "^29.7.0", "@types/jest": "^29.5.10", "ts-jest": "^29.1.1", diff --git a/frontend/src/features/admin/components/AuditLogDrawer.tsx b/frontend/src/features/admin/components/AuditLogDrawer.tsx index 5d4b45c..08bbcff 100644 --- a/frontend/src/features/admin/components/AuditLogDrawer.tsx +++ b/frontend/src/features/admin/components/AuditLogDrawer.tsx @@ -15,12 +15,15 @@ import { Divider, } from '@mui/material'; import { Close, ChevronLeft, ChevronRight } from '@mui/icons-material'; -import { formatDistanceToNow } from 'date-fns'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; import { useAuditLogStream } from '../hooks/useAuditLogStream'; import { AdminSkeleton } from './AdminSkeleton'; import { EmptyState } from './EmptyState'; import { ErrorState } from './ErrorState'; +dayjs.extend(relativeTime); + /** * Props for AuditLogDrawer component */ @@ -154,9 +157,7 @@ export const AuditLogDrawer: React.FC = ({ component="span" color="text.secondary" > - {formatDistanceToNow(new Date(log.createdAt), { - addSuffix: true, - })} + {dayjs(log.createdAt).fromNow()} {log.resourceType && ( = ({ }} > - Updated{' '} - {formatDistanceToNow(lastUpdated, { - addSuffix: true, - })} + Updated {dayjs(lastUpdated).fromNow()} = ({ component="span" color="text.secondary" > - {formatDistanceToNow(new Date(log.createdAt), { - addSuffix: true, - })} + {dayjs(log.createdAt).fromNow()} {log.resourceType && ( = ({ }} > - Updated{' '} - {formatDistanceToNow(lastUpdated, { - addSuffix: true, - })} + Updated {dayjs(lastUpdated).fromNow()} = ({ onSuccess, onCancel }; return ( +
@@ -207,21 +212,39 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel
- - setEffectiveDate(e.target.value)} + setEffectiveDate(newValue?.format('YYYY-MM-DD') || '')} + format="MM/DD/YYYY" + slotProps={{ + textField: { + fullWidth: true, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: 44, + }, + }, + }, + }} />
- - setExpirationDate(e.target.value)} + setExpirationDate(newValue?.format('YYYY-MM-DD') || '')} + format="MM/DD/YYYY" + slotProps={{ + textField: { + fullWidth: true, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: 44, + }, + }, + }, + }} />
@@ -282,12 +305,21 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel />
- - setRegistrationExpirationDate(e.target.value)} + setRegistrationExpirationDate(newValue?.format('YYYY-MM-DD') || '')} + format="MM/DD/YYYY" + slotProps={{ + textField: { + fullWidth: true, + sx: { + '& .MuiOutlinedInput-root': { + minHeight: 44, + }, + }, + }, + }} />
@@ -336,6 +368,7 @@ export const DocumentForm: React.FC = ({ onSuccess, onCancel
+
); }; diff --git a/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx b/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx index f373479..f385142 100644 --- a/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx +++ b/frontend/src/features/fuel-logs/components/FuelLogEditDialog.tsx @@ -16,8 +16,9 @@ import { useMediaQuery } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import dayjs from 'dayjs'; import { FuelLogResponse, UpdateFuelLogRequest, FuelType } from '../types/fuel-logs.types'; import { useFuelGrades } from '../hooks/useFuelGrades'; @@ -135,7 +136,7 @@ export const FuelLogEditDialog: React.FC = ({ return ( - + = ({ handleInputChange('dateTime', newValue?.toISOString() || '')} - format="MM/dd/yyyy hh:mm a" + format="MM/DD/YYYY hh:mm a" slotProps={{ textField: { fullWidth: true, diff --git a/frontend/src/features/fuel-logs/components/FuelLogForm.tsx b/frontend/src/features/fuel-logs/components/FuelLogForm.tsx index cf35919..f65a37c 100644 --- a/frontend/src/features/fuel-logs/components/FuelLogForm.tsx +++ b/frontend/src/features/fuel-logs/components/FuelLogForm.tsx @@ -4,8 +4,9 @@ import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { Grid, Card, CardHeader, CardContent, TextField, Box, Button, CircularProgress, ToggleButton, ToggleButtonGroup } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; +import dayjs from 'dayjs'; import { VehicleSelector } from './VehicleSelector'; import { DistanceInput } from './DistanceInput'; import { FuelTypeSelector } from './FuelTypeSelector'; @@ -143,7 +144,7 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial }, [useOdometer, setValue]); return ( - + } /> @@ -161,9 +162,9 @@ const FuelLogFormComponent: React.FC<{ onSuccess?: () => void; initial?: Partial ( field.onChange(newValue?.toISOString() || '')} - format="MM/dd/yyyy hh:mm a" + format="MM/DD/YYYY hh:mm a" slotProps={{ textField: { fullWidth: true, diff --git a/frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx b/frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx index b1bfdeb..0f59341 100644 --- a/frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceRecordEditDialog.tsx @@ -21,8 +21,9 @@ import { useMediaQuery, } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import dayjs from 'dayjs'; import { MaintenanceRecordResponse, UpdateMaintenanceRecordRequest, @@ -149,7 +150,7 @@ export const MaintenanceRecordEditDialog: React.FC + handleInputChange('date', newValue?.toISOString().split('T')[0] || '') } - format="MM/dd/yyyy" + format="MM/DD/YYYY" slotProps={{ textField: { fullWidth: true, diff --git a/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx b/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx index 62bce0a..0f536ad 100644 --- a/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx +++ b/frontend/src/features/maintenance/components/MaintenanceRecordForm.tsx @@ -25,8 +25,9 @@ import { InputAdornment, } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import dayjs from 'dayjs'; import { useMaintenanceRecords } from '../hooks/useMaintenanceRecords'; import { useVehicles } from '../../vehicles/hooks/useVehicles'; import { SubtypeCheckboxGroup } from './SubtypeCheckboxGroup'; @@ -135,7 +136,7 @@ export const MaintenanceRecordForm: React.FC = () => { } return ( - + @@ -238,11 +239,11 @@ export const MaintenanceRecordForm: React.FC = () => { render={({ field }) => ( field.onChange(newValue?.toISOString().split('T')[0] || '') } - format="MM/dd/yyyy" + format="MM/DD/YYYY" slotProps={{ textField: { fullWidth: true, diff --git a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx index 92fdcf9..2e64239 100644 --- a/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx +++ b/frontend/src/features/vehicles/mobile/VehicleDetailMobile.tsx @@ -3,7 +3,7 @@ */ import React, { useMemo, useState } from 'react'; -import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem } from '@mui/material'; +import { Box, Typography, Button, Card, CardContent, Divider, FormControl, InputLabel, Select, MenuItem, List, ListItem, ListItemButton } from '@mui/material'; import { useQueryClient } from '@tanstack/react-query'; import { Vehicle } from '../types/vehicles.types'; import { useFuelLogs } from '../../fuel-logs/hooks/useFuelLogs'; @@ -250,22 +250,24 @@ export const VehicleDetailMobile: React.FC = ({ ) : ( {filteredRecords.map(rec => ( - openEditLog(rec.id, rec.type)}> - - {/* Primary line: MPG/km-L and amount */} - - - {rec.summary || 'MPG: —'} - - - {rec.amount || '—'} + + openEditLog(rec.id, rec.type)}> + + {/* Primary line: MPG/km-L and amount */} + + + {rec.summary || 'MPG: —'} + + + {rec.amount || '—'} + + + {/* Secondary line: Grade • Date • Type */} + + {rec.secondary || `${new Date(rec.date).toLocaleDateString()} • ${rec.type}`} - {/* Secondary line: Grade • Date • Type */} - - {rec.secondary || `${new Date(rec.date).toLocaleDateString()} • ${rec.type}`} - - + ))} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9200211..58357fe 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -46,7 +46,7 @@ export default defineConfig({ 'forms': ['react-hook-form', '@hookform/resolvers', 'zod'], // Utilities - 'utils': ['date-fns', 'clsx'], + 'utils': ['dayjs', 'clsx'], // Animation and UI 'animation': ['framer-motion', 'react-hot-toast']