Commit graph

840 commits

Author SHA1 Message Date
DEls
0fff2d1f2a feat(api): add /purchase-orders/process-batch (the new callback)
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>
2026-08-19 19:40:45 +02:00
DEls
c39124c637 feat(api): add data-import-batches manual retry endpoint
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>
2026-08-19 19:36:29 +02:00
DEls
c12c66a20a feat(api): add data-import-batches status polling endpoint
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>
2026-08-19 19:32:33 +02:00
DEls
52efc53e55 refactor(batch): drop unused status field, expand test edges
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:29:35 +02:00
DEls
3f15c6ace7 refactor(batch): import BatchStatus from pb-types
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:24:57 +02:00
DEls
fce0719b62 feat(batch): add state-machine helpers (TDD)
TDD: tests first, then the pure helpers nextBatchState and nextRetryAt.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:22:45 +02:00
DEls
32dd1b2842 feat(types): add batch async-queue fields to pb-types
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:20:32 +02:00
DEls
c534572590 feat(migration): add async-queue fields to data_import_batches
Extends the data_import_batches collection for the PO/DN async-import
queue redesign (see spec docs/superpowers/specs/2026-08-19-po-import-queue-redesign.md
and plan docs/superpowers/plans/2026-08-19-po-import-queue-implementation.md).

Adds 8 fields:
  content_type, status (select: pending|processing|processed|failed|dead),
  attempts, last_error, last_attempt_at, next_retry_at, processed_at,
  processed_record_id

Extends the batch_type enum to include purchase_orders and delivery_notes.

Adds index idx_batches_pending on (status, batch_type, next_retry_at) for
the cron query.

Idempotent: safe to re-run.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 19:19:17 +02:00
DEls
109a6938da Enhance PO upload error handling and validation
- Implement improved error handling for the purchase order upload process, ensuring that failures from n8n are properly surfaced to the user.
- Introduce a timeout mechanism for the n8n fetch request to handle potential delays more effectively.
- Refine the validation logic for the purchase order payload to ensure stricter adherence to expected formats and values.
2026-08-19 15:03:48 +02:00
DEls
8f37f3467f Spec: clarify PO upload dialog does not auto-close
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 15:03:48 +02:00
DEls
a6aa3bdb5f Drop dead try/catch around Buffer.from(base64) in /ingest route
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 15:03:48 +02:00
DEls
5abc96116f Import PO MIME/size constants from shared schema module
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 15:03:48 +02:00
DEls
e7f8799c45 Mount UploadPurchaseOrderButton on /dashboard/purchasing 2026-08-19 15:03:47 +02:00
DEls
0b873ae308 Add UploadPurchaseOrderButton component 2026-08-19 15:03:47 +02:00
DEls
38844b29d4 Escape backslashes in PO duplicate-check filter for consistency with matchSite
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 15:03:47 +02:00
DEls
a295d9ea18 Add /api/purchase-orders/ingest route (n8n callback, writes to PB) 2026-08-19 15:03:47 +02:00
DEls
2be0c8de8b Surface n8n failures + add upload timeout + dedupe MIME/size constants
- 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.
2026-08-19 15:03:47 +02:00
DEls
508e78f72b Add /api/purchase-orders/upload route (forwards file to n8n) 2026-08-19 15:03:47 +02:00
DEls
75770073a6 Tighten PO payload validator (disc/vat range, raw_ocr shape, PB filter escape)
- Validate disc_percent and vat_percent are finite numbers in [0, 100]
- Reject raw_ocr arrays (was passing typeof === 'object' check)
- Escape backslashes before quotes in matchSite's PocketBase filter

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 15:03:47 +02:00
DEls
108b17b556 Refactor Purchase Order upload flow documentation
- 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.
2026-08-19 02:25:11 +02:00
DEls
716fa45bf6 Add Vitest + n8n env var documentation 2026-08-19 02:21:36 +02:00
DEls
24fc85d911 Plan: Purchase Order upload + n8n ingestion implementation
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 02:16:22 +02:00
DEls
14b63e50ab Design: Purchase Order upload + n8n ingestion
Adds design spec and n8n handoff doc for the new PO upload flow:
browser -> Next.js /api/purchase-orders/upload -> n8n OCR ->
Next.js /api/purchase-orders/ingest -> PocketBase via pbAdmin().

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-19 02:10:58 +02:00
DEls
fb1c922fbf Enhance Site Detail Page with Financial Roll-ups and Plant Utilization
- 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.
2026-08-19 02:09:13 +02:00
DEls
20a16a27f8 Enhance Dashboard and Sidebar Navigation
- 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.
2026-08-19 01:54:16 +02:00
DEls
c5b1acfc7f Update package dependencies and remove unused API routes
- 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.
2026-08-19 00:14:47 +02:00
DEls
8aca2aed20 Clear all placeholder data while PocketBase is sorted out
The previous commit dropped real data for The Hills ext 12
(sites, vehicles, employees, orders, plant hire rates, P&G
expenses) so the dashboard had something to render during design
iteration. With PB about to be wired, that data now belongs in
PocketBase rather than in the source tree.

This commit empties the data layer back to zero so the dashboard
falls into its empty-state UI on every page:

  - src/lib/mock-data.ts    SITES_DATA, SITES, SUPPLIERS, VEHICLES,
                            EMPLOYEES, ORDERS, FUEL_READINGS,
                            PLANT_HIRE_RATES, PG_EXPENSES → []
                            Header comment updated to point future
                            contributors at lib/pb.ts as the next
                            step.
  - src/lib/uploads.ts      UPLOAD_HISTORY → []. Acceptance rules
                            (mime / extensions / size limits) kept so
                            the Uploads page still shows format hints.

