-
-
Notifications
You must be signed in to change notification settings - Fork 0
Evidence Inventory
The Inventory tab on /dashboard tracks the physical drives that hold digital evidence.
A drive is the barcoded physical object; evidence items (CaseReceivedFile rows) are the
logical files on it.
Key design decisions:
- One drive → one current case (but reusable across a wipe-and-rotate lifecycle)
- Wipe keeps evidence records (history must survive) — only the drive link is cleared
- Lookup and management are open to any logged-in user (evidence-locker workflow)
available ──► in_use ──► wiped ──► available (rotate back into circulation)
│
└──► retired (permanently decommissioned)
- available — in the evidence locker, not assigned to a case
-
in_use — assigned to an active case;
date_assignedis stamped -
wiped — sanitised and ready to reuse;
date_wipedstamped,date_assignedcleared - retired — decommissioned; no further use
The summary card at the top of the Inventory tab shows a status donut chart + 4 stat tiles:
-
Available —
#2dce89(green) -
In use —
#5e72e4(indigo) -
Wiped —
#8898aa(grey) -
Retired —
#4a5568(dark grey)
Center overlay shows the total drive count. The card is hidden when no drives are registered.
If a data retention policy is configured and any in-use drives are overdue, an amber alert banner appears below the tiles:
"X drive(s) have exceeded the configured retention period — consider wiping and rotating them."
Set at /manage/settings → Storage tab:
- Retention period (months) — blank = no policy (no overdue indicators anywhere)
- When set,
_is_overdue(drive, retention_months, case_close_date)returnsTruewhen:status == 'in_use'- The associated case has a
close_date(open cases are never flagged overdue) (utcnow - close_date).days > retention_months * 30
The clock starts at case closure, not at drive assignment — the intent is "how long has this drive been sitting around after the investigation finished."
Two UI indicators when a drive is overdue:
- Amber
⚠ Overduebadge inline next to the status chip in the drive table row - The amber alert banner in the summary card
No indicator appears when the policy is NULL.
The Scan / enter barcode card at the top of the Inventory tab resolves a barcode to its drive, location, current case, and linked evidence items:
GET /api/v2/dashboard/inventory/lookup?barcode=<value>
Matching is exact first, then case-insensitive fallback.
When typing a barcode in the evidence modal and tabbing away, the field fires a lookup automatically. If a drive is found:
- The "On physical drive" picker auto-selects that drive
- A green chip
→ <label> · <location>appears - An inline location editor is revealed — update the drive's location without leaving the modal
If no match: amber chip (non-blocking — the barcode may be new and not yet registered in Inventory).
Click Register drive on the Inventory tab. Required field: Barcode (must be unique). Optional fields: Label, Serial number, Physical location, Capacity, Notes, Created by.
POST /api/v2/dashboard/inventory/drives
{
"barcode": "EV-2026-0042",
"label": "Seagate 4TB Evidence Drive",
"serial_number": "ST4000LM024-12345",
"physical_location": "Locker A / Shelf 3",
"capacity": "4TB",
"created_by": "Zachary Carter"
}
Sparse update — only fields present in the body are touched:
PUT /api/v2/dashboard/inventory/drives/<id>
{
"physical_location": "Locker B / Shelf 1"
}
Assigning a new case_id automatically flips status → in_use and stamps date_assigned
(unless an explicit status is provided).
POST /api/v2/dashboard/inventory/drives/<id>/wipe
Effect:
- All
case_received_file.drive_idFKs pointing to this drive →NULL(evidence rows kept) -
drive.case_id→NULL -
drive.status→available -
drive.date_wipedstamped,drive.date_assignedcleared
The drive is now available for the next case. Evidence history is preserved.
Two paths:
-
Barcode field in the Register / Edit Evidence modal — typing a barcode that matches
a drive auto-links
drive_idon save (_reconcile_evidence_driveincase_rfiles_db.py) - "On physical drive" picker — explicit select2 dropdown; barcode is backfilled from the chosen drive if the barcode field is empty
barcode ↔ drive_id reconciliation rules (in priority order):
- Explicit
drive_idalways wins - Typed barcode matching a drive → auto-link
drive_id -
drive_idset + barcode empty → backfill barcode from the drive
Retroactive backfill: scripts/backfill_evidence_drive_links.py (dry-run default, --apply to commit).
All seven data columns in the Drive inventory table are clickable sort headers: Bar code / Label / Status / Physical location / Case / Capacity / Added
- First click on a column → A→Z (ascending, ▲)
- Click the same column again → Z→A (descending, ▼)
- Click a different column → resets direction to A→Z on that column
Active sort column gets a violet ▲/▼ indicator; all other columns show a neutral ⇅.
Capacity sorts numerically — iris_inv_capacity_bytes() parses "4TB", "500GB",
"256MB", etc. into byte values so "1 TB" correctly sorts after "500 GB" (plain
string sort would invert them).
Case column sorts by the underlying case_id integer regardless of display name.
Drives with no associated case sort last regardless of direction.
Sort state is in-memory only — clicking Refresh re-fetches from the server and resets to natural insertion order.
Six-touch pattern:
- Model column on
EvidenceDriveinmodels/models.py - Guarded Alembic migration (
_table_has_columncheck) -
create_drive/update_driveloop +_serialize_driveininventory_db.py - Both REST endpoints (POST create + PUT update,
body.get(...)) - Modal field + clear/populate/save JS in
index.html(Inventory pane) - Update the lookup result card if the field belongs there
No bundle rebuild needed — all inline JS + server-side. docker restart iriswebapp_app
applies the migration and flushes the Jinja template cache.
| Method | Path | Description |
|---|---|---|
GET |
/api/v2/dashboard/inventory/drives |
List all drives (includes overdue: bool when policy is set) |
POST |
/api/v2/dashboard/inventory/drives |
Register a new drive |
PUT |
/api/v2/dashboard/inventory/drives/<id> |
Update drive (sparse) |
DELETE |
/api/v2/dashboard/inventory/drives/<id> |
Delete drive (evidence rows survive, drive_id → NULL) |
POST |
/api/v2/dashboard/inventory/drives/<id>/wipe |
Wipe and return to circulation |
GET |
/api/v2/dashboard/inventory/lookup?barcode= |
Resolve barcode → drive + case + evidence items |
GET drives response includes overdue: bool on each drive object when a retention policy is
configured. Clients that display a status should check this field.
| Script | Purpose |
|---|---|
backfill_evidence_drive_links.py |
Retroactively link evidence rows to drives via barcode matching (--apply to commit) |
Drive capacity planning — order-more indicator. A planned addition to the Inventory tab summary card that surfaces a drive-ordering recommendation based on current inventory vs projected demand.
How it works:
-
Average case intake — rolling window of
Cases.open_dateover recent months - Effective 30-day supply — available drives + in-use drives whose case closed within the retention window (wipe-eligible within 30 days)
- Runway = effective supply ÷ avg cases/month
- When runway drops below the configured threshold (default 2 months), the card shows an order recommendation with a suggested quantity
No new tables or migrations needed — all required data is already fetched in list_drives().
Configuration lives alongside retention_months in /manage/settings → Storage tab.