The catch block on batch_create only logged the PB error server-side
and returned a generic 500 'batch_create_failed', which forced
operators to dig through Coolify logs to find the real cause (e.g.
mime type not allowed, missing field). Now the response includes
pb_status, pb_message, and pb_data so the actual reason is visible
in the browser's Network tab and the dashboard's error toast.
Co-Authored-By: Claude <noreply@anthropic.com>
POST /api/purchase-orders/upload no longer forwards the file to n8n
synchronously. It now validates size/MIME, writes a data_import_batches
row (batch_type='purchase_orders', status='pending', attempts=0) with the
uploaded file attached, and returns 202 { batch_id, status: "queued",
filename } immediately. n8n's 5-minute cron claims pending rows and calls
back to /api/purchase-orders/process-batch; the browser polls
/api/data-import-batches/[id]/status.
Adds route tests covering validation guards, the queued 202 contract,
the batch-create failure path, and that no synchronous fetch to n8n
happens.
Co-Authored-By: Claude <noreply@anthropic.com>
n8n POSTs extracted PO JSON + batch_id after downloading the file
from PocketBase. The route validates the payload, dedups by po_number
(idempotent), resolves the site, moves the batch's file onto a new
purchase_orders row + items, and finalizes the batch with
status=processed, processed_record_id, processed_at, original_file
cleared.
Co-Authored-By: Claude <noreply@anthropic.com>
POST /api/data-import-batches/[id]/retry resets a failed or dead batch
back to pending (attempts=0, last_error/next_retry_at cleared) so the
next cron sweep picks it up. Non-retryable states return 409 with the
current status; legacy rows with no status default to processed.
Co-Authored-By: Claude <noreply@anthropic.com>
GET /api/data-import-batches/[id]/status returns the current batch
state for the upload button's 2-second polling loop. Returns 400 on
bad id format, 404 if missing, 500 on lookup failure. Defaults
missing status to 'processed' for legacy timesheets/fuel-logs rows.
Co-Authored-By: Claude <noreply@anthropic.com>
- Purchase order upload route now returns 502 n8n_rejected when n8n
responds with a non-2xx status, instead of silently claiming success.
- Add a 10s AbortController timeout on the n8n fetch, returning 504
n8n_timeout on expiry (502 n8n_unreachable on other network errors).
- Extract ALLOWED_MIME and MAX_FILE_BYTES from upload/route.ts into
src/lib/purchase-order-schema.ts so the upcoming ingest route can
share the same validation constants.
- Updated the documentation to reflect the latest changes in the Purchase Order upload and n8n ingestion process.
- Clarified the data flow from browser to Next.js API endpoints and through to PocketBase.
- Ensured that the documentation aligns with the recent implementation and design specifications.
- Introduced new data fetching for plant and income claims, enhancing the financial overview.
- Added calculations for daily costs and integrated weekly and monthly income vs expenses roll-ups.
- Implemented plant utilization metrics to track equipment efficiency.
- Updated the SiteDetailData interface and SiteDetailView component to accommodate new data structures and display improvements.
- Updated the Dashboard page to improve card styling with a warm-amber glow strip and adjusted icon presentation for better visual appeal.
- Expanded the Sidebar navigation by adding new entries for "Sites," "Workforce," "Plant & Equipment," "Fleet & Fuel," and "Purchasing," each with corresponding icons and descriptions to enhance user navigation and access to key features.
- Added `pocketbase` as a dependency in `package.json` and `package-lock.json`.
- Updated the `dev` script in `package.json` to run on port 6004 instead of 3000.
- Removed unused API routes: `src/app/api/health/route.ts`, `src/app/api/mcp/route.ts`, `src/app/api/upload/route.ts`, and their associated logic to streamline the codebase.
- Cleaned up `package-lock.json` by removing unnecessary peer dependencies and ensuring consistency in the dependency tree.
Data wired from the site cost sheet, fuel log, wage schedule and two
PO PDFs the user shared:
- src/lib/types.ts VehicleType expanded (Roller, TLB,
Pickup, Water Truck, Compressor,
Mixer) so plant types map cleanly to
the source data. Vehicle gains
optional kmPerL and expectedLPerHour
fields for the mixed L/h + km/L
allowance model.
- src/lib/mock-data.ts SITES_DATA[0] = The Hills ext 12,
contract value R24,988,663, contract
code C26/09, 8-month duration.
SITES, SUPPLIERS updated.
VEHICLES populated with 47 entries
covering rollers, TLBs, excavators,
graders, loaders, tipper trucks,
water trucks, pickups, mixers and
auxiliary equipment (compressors,
crushing machine).
EMPLOYEES populated with 15 permanent
+ 53 casuals, all at the project's
45h/week standard.
PLANT_HIRE_RATES populated with 32
entries matching the cost sheet
(R185–R680/h depending on equipment).
PG_EXPENSES populated with all 6 P&G
monthly lines (container, security,
salaries, overheads, insurance,
site facilities = R567,403.50/m).
ORDERS populated with MEGCPO9199
(PPE, Delivered) and MEGCPO9165
(aggregates, Partial).
FUEL_READINGS starts empty — drop
CSV-imported rows in once the depot
system is wired.
- src/lib/uploads.ts UPLOAD_HISTORY seeded with the GRN
attached to MEGCPO9199.
- src/lib/burn.ts New helper. burnBreakdown() joins
plant hire rates with the vehicles
list, multiplies each hire rate by
the standard 45h/week, adds weekly
P&G share, and surfaces both weekly
and monthly totals.
- src/lib/sites.ts summarizeSite + summarizeAllSites
now take PlantHireRate[] and
PgExpense[] so the site summary
exposes weeklyBurn / monthlyBurn /
weeklyPlantHire / monthlyPg etc.
- src/app/dashboard/page.tsx New 'Burn rate — weekly vs monthly'
panel. Two side-by-side cards show
weekly + monthly totals with plant
hire vs P&G breakdown, plus a
'Cost breakdown' card on the right.
- src/app/dashboard/sites/* Pass the new hireRates + pgExpenses
arrays through to the summary helpers.
Notes:
- The 'Spent to date' KPI on the Sites list / detail now sums PO
value (since there are no fuel readings yet). Once depot data is
ingested, it will pick up the actual R25.65/L burn automatically.
- Tank capacities + odometer start values are sensible estimates
for the equipment types — adjust per-unit when you have real
fleet specs.
- Weekly hours per employee default to 45h so the workforce
trends look populated; replace with timesheet imports as soon as
those land.
Wipe all seeded mock data so the user can drop in real numbers:
- src/lib/mock-data.ts SITES_DATA, SITES, SUPPLIERS, VEHICLES,
EMPLOYEES, ORDERS, FUEL_READINGS → []
Type definitions and helper functions
preserved; the FUEL_READINGS generator is
preserved as a commented block ready to
re-enable.
- src/lib/uploads.ts UPLOAD_HISTORY → []. Acceptance rules
(mime / extensions / size limits per kind)
kept so the UI still shows format hints.
While verifying empty states, caught a divide-by-zero on the Sites
list page: 'Workforce this week' KPI rendered as 'NaN% of total
contract value' when there are zero sites. Guard the divide.
All pages now render in their empty-state UI — KPIs show 0, charts
draw empty axes, tables say 'No X match the current filters', upload
history cards say 'No uploads yet'. Typecheck clean, all routes
return 200, no runtime errors.
The previous commit used http:// for the Umami script. Any browser
loading the dashboard over HTTPS would silently drop the HTTP
request as mixed content, so the tracker would never fire in
production. Switch to https:// — the SSLip.io subdomain is
already served over TLS.
Inject the Umami tracking script into the root <head> via Next.js's
Script component with the afterInteractive strategy so it loads after
the page is interactive and never blocks first paint.
- src: http://umami-vmyngbmelll45wokwr270wbx.169.239.180.130.sslip.io/script.js
- website-id: 7655a10d-78f7-4391-a0b5-1f8449d4598a
The script is self-hosted at the Umami instance URL and does not set
any cookies of its own; it reads the visitor IP / User-Agent directly
from the request for the standard, privacy-friendly page-view events.
Two small cleanup changes:
1. Drop the 'CSV uploads' card at the bottom of the Fuel page.
The Template / Upload CSV buttons in the topbar still let users
ingest readings, but the recent-uploads mini-list was redundant —
the Uploads page already shows every upload. The fuelCsvTemplate
import is still in use by the topbar download button.
Also clean up the now-unused uploads/setUploads state, the
FileSpreadsheet icon, and the unused CardDescription / Badge /
FileSpreadsheet references in the bottom block.
2. Plant a guaranteed critical anomaly on the most recent working
day so the Anomaly log's first page and the dashboard's 'Needs
attention' rail show one for demo purposes. The previous random
spike day often landed too far back to be visible without
paginating. Now MI-TT-07 (one of the tipper trucks) always has
a 2.4x-2.8x theft spike on the latest day, while the other
eight vehicles still get a random earlier spike for variety.
3. Sort the Anomaly log by date desc, then by severity desc
(critical → alert → watch → ok) so actionable entries surface
first within each day. The dashboard's 'Needs attention' rail
also benefits from the more recent critical sitting at the top.
4. Restore the Uploads nav item in the sidebar that was removed
in the previous commit by mistake.
When someone shares a link to the dashboard in WhatsApp, Facebook,
LinkedIn, Discord, etc., the platform now shows a custom preview
instead of a plain blank card.
- public/og-image.png 1200x630 PNG with the brand mark, the
MEGA INFRASTRUCTURE wordmark, and a
tagline over a dark gradient with the
brand accent bars.
- public/fonts/ Inter ExtraBold (woff) used by the OG image
generator so the rendered text is identical
on every machine.
- scripts/build-og-image.mjs
Generates the image from an SVG template
using sharp. The font is loaded from the
local file (or Google Fonts CDN as fallback)
and embedded as base64 inside the SVG so
the renderer doesn't depend on system
fonts.
- src/app/layout.tsx Adds the openGraph + twitter metadata
pointing at /og-image.png so the preview
appears in link-unfurl contexts.
- package.json 'npm run og-image' runs the generator;
'npm run build' chains it via prebuild so
the image is regenerated on every CI build.
New /dashboard/uploads page with a 2x2 grid of drop zones, one per
ingest source:
- Orders (orange) — CSV / XLS / XLSX, max 10 MB
- Delivery Notes (teal) — PDF / images, max 20 MB
- Fuel Logs (yellow-green) — CSV / XLS / XLSX, max 5 MB
- Time Sheets (green) — CSV / XLS / XLSX, max 5 MB
Each block has:
- A coloured left stripe + icon disc in the accent colour
- Title, format-count badge, and a long description of what to upload
- File-format chips and a max-size hint
- A drag-and-drop drop zone that also accepts files via click
- A primary 'Upload X' button in the accent colour
- A 'Recent' list showing the last 4 uploads with status badges
When files are dropped or picked, the client POSTs to
/api/upload with a 'type' field. The endpoint validates MIME and size
against the kind-specific rules in src/lib/uploads.ts and returns a
JSON descriptor with accepted/rejected entries. New entries are
prepended to the page's recent-uploads list in real time, and a
bottom-right toast shows accepted/rejected counts.
The sidebar gets a new 'Uploads' nav item with the upload icon.
Files:
- src/lib/types.ts type definitions
- src/lib/uploads.ts acceptance rules + mock history
- src/app/api/upload/route.ts type-aware upload handler
- src/app/dashboard/uploads/page.tsx the new page
- src/components/layout/sidebar.tsx Uploads nav entry