Type definitions, helper functions and date utilities are untouched
so all routes continue to render (HTTP 200 across the board). The
sidebar, dashboard, fuel, orders, employees, sites and uploads pages
all gracefully render their empty states.

When PocketBase is wired:
  1. Install 'pocketbase' (npm)
  2. Build src/lib/pb.ts with admin-authed server client + public
     client
  3. Replace the imports in pages like
        import { SITES_DATA, VEHICLES, ... } from '@/lib/mock-data';
     with calls like
        import { getServerPB } from '@/lib/pb';
        const records = await getServerPB().collection('sites').getList();
  4. Keep the empty exports here as a final fallback so the dashboard
     still works if PB is ever unreachable.
2026-08-18 13:49:28 +02:00
DEls
97cb5a48b5 Sidebar: switch breakpoint from xl (1280px) to lg (1024px)
The sidebar was hidden until 1280px wide, which meant laptops and
small desktops between 1024-1280px (incl. iPads in landscape, many
Windows laptops, Chromebooks) saw the hamburger instead of the
persistent sidebar. That felt wrong on what was clearly a desktop.

Dropping to 'lg' (1024px):
- Phone portrait (< 768 px)  → hamburger ✅
- Tablet portrait (768-1024 px) → hamburger ✅
- iPad landscape / laptop (1024 px+) → sidebar ✅
- 932 px iPhone landscape cap is below 1024 px, so the hamburger
  still kicks in for all landscape phones.

Changed 4 occurrences: 3x 'xl:hidden' → 'lg:hidden', 1x
'hidden xl:block' → 'hidden lg:block' on the <aside> element.
2026-08-18 13:31:31 +02:00
DEls
3e4fee2707 Document PocketBase env vars in .env.local.example
Adds the four variables the PocketBase integration will use:

  - NEXT_PUBLIC_POCKETBASE_URL    — client-safe PB URL (no
                                     NEXT_PUBLIC_ on the auth values!)
  - POCKETBASE_ADMIN_EMAIL         — server-only admin login
  - POCKETBASE_ADMIN_PASSWORD      — server-only admin password
  - POCKETBASE_ADMIN_TOKEN         — optional pre-issued token

The real .env.local stays out of git (.env.local is gitignored);
developers copy this file to .env.local and fill in their credentials.
2026-08-18 13:06:58 +02:00
DEls
fa5611cb38 Drop in real data for The Hills ext 12 (C26/09) + Burn panel
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.
2026-08-18 12:35:34 +02:00
DEls
c09094c1dc Clear placeholder data + fix divide-by-zero on Sites page
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.
2026-08-18 12:10:44 +02:00
DEls
d33eb01d5b Fix Umami script URL: use HTTPS to avoid mixed-content blocking
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.
2026-08-10 11:35:59 +02:00
DEls
54963a9d0d Add Umami analytics tracking
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.
2026-08-10 11:29:36 +02:00
DEls
44ad91daf9 Fix Coolify build: include scripts/ in Docker context
The previous .dockerignore excluded the scripts/ directory to keep
the runtime image lean, but build-og-image.mjs is invoked as part
of 'npm run build' (which is 'npm run og-image && next build'). Without
the script in the build context, the Docker build fails with:

  Error: Cannot find module '/app/scripts/build-og-image.mjs'

The scripts/ directory is needed at build time only; the final
runner image doesn't execute any of these files, so including them
in the build context is harmless. The script is small (~4.6 KB)
and only runs during 'npm run build' in the builder stage.
2026-08-09 20:30:29 +02:00
DEls
c6ac2f06b8 Remove Fuel CSV uploads card + add demo critical anomaly
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.
2026-08-09 20:21:54 +02:00
DEls
4b03125002 Add Open Graph image + social link preview metadata
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.
2026-08-09 20:01:13 +02:00
DEls
3506640e4a Add Uploads page with 4 colored drop zones
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
2026-08-09 19:30:00 +02:00
SJCoetz
df459de0f5 Remove Active Sites card from sidebar
The card showed hardcoded '5 projects · 3 flagged · 2 on schedule'
stats that were never updated from the mock data and duplicated
information in the Sites module. Removing it gives the sidebar a
cleaner Logo → Nav → Lock structure and means the Lock screen button
is visible without scrolling on standard heights.

Also align the mobile drawer's close-button breakpoint from lg to xl
to match the rest of the sidebar's breakpoint logic.
2026-08-09 03:02:59 +02:00
SJCoetz
7d2684f321 Fix Coolify build: skip postinstall in deps stage
The postinstall hook runs scripts/patch-next-dev-blocker.cjs, but at
that point in the Docker build only package.json and package-lock.json
have been copied — the scripts/ directory isn't there yet, so the
hook fails with MODULE_NOT_FOUND.

The patch is dev-only (it mutates Next 16's dev-only cross-site
guard, which is never loaded in production), so it's safe to skip
during the Docker build. Two changes:

1. Dockerfile: use 'npm ci --ignore-scripts' in the deps stage so
   the postinstall hook doesn't run before the scripts/ directory
   is available.
2. scripts/patch-next-dev-blocker.cjs: bail out cleanly with exit 0
   when node_modules/next is missing, so the script is also safe to
   run in any intermediate state.
2026-08-09 02:29:48 +02:00
SJCoetz
eeea5b7592 Initial commit: MEGA INFRASTRUCTURE dashboard + Coolify Dockerfile 2026-08-09 01:37:40 +02:00