Commit graph

636 commits

Author SHA1 Message Date
Alexandre Brandizzi
ed5c75223e
Merge pull request #188 from Sea-Haven-Industries/feat/ab/sh-295-sla-alerts
feat(notifications): SEV response-window alerts and breach acknowledgement
2026-09-25 14:31:53 +00:00
Alexandre Brandizzi
14c8e46dd0 feat(notifications): SEV response-window alerts and breach acknowledgement
Reactive/Emergency work orders with a SEV 1-5 level are timed from their
creation against the SEV Respond deadline (2/4/8/24/72 hours, one backend
table). From 50% they are at risk: a dismissable High row in the "SLA at
Risk" section and an entry in the feed's slaAtRisk set with the server
clock (start, deadline, percent) for the banner and toast. From 100% they
are a Critical acknowledge row that only acknowledging removes.

POST /api/notifications/sla/{id}/acknowledge records who and when as a
work-order audit entry ("SLA breach acknowledged by <name>"), scoped to the
caller's feed audience: 404 outside it, 409 before the deadline, 204 when
recorded or already recorded. A later severity change is a new breach.
2026-09-25 11:16:21 -03:00
Alexandre Brandizzi
06eae2fb02
Merge pull request #175 from Sea-Haven-Industries/feat/ab/sh-392-dashboard-unassigned
Some checks are pending
Backend CI / Build and test (push) Waiting to run
Backend CI / architecture (push) Waiting to run
Backend CI / review (push) Waiting to run
Backend CI / ci-complete (push) Blocked by required conditions
SH-392: count open unassigned work orders on the Dashboard
2026-09-25 06:46:01 +00:00
Alexandre Brandizzi
702069f09c test(dashboard): pin drill-down rows for legacy open work orders
The parity test now also asserts which rows the drill-down lists: legacy
open rows (status in Status or in LegacyStatus, or none) are listed and
legacy closed, cancelled or assigned rows are not. Drops ticket keys
from comments.
2026-09-25 03:38:31 -03:00
Alexandre Brandizzi
d6c5357fab fix(dashboard): count unassigned legacy rows with no lifecycle status (SH-392)
The legacy create paths (WorkOrderDTOs, SyncService, the Blazor
WorkorderService) still write Status without LifecycleStatus. The board
status filter only matched LifecycleStatus. So an open unassigned row of
that kind was left out of the Unassigned tile and its drill-down list,
even though the Open tile in the same response counted it.

ApplyStatusFilter now reads a row with no LifecycleStatus by its legacy
status (LegacyStatus ?? Status), using the Phase0 backfill rules: known
text maps as LifecycleStatusMapper does, and anything else, blank
included, counts as Incomplete. The tile and /board/search share the
predicate, so the count still matches the list it opens.
2026-09-25 03:34:24 -03:00
Alexandre Brandizzi
c49a98db18
Merge branch 'main' into feat/ab/sh-392-dashboard-unassigned 2026-09-25 03:27:33 -03:00
Alexandre Brandizzi
2c8ffaf10e
Merge pull request #173 from Sea-Haven-Industries/fix/ab/sh-383-media-contract
Some checks are pending
Backend CI / Build and test (push) Waiting to run
Backend CI / architecture (push) Waiting to run
Backend CI / review (push) Waiting to run
Backend CI / ci-complete (push) Blocked by required conditions
fix(media): lift the 1 MB proxy body cap and apply the SH-116 media contract
2026-09-25 06:02:21 +00:00
Alexandre Brandizzi
c1ed5dc98d
Merge pull request #181 from Sea-Haven-Industries/fix/ab/sh-400-vendor-readonly-uplifts
Keep work-order uplift requests read-only in the Vendor Portal
2026-09-25 05:58:38 +00:00
Alexandre Brandizzi
207bf59208 fix(media): name HEIC in the unsupported-type message and keep ticket keys out of comments
The rejection message now lists every type the media allowlist accepts, and a
test fails if the message and the allowlist drift apart.
2026-09-25 02:58:15 -03:00
Alexandre Brandizzi
203fc92e4a
Merge pull request #172 from Sea-Haven-Industries/fix/ab/sh-391-adv-search-date-range
fix(board-search): make the Advanced Filters date range narrow results (SH-391)
2026-09-25 05:53:42 +00:00
Alexandre Brandizzi
e02f9774dc Keep work-order uplift requests read-only in the Vendor Portal
A vendor could withdraw (or cancel) an uplift a dispatcher raised from the work
order. Withdraw and its cancel alias now refuse requests with createdby set,
using the portal's not-found response, and the portal read model reports
RaisedByVendor so the portal can hide Revise and Withdraw on those requests.
2026-09-25 02:52:03 -03:00
Alexandre Brandizzi
3b69b9fbcc
Merge pull request #178 from Sea-Haven-Industries/fix/ab/sh-398-pending-exposure
fix(uplifts): one per-path uplift amount for queue, approval and exposure
2026-09-25 05:46:17 +00:00
Alexandre Brandizzi
85f2e709c9 test(uplifts): board summary and notification delta use the per-path amount
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 02:36:17 -03:00
Alexandre Brandizzi
d33ba34db9 fix(vendor-portal): vendors revise only uplift requests they raised
A work-order request stores the requested increase, not a total. Letting
the vendor revise one after changes were requested rewrote RequestedNTE
as a total while it still read as a work-order request, corrupting its
amount and the NTE it would be approved to. Revise now answers not-found
for any request the vendor did not raise and leaves the row untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 02:35:06 -03:00
Alexandre Brandizzi
eb2b442775 fix(uplifts): one per-path uplift amount for queue, approval and exposure
Work-order requests store the requested increase in RequestedNTE; vendor
portal requests store the requested NTE total. The queue Delta, the
pending and approved exposure totals, the work-order uplift list, the
board summary and the notification Delta now all read one definition
(UpliftAmount) that honours both meanings and translates to SQL.

