Site Diary¶
Status: Specified — ready to build Build order: 5 of 10 — depends on Projects & Sites, Personnel & Attendance
Purpose¶
The Site Diary is the daily operational record for a site. Each day, the site manager (or agent) creates an entry capturing weather, a description of works carried out, headcount, visitors, and any site instructions received. Entries are signed off and form part of the auditable project record. The diary is the closest digital equivalent to the paper site diary that UK construction teams carry by law.
Entities¶
DiaryEntry¶
One entry per site per day. A day without an entry is simply absent — there is no concept of a blank entry.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | UUID | System | PK, generated |
| site_id | UUID | ✓ | FK → sites.id |
| project_id | UUID | ✓ | Denormalised |
| entry_date | DATE | ✓ | The date this entry covers — unique per site |
| weather_am | VARCHAR(50) | Morning weather condition — see values below | |
| weather_pm | VARCHAR(50) | Afternoon weather condition | |
| temperature_c | NUMERIC(4,1) | Celsius | |
| works_description | TEXT | ✓ | Narrative of works carried out |
| works_status | VARCHAR(20) | ✓ | Default: in_progress. See values below |
| headcount | INTEGER | Snapshot of workers on site (auto-populated from attendance; editable) | |
| visitors | JSONB | Ordered list of visitor records — see structure below | |
| instructions | JSONB | Site instructions received — see structure below | |
| delays | JSONB | Any delays recorded — see structure below | |
| plant_working | TEXT | Free text: summary of plant in operation | |
| materials_received | TEXT | Free text: materials delivered (cross-ref with Deliveries module) | |
| status | VARCHAR(20) | ✓ | Default: draft |
| signed_off_by | UUID | FK → users.id | |
| signed_off_at | TIMESTAMPTZ | ||
| custom_fields | JSONB | ✓ | Default {} |
| created_at | TIMESTAMPTZ | System | |
| updated_at | TIMESTAMPTZ | System | Trigger-maintained |
| created_by | UUID | System | FK → users.id |
| updated_by | UUID | System | |
| deleted_at | TIMESTAMPTZ |
-- One entry per site per day
CREATE UNIQUE INDEX idx_diary_entry_date
ON diary_entries(site_id, entry_date) WHERE deleted_at IS NULL;
CREATE INDEX idx_diary_entry_project
ON diary_entries(project_id, entry_date DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_diary_entry_status
ON diary_entries(site_id, status) WHERE deleted_at IS NULL;
Weather condition values:
sunny | partly_cloudy | overcast | light_rain | heavy_rain | fog | frost | snow | windy
Works status values:
in_progress | delayed | complete | no_work (e.g. Sunday, public holiday, weather stoppage)
Diary entry status values:
draft | signed_off
DiaryPhoto¶
Photos attached to a diary entry. Stored as separate records for efficient listing and offline sync.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | UUID | System | PK, generated |
| diary_entry_id | UUID | ✓ | FK → diary_entries.id |
| site_id | UUID | ✓ | Denormalised |
| project_id | UUID | ✓ | Denormalised |
| photo_key | VARCHAR(500) | ✓ | S3 object key |
| caption | VARCHAR(255) | Optional description | |
| taken_at | TIMESTAMPTZ | From EXIF or upload time if not available | |
| sort_order | INTEGER | ✓ | Default: 0 |
| created_at | TIMESTAMPTZ | System | |
| created_by | UUID | System | |
| deleted_at | TIMESTAMPTZ |
CREATE INDEX idx_diary_photo_entry
ON diary_photos(diary_entry_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_diary_photo_project
ON diary_photos(project_id, taken_at DESC) WHERE deleted_at IS NULL;
JSONB Field Structures¶
visitors¶
[
{
"name": "David Okafor",
"company": "National Highways",
"purpose": "Site inspection",
"time_in": "10:30",
"time_out": "12:00"
}
]
instructions¶
[
{
"from": "National Highways",
"reference": "SI-2026-047",
"description": "Additional earthworks required at chainage 1450. NEC compensation event to follow.",
"received_at": "2026-05-28T09:15:00Z"
}
]
delays¶
[
{
"reason": "weather",
"description": "Heavy rain halted excavation works 08:00–11:30",
"hours_lost": 3.5
}
]
Delay reason values: weather | access | plant_breakdown | material_shortage | instruction | third_party | other
Business Rules¶
-
Only one diary entry per site per day. Creating a duplicate returns 409.
-
headcountis auto-populated from the Attendance module when the entry is created (current headcount for that site on that date). The site manager can edit it if the attendance record is incomplete or if workers were tracked informally. -
A
draftentry can be edited freely. Asigned_offentry is locked — editing requiresstatusto be reset todraftfirst (Project Manager+ only). -
Sign-off (
status → signed_off) setssigned_off_byandsigned_off_atautomatically from the current user. -
Entries cannot be created for future dates.
entry_date > CURRENT_DATE→ 422. -
Entries can be backdated by Project Manager+, but not by Site Manager (Site Manager can only create entries for today or yesterday).
-
Soft deleting a diary entry soft-deletes all its photos in the same transaction.
-
Photos are uploaded via the presigned S3 pattern. Up to 20 photos per entry.
API Endpoints¶
Diary Entries¶
| Method | Path | Description |
|---|---|---|
| GET | /sites/{siteId}/diary |
List entries (paginated, filterable) |
| POST | /sites/{siteId}/diary |
Create entry |
| GET | /sites/{siteId}/diary/{id} |
Full detail |
| PATCH | /sites/{siteId}/diary/{id} |
Update entry |
| DELETE | /sites/{siteId}/diary/{id} |
Soft delete |
| POST | /sites/{siteId}/diary/{id}/sign-off |
Sign off entry |
| POST | /sites/{siteId}/diary/{id}/reopen |
Revert to draft (Project Manager+) |
| GET | /sites/{siteId}/diary/today |
Today's entry (or 404 if not yet created) |
Photos¶
| Method | Path | Description |
|---|---|---|
| GET | /sites/{siteId}/diary/{id}/photos |
List photos |
| POST | /sites/{siteId}/diary/{id}/photos |
Add photo (returns presigned PUT URL) |
| PATCH | /sites/{siteId}/diary/{id}/photos/{photoId} |
Update caption or sort order |
| DELETE | /sites/{siteId}/diary/{id}/photos/{photoId} |
Soft delete photo |
Project-level view¶
| Method | Path | Description |
|---|---|---|
| GET | /projects/{projectId}/diary |
Entries across all sites (paginated, filterable) |
Request / Response Shapes¶
POST /sites/{siteId}/diary¶
// Request
{
"entry_date": "2026-05-28",
"weather_am": "overcast",
"weather_pm": "light_rain",
"temperature_c": 14.5,
"works_description": "Continued foundation excavation at Grid E–F. Concrete poured to pad foundations P7–P12. Steel reinforcement delivered and stored in compound.",
"works_status": "in_progress",
"visitors": [
{
"name": "David Okafor",
"company": "National Highways",
"purpose": "Site inspection",
"time_in": "10:30",
"time_out": "12:00"
}
],
"instructions": [],
"delays": [],
"plant_working": "360° excavator (CPCS), compactor plate",
"materials_received": "See delivery log — concrete, rebar",
"custom_fields": {}
}
// Response 201
{
"id": "uuid-entry",
"site_id": "uuid-site",
"project_id": "uuid-project",
"entry_date": "2026-05-28",
"weather_am": "overcast",
"weather_pm": "light_rain",
"temperature_c": 14.5,
"works_description": "Continued foundation excavation...",
"works_status": "in_progress",
"headcount": 14,
"visitors": [...],
"instructions": [],
"delays": [],
"plant_working": "360° excavator (CPCS), compactor plate",
"materials_received": "See delivery log — concrete, rebar",
"status": "draft",
"signed_off_by": null,
"signed_off_at": null,
"photo_count": 0,
"custom_fields": {},
"created_at": "2026-05-28T16:45:00Z",
"created_by": "uuid-user"
}
// Response 409 — entry already exists for this date
{
"error": "duplicate_entry",
"message": "A diary entry already exists for 2026-05-28 on this site.",
"existing_entry_id": "uuid-existing"
}
GET /sites/{siteId}/diary¶
Query params: status (draft|signed_off), from_date, to_date,
works_status, sort (default -entry_date), limit, cursor
// Response 200
{
"data": [
{
"id": "uuid-entry",
"entry_date": "2026-05-28",
"weather_am": "overcast",
"works_description": "Continued foundation excavation at Grid E–F...",
"works_status": "in_progress",
"headcount": 14,
"status": "draft",
"photo_count": 3,
"created_by_name": "James Fletcher",
"created_at": "2026-05-28T16:45:00Z"
}
],
"pagination": { "total": 42, "cursor": "eyJpZCI6Ii4uLiJ9", "has_more": true }
}
POST /sites/{siteId}/diary/{id}/sign-off¶
// Response 200
{
"id": "uuid-entry",
"status": "signed_off",
"signed_off_by": "uuid-user",
"signed_off_by_name": "James Fletcher",
"signed_off_at": "2026-05-28T17:30:00Z"
}
// Response 409 — already signed off
{
"error": "already_signed_off",
"message": "This entry has already been signed off."
}
Permission Matrix¶
| Action | Platform Admin | Tenant Admin | Project Manager | Site Manager | Viewer |
|---|---|---|---|---|---|
| List entries | ✓ | ✓ | ✓ assigned | ✓ own site | ✓ assigned |
| Create entry (today) | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
| Create entry (backdate) | ✓ | ✓ | ✓ assigned | Yesterday only | ✗ |
| Edit draft entry | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
| Edit signed-off entry | ✓ | ✓ | ✓ assigned | ✗ | ✗ |
| Sign off | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
| Reopen (draft) | ✓ | ✓ | ✓ assigned | ✗ | ✗ |
| Delete entry | ✓ | ✓ | ✓ assigned | ✗ | ✗ |
| Add photos | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
| View photos | ✓ | ✓ | ✓ assigned | ✓ own site | ✓ |
| Delete photos | ✓ | ✓ | ✓ assigned | ✓ (draft only) | ✗ |
Notifications Triggered¶
None. The Site Diary does not fire any notifications. Alerts about missing entries (e.g. "no diary entry for yesterday") are a V2 feature.
Offline Sync Scope¶
| Data | Syncs to | Notes |
|---|---|---|
| DiaryEntries | Site Manager | Last 90 days for assigned sites |
| DiaryPhotos metadata | Site Manager | Last 90 days — keys only, not S3 data |
| Photo thumbnails | Not synced | Loaded on demand |
Offline create flow: - Site Manager creates entry on device, writes to local SQLite - Photos captured offline are stored in device storage - On reconnect: entry and photos upload sequentially - If entry conflicts (another device created the same date), server returns 409 — site manager sees the conflict and resolves manually
Headcount auto-population: - When creating an entry offline, headcount is derived from local AttendanceRecords - If sync is behind, the count may be lower than actual — site manager can correct it
Cross-Module Dependencies¶
Upstream¶
| Module | Dependency |
|---|---|
| Projects & Sites | site_id, project_id |
| Personnel & Attendance | Headcount auto-populated from attendance_records |
Downstream¶
None. The Site Diary is a terminal module — nothing reads from it.
Tests Required¶
Diary Entries¶
- Create entry — valid payload, today's date → 201, headcount auto-populated
- Create entry — duplicate date for same site → 409
- Create entry — future date → 422
- Create entry — backdated by Site Manager to more than yesterday → 403
- Create entry — backdated by Project Manager to any past date → 201
- Update entry — PATCH semantics on draft
- Update entry — signed_off status → 422 (cannot edit without reopening)
- Sign off — valid → 200,
signed_off_byandsigned_off_atset - Sign off — already signed off → 409
- Reopen — Project Manager can revert to draft → 200
- Reopen — Site Manager → 403
- Delete entry — soft deletes entry and all photos
- Headcount — auto-populated from attendance records for that date
Photos¶
- Add photo — returns presigned PUT URL
- List photos — ordered by
sort_order - Update caption — success
- Delete photo — soft delete; entry photo_count decremented
- Maximum 20 photos per entry — 21st upload → 422
Cross-tenant isolation (mandatory)¶
- GET entry from another tenant's site → 404
- POST entry with
site_idfrom another tenant → 404 - POST photo on an entry from another tenant → 404
Permission boundaries¶
- Site Manager cannot edit signed-off entry → 422
- Site Manager cannot reopen entry → 403
- Site Manager cannot delete entry → 403
- Viewer cannot create or edit → 403
- Viewer can view entries and photos → 200
Default Tenant Labels¶
| Key | Default |
|---|---|
module.site_diary |
Site Diary |
field.diary.works_description |
Works Description |
field.diary.weather_am |
Morning Weather |
field.diary.weather_pm |
Afternoon Weather |
field.diary.visitors |
Visitors |
field.diary.instructions |
Site Instructions |
field.diary.delays |
Delays |
field.diary.plant_working |
Plant Working |
field.diary.materials_received |
Materials Received |
status.diary.draft |
Draft |
status.diary.signed_off |
Signed Off |
status.diary.in_progress |
In Progress |
status.diary.delayed |
Delayed |
status.diary.no_work |
No Work |
Foundation Dependencies¶
| Component | Location | Used for |
|---|---|---|
TenantMiddleware |
core/middleware.py |
Sets request.state.tenant |
get_db |
core/db.py |
Schema-scoped AsyncSession |
get_current_user |
core/security.py |
Auth |
require_permission |
core/security.py |
RBAC |
EntityMixin |
core/models.py |
Universal columns |
PaginatedResponse |
core/pagination.py |
List endpoint wrapper |
ConstruoError hierarchy |
core/exceptions.py |
Typed exceptions |
presigned_put_url |
core/storage.py |
Photo upload |
audit_entity_change trigger |
DB foundation | Applied to diary_entries, diary_photos |
factories.py |
tests/factories.py |
create_diary_entry, create_diary_photo |