Skip to content

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

  1. Only one diary entry per site per day. Creating a duplicate returns 409.

  2. headcount is 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.

  3. A draft entry can be edited freely. A signed_off entry is locked — editing requires status to be reset to draft first (Project Manager+ only).

  4. Sign-off (status → signed_off) sets signed_off_by and signed_off_at automatically from the current user.

  5. Entries cannot be created for future dates. entry_date > CURRENT_DATE → 422.

  6. Entries can be backdated by Project Manager+, but not by Site Manager (Site Manager can only create entries for today or yesterday).

  7. Soft deleting a diary entry soft-deletes all its photos in the same transaction.

  8. 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_by and signed_off_at set
  • 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_id from 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