Approving a work-order request now adds its increase to the dispatch NTE
instead of replacing the NTE with the increase; vendor requests still end
at their requested total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 02:32:58 -03:00
Alexandre Brandizzi
f40ad7c1ef fix(uplifts): pending exposure header sums the row deltas
The approvals header summed the whole RequestedNTE for work-order-path
requests, while each Pending row shows RequestedNTE - CurrentNTE. When a
work order already had an NTE the header overstated exposure by that NTE.

The header now sums the same Delta the rows display, over the same rows
the Pending tab lists (non-deleted request on a non-deleted dispatch).
The unused duplicate aggregate is removed so one definition remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 02:10:41 -03:00
Alexandre Brandizzi
67cd9c589b fix(board-search): keep undated rows for clients that omit includeDateless (SH-391)
The strict date range made undated open work orders disappear for every
client that predates the includeDateless flag. The frontend on main sends
the all-weeks window (2000-01-01..2099-12-31) for "no range", the
1970-01-01..2099-12-31 window for the pinned Unassigned queue, and no date
input at all for the WO# duplicate lookup. None of those send the flag, so
deploying this backend before the frontend would have dropped undated WOs
from all three flows.

includeDateless is now optional. An explicit value still wins. When it is
omitted, a request with no date input or with an all-weeks Custom window
keeps its open undated rows, as before SH-391. Any other range stays
strict. The backend and frontend can therefore deploy in either order.
2026-09-25 01:41:49 -03:00
Alexandre Brandizzi
0d8f32d148 fix(media): check the file type before the size cap on media upload
AddMedia sized a file before validating its type, and ValidateSize's Unknown
arm returned the video message, so a 150 MB .exe sent as
application/octet-stream was rejected as FileTooLarge with "Videos must be
100 MB or smaller." EnsureAllowed now runs first, so an unsupported file always
reports UnsupportedMediaType, and the Unknown arm uses a type-neutral message.
The oversize controller tests now use real file headers so they pass the type
check before reaching the size cap.
2026-09-24 23:47:18 -03:00
Alexandre Brandizzi
50e5553e57
Merge pull request #177 from Sea-Haven-Industries/fix/ab/sh-397-uplift-decision-audit
Some checks are pending
Backend CI / Build and test (push) Waiting to run
Backend CI / architecture (push) Waiting to run
Backend CI / review (push) Waiting to run
Backend CI / ci-complete (push) Blocked by required conditions
fix(uplifts): let admins decide uplifts whose dispatch has no work order
2026-09-25 02:45:18 +00:00
Alexandre Brandizzi
b2b9fc3588 test(uplifts): cover escalation audit on a dispatch without an owning work order 2026-09-24 23:37:58 -03:00
Alexandre Brandizzi
59b8cdaf7d
Merge pull request #168 from Sea-Haven-Industries/fix/sh-327-request-uplifts-permission
fix(uplifts): enforce request permission at service boundary
2026-09-25 02:35:19 +00:00
Alexandre Brandizzi
c5d82a996a fix(uplifts): audit uplift decisions on the dispatch's resolved work order
Approve, reject, request-changes, expiry and escalation staged their work
order audit with WorkOrderId = dispatch.WorkOrderId ?? 0. WorkOrderAuditLogs
requires a real work order, so any uplift on a dispatch without an owning
work order failed to save: the decision returned a 500 and the request stayed
Pending, and the expiry sweep failed on it every run.

