composer install needs full app source for post-autoload-dump scripts.
Use --no-scripts in the cached layer, run dump-autoload after full copy.
Build verified locally.
Copy composer.lock and package-lock.json before the full source so
Docker caches the expensive install steps. Only the final COPY layer
changes on code-only commits, making deploys much faster.
- No files: minimal 'No files attached' + small 'Attach file' link
- With files: badge 'N files attached' with chevron toggle (Alpine x-data)
- Click badge to expand/collapse file list on surface-1 background
- Upload is an icon-only button (arrow-up-tray) next to the badge
- Existing Livewire wire:model/wire:click backend unchanged
Livewire temp file getSize()/getMimeType() fails in container volume
environment. Store the file first, then read metadata from the stored
file on the local disk instead.
- Dashed-border upload zone with hover state and upload icon
- Hidden native file input, label triggers file picker
- Loading spinner on Attach button during upload
- Selected file name display with green highlight
- Dark mode support throughout
- Show the file picker for all editable rows, not just saved ones
- Auto-save the row when Attach is clicked if it has no id yet
- Remove the 'Save the row to attach files' restriction
Pier rebuilds the container on every commit, so the image is now the real
runtime. Bring it to parity with scripts/deploy-staging-vps.sh and make bad
builds fail loudly instead of silently drifting:
- Entrypoint mirrors the VPS runtime steps: migrate --force, config:cache and
view:cache (routes are NOT cached because the /up closure route can't be
serialized), favicon generation, and storage permission fixes.
- Run web + queue:work + schedule:work under supervisor in one container.
Previously nothing processed the database queue and no scheduler ran, so
jobs and scheduled tasks never executed.
- Declare /app/storage/app as a VOLUME so a Pier persistent volume keeps
uploaded attachments across rebuilds (they were on ephemeral disk before).
- Add a HEALTHCHECK on the public /login route so Pier can detect a boot that
never becomes healthy (/up is IP-restricted and unsuitable from loopback).
- composer install now uses --optimize-autoloader, matching the VPS deploy.
- Bake trustProxies into bootstrap/app.php and drop the fragile sed injection
that silently no-opped if the file was reformatted.
- Add .dockerignore so local .env/vendor/node_modules are never baked into
image layers and builds stay small.
- Set PHP_CLI_SERVER_WORKERS so artisan serve handles concurrent requests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qMh7msL8qSXMA9nSLTY38
The container CMD only ran 'php artisan serve', so migrations added to
the codebase (e.g. create_timesheet_attachments_table) were never applied
on the Docker-based staging host. Viewing a timesheet then failed with
'relation "timesheet_attachments" does not exist' because ViewTimesheet
calls $record->attachments()->exists() against a table that was never
created.
Run 'php artisan migrate --force' before serving, matching what the VPS
deploy script already does, and fail fast rather than serving a stale
schema.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qMh7msL8qSXMA9nSLTY38
Employees can now attach supporting files (PDFs, images, office docs)
to a project row on the Weekly hours page and view/download them when
viewing a timesheet.
- New timesheet_attachments table + TimesheetAttachment model with
automatic file cleanup on delete and on parent timesheet deletion
- Weekly hours grid: per-row upload/remove controls for editable saved
rows, with existing attachments shown as downloadable chips
- ViewTimesheet infolist: new Attachments section with download links
- Authenticated download route authorized via TimesheetAccess so only
users who can view a timesheet can fetch its files
- Uploads validated for type (pdf/images/doc/xls/csv/txt) and size (10MB)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cwd7t8ySm9Kegxxfefmm69
- Use unescaped sed to inject trustProxies(at: "*") into bootstrap/app.php
- Copy node/npm from node:22-alpine stage and run npm run build
- Remove key:generate from CMD (APP_KEY provided via env)
- Trust proxies so HTTPS URLs are generated behind Traefik
When program-manager approval is off, PM-owned submits stay pending for admin review; notify admins instead of self, align reject messaging, and keep approval history readable after user deletion.
Co-authored-by: Cursor <cursoragent@cursor.com>
Include approvers' own rows in timesheet scope, lock submit/approve/reject/revert, abort unauthorized approve, and split set-password throttles so GET refreshes are not locked out.
Co-authored-by: Cursor <cursoragent@cursor.com>
The activitylog:clean command uses ConfirmableTrait, which in production
blocks on an interactive confirm() prompt. Under cron there is no TTY, so
the prompt resolves to its default (false), the command exits 1, and
ScheduleRunCommand throws a generic Exception on every daily run.
Pass --force so the scheduled task actually runs instead of failing.
Co-Authored-By: Claude <noreply@anthropic.com>
Block submission and approval of timesheets whose week_start Monday has
not arrived yet (a future week). Drafting ahead stays allowed; only the
submit and approve gates close, since hours not yet worked cannot be
verified. The current in-progress week remains submittable.
- Timesheet::isFutureWeek() is the single source of truth
- canUserSubmitTimesheet hides the Submit action for future weeks
- submitTimesheet throws a friendly ValidationException before the 403
- canBeApprovedBy hides Approve/Reject for future weeks
- handleApprove defends with the same ValidationException
- cover with submit + approval gate tests
Co-Authored-By: Claude <noreply@anthropic.com>
The standard Alpine build bundled by Livewire/Filament evaluates directive
expressions via new Function, so the admin UI fails to initialize when
'unsafe-eval' is dropped from script-src. Restore it and drop only the
unused cdn.jsdelivr.net origin (still blocks the "load arbitrary script from
a public CDN" vector). Revert csp_enforce to false (report-only) — enforcing
the current policy can't fully stop XSS while unsafe-inline/unsafe-eval
remain, and full no-eval enforcement needs Livewire's CSP-safe build
(livewire.csp_safe=true) + nonces as a separate rollout. Update the CSP test
to the corrected policy string.
Co-authored-by-by: Claude <noreply@anthropic.com>
Add a comment to TimesheetAccess::scopeTimesheetsForUser noting that the
approvalLogs clause (approvers keep read access to a user's hours on a
project after acting on any of that user's timesheets) is intended, not a
leak to be tightened to the single approved row.
Co-authored-by: Claude <noreply@anthropic.com>
- CSP: drop 'unsafe-eval' and the unused cdn.jsdelivr.net script origin;
enforce by default (SECURITY_CSP_ENFORCE, env-overridable for rollout).
Report-Only stays on so violations surface during transition.
- HSTS: emit when APP_URL is https, not only when the current leg is TLS,
so it survives a TLS-terminating proxy; add preload.
- Cache headers: guard no-store on auth() + GET + text/html instead of the
dead admin/* prefix (panel is mounted at root); uniform X-Frame-Options: DENY.
- Activation email: stop mailing the cleartext temporary password; the
set-password token link is the sole onboarding path.
- Uptime heartbeat: read the token from the X-Uptime-Token header instead
of the query string (leaks via logs/referer); update docs.
- Project aggregates: select only the hour columns to bound memory.
- Password hashing: standardize on the User model's hashed cast; remove
redundant Hash::make in the reset action and the user form.
- Tests: add login-throttle test (5 failed -> 6th throttled); update
CSP/header and uptime/activation tests to the new behavior.
Co-authored-by: Claude <noreply@anthropic.com>
Soft-delete projects with restore support, prevent timesheet cascade loss, and redesign the team members and contributions panels with matched-height scrollable cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
One-time portable (SQLite + Postgres) migration relabeling the audit log
badge column for existing rows; new entries already write the acting role.
Co-Authored-By: Claude <noreply@anthropic.com>
The Audit Log page's "Log" badge always read "admin" regardless of who
acted, because both manual AuditLogger::log and the LogsAuditableChanges
trait hardcoded log_name='admin'. Default to Auth::user()'s role (or
'system' for console/jobs) in both, and the backfill command. Update the
status-change test to expect the employee's role.
Co-Authored-By: Claude <noreply@anthropic.com>
afterStateUpdated callbacks received Filament\Schemas\Components\Utilities\Set,
not the non-existent Filament\Forms\Set — selecting a template/reuse preset
threw a TypeError. Use the correct class.
Co-Authored-By: Claude <noreply@anthropic.com>
Add BroadcastTemplateSeeder with Welcome + Password reset reminder
templates, wired into DatabaseSeeder. Make the Broadcast email modal
stick its header and trim the body textarea so the title stays visible.
Co-Authored-By: Claude <noreply@anthropic.com>
Reusable subject/body pairs managed via a Broadcast templates admin resource
(Filament CRUD, gated to admins and project_admins). The Broadcast email action
gains a "Use a template" picker that fills the subject and body, so common
messages don't need retyping. Complements the existing "Reuse a previous
broadcast" history picker.
Co-Authored-By: Claude <noreply@anthropic.com>
Each broadcast now stores sender, subject, body, and recipient count in a
broadcast_emails table. The Broadcast email action gains a "Reuse a previous
broadcast" dropdown that auto-fills subject and body from a past broadcast, so
admins can re-blast a message without retyping it.
Co-Authored-By: Claude <noreply@anthropic.com>
The email is fixed by the activation token; making it read-only stops users
from changing it and breaking the token match.
Co-Authored-By: Claude <noreply@anthropic.com>
Use Filament\Facades\Filament (the app's convention) instead of the
non-existent Illuminate\Support\Facades\Filament, which threw in the
queue worker and silently failed every activation email. Also correct the
Notification facade import in UserNotifier (should be
Illuminate\Support\Facades\Notification) so the non-queued send path works.
Co-Authored-By: Claude <noreply@anthropic.com>
Email new users their login URL and typed password plus a set-your-own-password
link on creation; add a broadcast action to message all visible users with a
per-user activation link, and a per-row resend action. Adds a public
set-password route backed by the Laravel password broker.
Co-Authored-By: Claude <noreply@anthropic.com>
Add columnSpanFull() to the two-column Flex container so the timesheet view uses the full available width instead of leaving unused horizontal space.
Co-authored-by: Cursor <cursoragent@cursor.com>
Place details, approval history, and notes in the left column and the weekly breakdown in the right column so the view page uses vertical space more effectively.
Co-authored-by: Cursor <cursoragent@cursor.com>
Refresh the lockfile metadata so it matches composer.json after raising the declared runtime to PHP 8.4, avoiding install warnings on staging and production.
Co-authored-by: Cursor <cursoragent@cursor.com>
Scope dashboard pending counts to each approver's assigned projects, harden overtime validation against incomplete weekly arrays, remove legacy PD backfill labels, and declare the PHP 8.4 runtime required by the app.
Co-authored-by: Cursor <cursoragent@cursor.com>
Employees see labeled PM and Program Manager contacts on assigned projects; PMs and program managers can pick employees when staffing projects; weekly-hours print/PDF exports include overtime totals; reports summary cards align consistently.
Co-authored-by: Cursor <cursoragent@cursor.com>
Windows tar archives can leave storage read-only; restore writable storage paths and use route:clear instead of route:cache.
Co-authored-by: Cursor <cursoragent@cursor.com>
Shows per-project overtime rows with live totals on Weekly Hours, and matches the day-strip layout and breakpoints so Friday OT lines up with the planner dates.
Co-authored-by: Cursor <cursoragent@cursor.com>
Return updated buckets from TimesheetSummaryBuilder instead of passing by reference, and filter member options to users not already selected in other repeater rows.
Co-authored-by: Cursor <cursoragent@cursor.com>