The audit now goes to the work order the uplift resolves to through the
existing owner-or-linked read that revoke already uses. When none resolves,
the status change is saved on the request and no audit row is written.
2026-09-24 23:30:16 -03:00
Alexandre Brandizzi
8185875ad2
Merge pull request #176 from Sea-Haven-Industries/fix/ab/sh-327-admin-approves-uplifts
fix(uplifts): Admin passes uplift approval checks regardless of tier config (SH-327)
2026-09-25 02:13:26 +00:00
Alexandre Brandizzi
f3ef11b504 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	Api.SeaHavenIndustries/Controllers/VendorPortalController.cs
#	SeaHaven.Services/Implementation/VendorPortalService.cs
2026-09-24 23:05:17 -03:00
Alexandre Brandizzi
beb091fcc8 Merge remote-tracking branch 'origin/main' into lane/sh-327
# Conflicts:
#	Api.SeaHavenIndustries.Tests/WorkOrderUpliftControllerTests.cs
2026-09-24 23:00:18 -03:00
Alexandre Brandizzi
1e2f39a5fa chore(uplifts): drop ticket key from source comments 2026-09-24 22:57:09 -03:00
Alexandre Brandizzi
3e3f0f383d fix(media): serialize SH-116 media counts under the work-order lock and serve HEIC as image/heic
The 10-photo / 3-video cap was a check-then-insert with no lock on both
upload surfaces, so two overlapping uploads could both take the last slot.
The board media upload and the vendor portal upload now run count, insert
and save inside ExecuteWorkOrderMutationAsync. GetMediaContent maps .heic
to image/heic.
2026-09-24 22:56:21 -03:00
Alexandre Brandizzi
7591869462
Merge pull request #169 from Sea-Haven-Industries/hotfix/sh-387-concurrent-uplift
fix: return conflict for concurrent dispatch uplift requests
2026-09-25 01:54:50 +00:00
Alexandre Brandizzi
26b4aaf1b2 test(uplifts): cover Admin request-changes and evidence download with empty tier config (SH-327) 2026-09-24 22:51:57 -03:00
Alexandre Brandizzi
cfb05a906f fix(uplifts): Admin passes uplift approval checks regardless of tier config (SH-327)
UserCanApprove only accepted roles listed in Approvals:Tier1Roles/Tier2Roles,
so an environment whose config omits Admin denied every approval surface to
admins: can-approve, per-row CanDecide, approve/reject/request-changes and
evidence download. Admin now short-circuits the check; tier-role config still
governs every other role.
2026-09-24 22:34:32 -03:00
Alexandre Brandizzi
b116e71d16 test(uplifts): grant the SH-393 ownership fixture actor RequestUplifts
Uplift creation now checks the actor's RequestUplifts permission, so the
ownership tests construct the service with the permission services and seed
their actor as an Admin.
2026-09-24 22:31:07 -03:00
Alexandre Brandizzi
cc7cda4818 Merge remote-tracking branch 'origin/main' into lane/sh-327 2026-09-24 22:28:15 -03:00
Alexandre Brandizzi
2be74b261f test(uplifts): share conflict fixture dispatch through owned and linked work orders
After SH-393 the uplift dispatch resolves only through dispatches the work
order owns or links through DispatchWorkOrders. Model the shared dispatch
that way so the concurrent-insert conflict test exercises the conflict
mapping again; assertions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 22:28:04 -03:00
Alexandre Brandizzi
24ad21d7f2 Merge remote-tracking branch 'origin/main' into lane/sh-387 2026-09-24 22:25:54 -03:00
Alexandre Brandizzi
eecc2a61bd feat(vendor-portal): report work-order media counts on the dispatch detail
Adds MediaCounts (limits, current photos/videos, and the counts a new
completion version would see) so the portal can refuse an 11th photo or
4th video before uploading it. Uses the same count and replacement rule
the upload check enforces.
2026-09-24 22:24:07 -03:00
Alexandre Brandizzi
7b7df4463e
Merge pull request #174 from Sea-Haven-Industries/fix/ab/sh-393-uplift-scope
Some checks are pending
Backend CI / Build and test (push) Waiting to run
Backend CI / architecture (push) Waiting to run
Backend CI / review (push) Waiting to run
Backend CI / ci-complete (push) Blocked by required conditions
fix(uplifts): uplifts created from a work order show on that work order (SH-393)
2026-09-25 00:54:42 +00:00
Alexandre Brandizzi
76b13fb5da
Merge pull request #170 from Sea-Haven-Industries/fix/ab/sh-377-atomic-team-member-create
fix: make team member creation atomic
2026-09-25 00:52:34 +00:00
Alexandre Brandizzi
7297e30212 fix(dashboard): count All time over the same window the board lists (SH-392)
The Unassigned drill-down opens the board with no range, which searches
2000-01-01..2099-12-31 plus undated work. The All time count had no
bounds, so a work order dated outside that window was counted but not
listed. The count now uses the same window.
2026-09-24 21:45:06 -03:00
Alexandre Brandizzi
ce3b241991 test(uplifts): prove board-create dispatch ownership persists relationally (SH-393)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:45:00 -03:00
Alexandre Brandizzi
fd59a3da13 fix(media): enforce the 90-second video limit on the server
Read the duration from the MP4/MOV movie header (moov/mvhd) on both the
dispatcher media endpoint and the vendor portal completion upload, so a
direct request cannot bypass the browser check. Unreadable metadata still
never blocks an upload.
2026-09-24 21:38:00 -03:00
Alexandre Brandizzi
a8414cd4fa style(tests): format vendor quota test initializers 2026-09-24 21:29:51 -03:00
Alexandre Brandizzi
e15de6e90b fix(media): enforce the per-work-order photo/video limit across both surfaces
The 10-photo / 3-video limit only counted dispatcher attachments, so vendor
portal uploads could push a work order past it (and vice versa). Count the
work order's current vendor documents alongside its attachments on both the
dispatcher media endpoint and the vendor portal. A new completion version
does not count the version it replaces.
2026-09-24 21:27:01 -03:00
Alexandre Brandizzi
89376e99e4 fix(uplifts): resolve work-order uplifts through owned dispatches (SH-393)
Board create left the new primary dispatch with no WorkOrderId, so uplifts
created on those work orders were written to a dispatch the work order's
uplift reads never resolve: not listed, allowance never consumed, queue WO
number blank. The same orphan made ApptDate/vendor patches fail with
"A primary dispatch is required".

- Board create backfills Dispatch.WorkOrderId after the first save.
- Uplift create resolves its dispatch through the read-side scope
  (non-deleted, owned or linked); otherwise the stable
  "no primary dispatch" error.
- Data-only migration assigns existing orphaned primaries to the single
  work order naming them primary; idempotent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:22:13 -03:00
Alexandre Brandizzi
16845a55f3 test(vendor-portal): use a valid PDF signature in the size-class regression 2026-09-24 21:18:49 -03:00
Alexandre Brandizzi
07bc81cc52 fix(media): size uploads by the validated content type
A misleading file name could move a file into a larger size class: a real
PDF or JPEG named .mp4/.mov got the 100 MB video cap on the vendor portal,
and a .jpg declared with a foreign video type got it on the media endpoint.
Classify by the same resolved type the signature check validates; the
extension only decides when no allowlisted type is known.
2026-09-24 21:18:00 -03:00
Alexandre Brandizzi
4876fa2717 feat(dashboard): count open unassigned work orders for the period (SH-392)
GET /dashboard/stats now returns unassigned: open (not Completed or
Canceled) work orders with no dispatcher in the selected period, within the
caller's server-derived account scope. It uses the same filters as the
Unassigned board search, so the Dashboard number equals the list it drills
into. A dispatcher-scoped Dashboard has no unassigned work and returns 0.
2026-09-24 21:11:31 -03:00
Alexandre Brandizzi
9d5798437d test(workorders): pin the week-only date range rule shared with the board page (SH-391) 2026-09-24 21:11:20 -03:00
Alexandre Brandizzi
dc251c42d4 fix(media): apply SH-116 media contract and lift the 1 MB proxy body cap
The Elastic Beanstalk nginx proxy kept its 1 MB default body limit, so every
media upload over ~1 MB got an nginx 413 before reaching the API. Ship a
.platform nginx override (120M) in the bundle and assert it in the bundle
contract.

Apply the client-confirmed contract: photos up to 10 MB (JPEG/PNG/HEIC),
videos up to 100 MB (MP4/MOV), at most 10 photos and 3 videos per work order,
with stable generic rejection messages. The request ceiling (110 MB) sits
between the per-kind caps and the proxy so oversize files get the generic
message. The vendor portal accepts the same photo/video types and caps.
2026-09-24 20:52:44 -03:00
Alexandre Brandizzi
028908bd5a fix(board-search): make the Advanced Filters date range narrow results (SH-391)
An explicit date range let every undated, non-terminal work order through,
so Unassigned plus a range still returned all ~1,780 unassigned rows. The
range now matches Schedule On, or the Target Week for week-only rows
(overlap), the same date the weekly board groups by. Undated rows are added
only when the caller sends includeDateless, which the client uses for
searches with no range selected and for the Unassigned queue.
2026-09-24 20:51:44 -03:00
Alexandre Brandizzi
fddb7b7909 test: verify team member commit and rollback 2026-09-23 03:49:57 -03:00