This README is my walkthrough for the Grand Coast Construction website and internal operations platform. I wrote it in the order that I would set up, run, use, and test the application myself.
The project is a Django application for:
- Grand Coast Construction's public marketing website
- The private staff Operations workspace
- The employee Team workspace
- The secure client portal
- The lower-level Django administration interface, styled with Unfold
The visual system stays consistent everywhere: Grand Coast navy, blue, white, and gold; the supplied Grand Coast logo; Montserrat for interface text; and Merriweather for major headings.
I converted the original frontend prototype into a database-backed Django application. The browser is no longer the source of truth for leads, estimates, projects, tasks, users, documents, messages, or media.
The application now includes:
- Secure Django session authentication
- Owner, Manager, Office, Sales, and Field employee roles
- Employee profiles and one-time employee invitation links
- Client records and one-time client portal invitations
- Lead capture from the public contact form
- Lead assignment, statuses, priorities, notes, and tasks
- Lead-to-client conversion
- Server-calculated estimates with editable line items
- Client estimate review and acceptance
- Project creation from accepted estimates
- Project assignments, milestones, updates, documents, and media
- Unified internal tasks
- Internal schedule events and employee calendars
- Employee clock-in, clock-out, and time entries
- Two-way client and staff messaging
- Protected client-only documents and media
- Public project detail pages and gallery readiness
- Content Studio for public website content
- Optional Google Review link
- Unfold for the private /gccad/ interface
- Operations search from the Unfold admin home
I intentionally did not add Stripe, payment processing, payment webhooks, card collection, contact-form email notifications, external calendar synchronization, payroll functionality, or geolocation tracking.
I keep the Django project inside the website directory:
GCC/
├── README.md
└── website/
├── manage.py
├── backend/
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
├── operations/
│ ├── models.py
│ ├── forms.py
│ ├── views.py
│ ├── urls.py
│ ├── services.py
│ ├── search.py
│ ├── admin.py
│ ├── migrations/
│ ├── management/commands/seed_operations.py
│ ├── templates/operations/
│ └── static/operations/
├── templates/
├── db.sqlite3
├── media/
└── venv/
The old demo app has been retired. Old /demo/... links only redirect to the current routes for bookmark compatibility; demo is not used in the visible navigation or page copy.
I open PowerShell and move into the Django project:
cd C:\dev\GCC\websiteIf I need to create the virtual environment from scratch, I run:
py -3.12 -m venv venv
.\venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
pip install -r requirements.txtIf PowerShell does not allow activation, I can use the virtual-environment Python executable directly for every command:
.\venv\Scripts\python.exe -m pip install -r requirements.txtThe main dependencies are:
- Django 6.1
- django-unfold 0.104.1
- django-storages with boto3 support for optional production object storage
For local development, the application uses SQLite and stores uploaded files under website/media/.
I apply Django's migrations:
.\venv\Scripts\python.exe manage.py migrateThe Operations migrations create and preserve the application data for:
- Clients and client invites
- Leads and staff assignment
- Estimates and estimate line items
- Projects and staff assignments
- Milestones and project updates
- Unified tasks
- Media assets and lead attachments
- Client messages
- Employee profiles and employee invites
- Schedule events
- Time entries
- Project documents
- Services, process steps, and site settings
- Activity history
The previous follow-up concept was migrated into the new Task model while preserving completion state. I do not delete or reset existing application data when I run migrations.
I create a superuser if I do not already have one:
.\venv\Scripts\python.exe manage.py createsuperuserI use that account for:
- The separate Django/Unfold administration area at /gccad/
- The owner-first Operations Command Center at /dashboard/
- Employee access, assignments, schedules, and visibility controls
- Local setup and data inspection
Only an active superuser can enter /gccad/ or /dashboard/. The seed command automatically places an existing superuser in the Owner group, creates an EmployeeProfile for staff accounts, and creates a disabled AdminSecurityProfile for each superuser. The Owner group is reserved for this account; employee invitations offer Manager, Office, Sales, and Field.
I run the idempotent seed command:
.\venv\Scripts\python.exe manage.py seed_operationsI can safely run this more than once. It uses stable identifying fields and does not duplicate the seeded records.
The seed command creates or preserves sample:
- Services and process steps
- Clients and leads
- Estimates and line items
- Projects and milestones
- Project updates
- Media records using the existing branded sample artwork
- Tasks and activity entries
- Site content
For a local client-portal walkthrough, I can create the seeded Maya client login by passing a password explicitly:
.\venv\Scripts\python.exe manage.py seed_operations --client-password "ChangeThisLocalOnly123!"That password is only for my local presentation database. I do not put credentials in source control, and the command has no hardcoded default password.
The command also creates these groups idempotently:
- Owner
- Manager
- Office
- Sales
- Field
I can verify the database state with:
.\venv\Scripts\python.exe manage.py showmigrationsI run the local server:
.\venv\Scripts\python.exe manage.py runserver 127.0.0.1:8000I then open:
- Public website: http://127.0.0.1:8000/
- Owner Operations workspace: http://127.0.0.1:8000/dashboard/
- Preserved workspace overview: http://127.0.0.1:8000/dashboard/overview/
- Employee Team workspace: http://127.0.0.1:8000/team/
- Authenticated client portal: http://127.0.0.1:8000/portal/
- Owner-only administration launchpad: http://127.0.0.1:8000/gccad/
- Sign-in page: http://127.0.0.1:8000/accounts/login/
All staff accounts use Django's existing User model, Group model, and session authentication.
I use an active superuser as the Owner. The Owner can use the full Operations workspace, manage team access, manage every operational record, control assignments and visibility, and use the native Django/Unfold user and group administration. The Owner group is a label for this account, not an employee invitation option.
A Manager uses /team/ and cannot enter /dashboard/ or /gccad/. A Manager can view the complete Team calendar, including assigned employee names, and work with their own permitted assigned projects, tasks, internal reports, personal time entries, and clock-in/out. The calendar is read-only for Managers; they cannot create or edit schedules, change assignments, change employee access, or publish client-visible or public material.
An Office user uses /team/ and cannot enter /dashboard/ or /gccad/. Office visibility is assignment-scoped: assigned projects, personally relevant tasks, assigned schedule events, relevant client context, internal reports, personal time entries, and clock-in/out. Office users cannot view another employee's schedule or change anyone's access, assignments, or publication visibility.
A Sales user uses /team/ for assigned leads, site visits, estimates, and follow-ups. Sales visibility is assignment-scoped and excludes project financials, internal costs, margins, and construction controls.
A Field user uses the dedicated Team workspace. A Field user can see only assigned projects, assigned tasks, relevant client context, personal schedule entries, personal time entries, internal updates, and permitted media reporting. Field uploads and reports are forced to Internal until the Owner publishes them. Field users cannot enter the staff dashboard, native Unfold admin, or another client's portal.
A client only sees the projects, estimates, milestones, client-visible updates, client-visible documents, permitted media, and messages connected to that client's own record.
I enforce these restrictions in both group permissions and view/queryset logic. I do not rely on hiding a link in the interface as a security boundary.
I start at / and check the public experience:
- The Grand Coast logo appears in the header and footer.
- The navy, blue, white, and gold palette is consistent.
- The home page includes the current headline, supporting copy, services, process, featured work, and contact call-to-action.
- The public navigation links to Services, Projects, Process, and Contact.
- The public layout works on desktop and mobile.
The public routes are:
- /
- /services/
- /projects/
- /process/
- /contact/
The public contact form uses normal Django form handling and CSRF protection. I can submit a test inquiry from /contact/, and it creates a real Lead record in the database.
I sign in as the Owner and open:
/dashboard/leads/
I use the lead workspace to:
- Search by name, service, location, or related client details.
- Filter by lead status.
- Select a lead to open its detail panel.
- Assign the lead to a staff member.
- Move the lead through New, Contacted, Qualified, Quoted, Won, or Lost.
- Mark the lead as a priority.
- Add or edit internal notes.
- Add a task with a due date, priority, and assignee.
- Review activity history.
- Convert the lead into a Client when I am ready.
The lead conversion is explicit. A lead does not silently become a client just because it exists.
The intended business sequence is:
Lead captured
→ Lead converted into Client
→ Estimate created
→ Estimate accepted by Client
→ Project created
→ Staff, tasks, milestones, documents, updates, and media assigned
Lead status changes, assignments, notes, priority changes, tasks, and conversion activity persist in the database.
From the client workspace at:
/dashboard/clients/
I can:
- Create or edit client records.
- See linked leads, estimates, and projects.
- Create a one-time client invite.
- Copy the invite link for the client.
- Revoke access.
- Review client messages and unread state.
When email delivery is enabled, creating the client invite also queues and sends the one-time link to the client email address. The copy link remains available in the Operations alert as a secure fallback. Local development keeps delivery disabled by default, so it is safe to test without contacting a real inbox.
For a real or development SMTP/provider delivery test, configure GCC_EMAIL_DELIVERY_ENABLED=true, DEFAULT_FROM_EMAIL, and the existing Django EMAIL_* settings. A local console or locmem backend is useful for confirming message contents but does not deliver to an external inbox. Pending and failed messages can be retried with:
python manage.py dispatch_email_outbox --limit 100
The invite email is idempotent per generated invite, never logs the raw token, and delivery failure does not roll back the client invite.
The client invite:
- Is stored as a hash rather than a raw token.
- Expires after seven days.
- Can only be used once.
- Lets the client create a username and password.
The client completes the invite at:
/accounts/invite/<token>/
After logging in, the client is redirected to /portal/.
I open:
/dashboard/estimates/
I can create an estimate from a lead that already has a client association. I add line items with:
- Description
- Quantity
- Unit price
- Sort order
The estimate editor recalculates the subtotal and total in the browser for convenience, but the server recalculates the final Decimal total before saving. The browser is not trusted for business calculations.
I can:
- Save an estimate as Draft.
- Edit line items.
- Remove line items.
- Set an informational deposit amount.
- Send the estimate.
- Preview the customer-facing estimate.
- Decline or accept through the appropriate workflow.
The supported estimate statuses are:
- Draft
- Sent
- Accepted
- Declined
Once an estimate is accepted, it becomes read-only. If I need changes, I create a new estimate revision rather than silently changing the accepted record.
The deposit amount is informational only. No payment status, card collection, Stripe request, Checkout session, PaymentIntent, or webhook exists in this application.
I sign in with the seeded client account or a client account created through an invite and open:
/portal/
I confirm that the client can see only their own:
- Project information
- Estimate
- Deposit information
- Milestones
- Client-visible updates
- Client-visible documents
- Public/client media
- Project conversation
I click the estimate acceptance action. The application records:
- Accepted status
- Acceptance timestamp
- Accepting client user
The interface clearly explains that acceptance does not collect payment.
From the estimate workspace, I select the action to create a project from an accepted estimate.
The project form can include:
- Project title
- Client
- Lead
- Accepted estimate
- Assigned staff
- Location
- Project type
- Status
- Next step
- Summary
- Start date
- Target date
- Published state
- Cover image or fallback artwork
Lead-derived projects require the accepted-estimate workflow. I can still create standalone projects for work that did not originate from a lead.
I manage projects at:
/dashboard/projects/
Inside a project I can:
- Change the project status.
- Assign employees.
- Toggle milestones complete or incomplete.
- Add project updates.
- Mark updates as client-visible or internal.
- Add documents.
- Add or edit media.
- Review activity history.
- Preview the client-facing project view.
I manage internal tasks at:
/dashboard/tasks/
Tasks can be connected to:
- A lead
- A project
- A milestone
Each task supports:
- Title
- Description
- One assigned employee
- Watcher employees
- Status
- Priority
- Due date
- Completion timestamp
- Created/updated metadata
The task statuses are:
- Open
- In progress
- Blocked
- Complete
The priorities are:
- Normal
- High
- Urgent
I can filter tasks by employee, project, status, priority, due date, or search text. Task creation, reassignment, and completion add activity entries.
Employees work with their assigned tasks at:
/team/tasks/
They can update permitted task fields and status, but they cannot see unrelated staff tasks.
Only the Owner creates and edits schedule events at:
/dashboard/calendar/
A schedule event supports:
- Title
- Assigned employees
- Optional project
- Optional task
- Start time
- End time
- Location
- Notes
The calendar is internal and does not synchronize with Google Calendar or Outlook. The Owner can also manage blank-by-default recurring employee templates at the bottom of the calendar page:
- one continuous shift per employee and weekday
- Pacific-time date overrides for adjusted hours or days off
- Clear / use weekly schedule to remove an exception without changing the template
- company-wide short and closed days that constrain or suppress effective employee hours
Closed and short days never delete schedule events or employee templates. Existing event validation prevents a new event from conflicting with a company closure or short-day window.
Managers view the complete read-only Team calendar at:
/team/calendar/
Office and Field employees view only their own assigned schedule at:
/team/calendar/
The Team workspace provides a monthly calendar with a mobile-friendly list fallback. Each event shows the complete range, such as Monday · 4:00 PM – 8:00 PM, in America/Los_Angeles. Clicking an employee event opens read-only details for non-Owners.
Schedule mutations also create persistent employee inbox notifications. When a native employee app is configured, the same notification is sent through Expo Push Service with the device default sound and a link back to the relevant calendar day. Client portal alerts use the same delivery path when a client has enabled notifications in the Expo app. Client payloads contain only the client-visible title, body, notification ID, type, and an authorized /portal/... destination; internal costs, margins, vendors, notes, and staff notifications are never included. Failed deliveries remain retryable with:
python manage.py dispatch_push_notificationsThe existing employee/owner device endpoints are /team/notifications/devices/ and /team/notifications/devices/deactivate/. Client devices use /portal/notifications/devices/ and /portal/notifications/devices/deactivate/; the mobile app selects the correct endpoint from the authenticated workspace. Device registration is scoped to the authenticated account, and logout or client account deletion deactivates that account's device. EXPO_PUSH_ENABLED, EXPO_PUSH_URL, and EXPO_ACCESS_TOKEN are shared by employee, owner, and client delivery. There is no separate client push flag.
I use the staff review page at:
/dashboard/time/
Employees use:
/team/time/
An employee can clock in and clock out. The application allows only one active time entry per employee.
A time entry can include:
- Employee
- Optional project
- Optional task
- Clock-in timestamp
- Clock-out timestamp
- Note
- Manager correction metadata
Only the Owner can correct time entries from /dashboard/time/. Employees can see their own recent entries and clock themselves in and out from /team/time/.
Database timestamps remain timezone-aware. The application uses America/Los_Angeles and displays times in 12-hour format, such as 1:05 PM.
This is operational time tracking only. I do not treat it as payroll, wage, overtime, geolocation, or compliance software.
I open:
/team/
The Team workspace includes:
- Personal overview
- Assigned projects
- Assigned tasks
- Today's schedule
- Time tracking
- Internal project reporting
- Permitted media reporting
- Employee profile
- Password change
Employees can update their own profile information at:
/team/profile/<user-id>/
I use the staff team-management area at:
/dashboard/team/
From there, the Owner can:
- Invite an employee.
- Choose the employee's group.
- View employee profiles.
- Update job title, phone, and active status.
- Control which projects, tasks, milestones, and schedule events employees can see.
- Create a one-time password reset link.
The employee onboarding link expires after seven days. Manager-issued password reset links expire after one day and can only be used once. Employees can also request a password-recovery email from the public sign-in page; those links expire after one hour and can only be used once. After five unmatched recovery-email submissions, that browser is paused for 15 minutes. Since onboarding and manager-issued reset email delivery is deferred, I copy those links manually.
Employees can update only their own profile details. They cannot change another employee's schedule, access, assignments, or visibility.
I upload and manage project documents at:
/dashboard/documents/
Documents support:
- Project association
- Title
- Category
- Description
- File
- Internal visibility
- Client-visible visibility
- Uploader and timestamps
The upload uses a multipart Django form with file type and size validation. Common PDF and office document formats are supported.
Client-visible documents are delivered through an authenticated Django view. A client cannot access an internal document by guessing its URL, and a client cannot access a document belonging to another client's project.
I manage media from:
/dashboard/media/
The system supports:
- Images and videos
- Captions
- Project association
- Public visibility
- Client-only visibility
- Internal visibility
- Protected authenticated delivery for non-public files
The current media is presentation artwork and seeded sample content. I have not added new real construction photography.
When I have real photos and videos, I can upload them through the existing staff interface. The system is already prepared for:
- Public project galleries
- Client-only project media
- Internal staff media
- Image lightbox previews
- Video display
- Empty gallery states when a project has no real photography yet
Field employees cannot publish internal reporting media publicly.
Published projects have public detail pages at:
/projects/<project-uuid>/
A public project page can show:
- Project title and location
- Project summary and story
- Current public status
- Cover image or branded fallback artwork
- Public media gallery
- An empty gallery state when there is no public photography yet
- Google Review call-to-action when configured
Only published projects and public media are exposed on the public website.
I edit public website content at:
/dashboard/content/
Content Studio controls:
- Homepage headline
- Homepage supporting copy
- Service names and descriptions
- Featured project information
- Process steps
- Google Review URL
After saving, the public pages read from the database and show the updated content. I do not need to edit templates for normal content changes.
The Google Review field is a normal external URL. If it is blank, the public review call-to-action does not render. There is no Google API, Places API, synchronization, or stored Google credential.
I open:
/gccad/
Unfold is the lower-level Django administration interface. It is separate from the branded Operations workspace at /dashboard/, and both are restricted to the active Owner superuser.
I use Unfold for:
- User and group administration
- Lower-level model inspection
- Permissions
- Database-backed record management
- History and change forms
The Unfold interface keeps the Grand Coast logo, navy/blue/white/gold palette, light theme, and Operations wording.
The Unfold admin search is extended to search Operations records, including:
- Clients
- Leads
- Employees
- Estimates
- Projects
- Tasks
- Schedule events
- Documents
- Messages
- Media
- Services
- Process steps
- Site settings
- Related milestones, updates, line items, and client details
When I select an Operations result, it opens the branded dashboard or Team workspace instead of sending me to an unrelated native edit page.
I use the private administration entry at:
/gccad/
The old /admin/ path intentionally returns 404. The public /accounts/login/ page is for employee and client accounts; it rejects superuser credentials with the same generic invalid-login response as any failed login. I do not reveal the private administration route through public navigation, the regular login page, or recovery pages.
When I open /gccad/, I first enter the administrator username or email. If that superuser has enabled PIN protection, I enter the six-digit PIN before the password form. If authenticator verification is enabled, I enter a fresh code from the authenticator app after the password is accepted. Direct requests to the login, model pages, or authenticator page cannot skip the gate.
After I sign in, I can open Security settings from the Unfold account area. I can enable or disable my own PIN and enroll or disable my own authenticator app without re-entering credentials. PIN setup uses two six-digit fields, and authenticator setup displays a QR code that I scan with my app before choosing the enable button. Existing superusers start with PIN and authenticator protection disabled until I turn them on.
If I forget the administrator password, I choose the password-recovery link on the private login or access screen and submit the verified administrator email. The form only proceeds for an active superuser email. An unmatched email stays on the form and shows the remaining attempts; after five unmatched submissions, that browser is paused for 15 minutes. A matching request uses the configured email backend to send a one-time link that expires after one hour. Password recovery changes only the password, does not disable PIN or authenticator protection, and never signs me in automatically.
If I forget the PIN or lose the authenticator, I choose the separate recovery link on the private access screen and submit the verified administrator email. The form only proceeds for an active superuser email. An unmatched email stays on the form and shows the remaining attempts; after five unmatched submissions, that browser is paused for 15 minutes. A matching request uses the configured email backend to send a one-time recovery link that expires after 30 minutes. Recovery lets me choose a new six-digit PIN, disables authenticator verification, revokes existing administrator sessions, and never signs me in automatically. Locally, email uses the console backend; production SMTP values come from environment variables.
The Security settings page also provides the active Owner superuser with an administration access watch. It records invalid administrator identifiers, passwords, PINs, authenticator codes, recovery verification failures, blocked attempts, and successful administrator sign-ins with the source IP, route, user agent, account when known, review state, and email-delivery state. It never records a password, PIN, authenticator code, session value, or recovery token. From that page I can search and review events, manually block an IP address for administration routes, lock an administrator's next sign-in, mark events reviewed, and reversibly unblock a target. Existing signed-in administrator sessions remain available so an accidental IP block can be removed. A signed-in administrator cannot lock their own account.
Failed administration authentication and security-verification events can send alerts to every active staff superuser with a non-empty, valid email address. Delivery is queued after the event is committed, so an SMTP failure never changes the login response. Configure DEFAULT_FROM_EMAIL and the existing Django email settings to enable delivery. The emergency ADMIN_SECURITY_EMAIL_ALERTS_ENABLED=false switch preserves event records while suppressing alert delivery. Because the alert policy intentionally emails every failed attempt, a brute-force attack can create substantial email volume; use a monitored security mailbox and review the dashboard as the primary audit trail. Failed or unavailable deliveries can be retried with:
.\venv\Scripts\python.exe manage.py dispatch_admin_security_emails --limit 100The administration security controls use REMOTE_ADDR by default. I leave ADMIN_TRUSTED_PROXY_IPS empty unless a known reverse proxy is terminating requests. Only explicitly listed proxy IPs are allowed to provide X-Forwarded-For or Forwarded client information; spoofed forwarding headers from other sources are ignored. For a trusted proxy deployment, I provide a comma-separated list of the proxy addresses, for example 203.0.113.10,2001:db8::10. Blocks apply only to administration access and do not block employee, client, public, or other account routes.
The website is also installable as a Progressive Web App. Public pages register /service-worker.js and reference /manifest.webmanifest. Public static assets can be cached for faster repeat visits, while public navigation uses the network first and falls back to /offline/. The service worker never caches POST requests, credentials, CSRF tokens, administrator pages, Team pages, client portal pages, account pages, private documents, or private media. Private routes require a live connection.
I can install the PWA from the browser’s install prompt or address-bar install control. The application name is Grand Coast Construction, it opens at /, and it uses the existing Grand Coast logo and navy, blue, gold, and white color system.
During development, I use:
MEDIA_ROOT = website/media/
MEDIA_URL = /media/
Django serves local media while DEBUG is enabled.
The production storage backend can be enabled only when I explicitly configure the environment:
$env:USE_SUPABASE_STORAGE = "true"
$env:SUPABASE_S3_ENDPOINT = "https://<project>.storage.supabase.co/storage/v1/s3"
$env:SUPABASE_S3_ACCESS_KEY = "<access-key>"
$env:SUPABASE_S3_SECRET_KEY = "<secret-key>"
$env:SUPABASE_STORAGE_BUCKET = "<bucket-name>"
$env:SUPABASE_S3_REGION = "us-east-1"I keep these values out of source control. If neither Supabase storage flag is enabled, the application makes no Supabase storage request and uses local filesystem storage.
For the current scoped rollout, I enable only contact-form attachments with USE_SUPABASE_CONTACT_STORAGE=true. This keeps the default storage for project, client, employee, and administrator uploads unchanged. USE_SUPABASE_STORAGE is the broader switch for moving every Django FileField to Supabase once those workflows are ready. The public URL base is not required; contact files use the protected storage path and signed access behavior.
Contact-form attachments are stored under the contact-form/YYYY/MM/ prefix inside the configured bucket. Other upload fields keep their existing projects/... prefixes until their client, employee, and administrator sharing workflows are finalized.
Before a real deployment, I also configure:
$env:DJANGO_SECRET_KEY = "<long-random-production-secret>"
$env:DJANGO_DEBUG = "false"
$env:ALLOWED_HOSTS = "grandcoastconstruction.com,www.grandcoastconstruction.com"
$env:GCC_OWNER_COMMAND_CENTER_ENABLED = "true"
$env:GCC_TURNSTILE_ENABLED = "true"
$env:GCC_TURNSTILE_ALLOW_MISSING = "false"
$env:GCC_MOBILE_TURNSTILE_BYPASS_ENABLED = "false"I do not use the development secret key or DEBUG=true in production.
GCC_OWNER_COMMAND_CENTER_ENABLED is the independent rollout switch for the owner's default Operations landing page. Set it to false to keep the existing workspace overview as the temporary /dashboard/ fallback; /dashboard/overview/ remains available as the preserved overview route.
The construction execution loop is a separate pilot switch. It is disabled by default until staging validation is complete:
$env:GCC_EXECUTION_LOOP_ENABLED = "true"
$env:GCC_EXECUTION_LOOP_PROJECT_IDS = "<pilot-project-uuid>"
$env:GCC_EXECUTION_LOOP_USER_IDS = "<owner-uuid>,<manager-uuid>,<field-uuid>"
$env:GCC_AI_ENABLED = "false"When either allowlist is non-empty, a user or project must be explicitly listed before the enhanced Project Operations controls, weekly action list, and derived calendar signals are available. The existing project page and routes remain the fallback when the execution-loop flag is off.
For production security-alert delivery, I also set the existing Django SMTP settings and the alert controls:
$env:DEFAULT_FROM_EMAIL = '[email protected]'
$env:EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
$env:EMAIL_HOST = 'smtp.example.com'
$env:EMAIL_PORT = '587'
$env:EMAIL_HOST_USER = '<smtp-user>'
$env:EMAIL_HOST_PASSWORD = '<smtp-password>'
$env:EMAIL_USE_TLS = 'true'
$env:EMAIL_USE_SSL = 'false'
$env:ADMIN_SECURITY_EMAIL_ALERTS_ENABLED = 'true'
# Only when a known reverse proxy is in front of Django:
$env:ADMIN_TRUSTED_PROXY_IPS = '203.0.113.10'DEFAULT_FROM_EMAIL and EMAIL_HOST/EMAIL_PORT/EMAIL_HOST_USER/EMAIL_HOST_PASSWORD must be configured for delivery. I keep SMTP credentials out of source control. I set ADMIN_TRUSTED_PROXY_IPS only to actual proxy addresses, never to a broad network range.
I run Django's system checks:
.\venv\Scripts\python.exe manage.py checkI confirm that no model changes are waiting for a migration:
.\venv\Scripts\python.exe manage.py makemigrations --check --dry-runI run the Operations test suite:
.\venv\Scripts\python.exe manage.py test operations --verbosity 1For a disposable full-lifecycle smoke test, I use the guarded launcher from the website directory:
.\run_operations_simulation.ps1The launcher creates a fresh SQLite database and media directory beneath the
operating-system temporary folder, migrates it, creates dummy role accounts,
and exercises the inquiry-to-warranty workflow with retries, permissions,
uploads, schedule conflicts, financial calculations, notifications, and
weekly review data. It prints generated pilot IDs and temporary credentials,
writes a sanitized JSON report, and removes the temporary data unless I pass
-Keep. It refuses to use the normal development database or media folder and
requires GCC_AI_ENABLED=false.
The current suite covers:
- Migration and seed behavior
- Idempotent Owner, Manager, Office, Sales, and Field groups
- Employee invitation creation, expiry, and one-time use
- Password setup and manager-issued reset links
- Owner-only Operations and Unfold access
- Manager, Office, Field, client, inactive, ungrouped, and anonymous access restrictions
- Role-aware Team calendar visibility and read-only employee calendar behavior
- Lead creation, assignment, status, priority, and client conversion
- Estimate creation, line-item Decimal totals, status transitions, and acceptance
- Accepted estimate to project conversion
- Task assignment, watcher access, status, priority, due dates, and activity
- Schedule event visibility
- Clock-in/out, duplicate active-entry prevention, corrections, and Pacific display
- Document upload validation and protected delivery
- Client-visible versus internal updates, documents, media, and messages
- Two-way client/staff messaging and unread behavior
- Public project pages and public-media filtering
- Google Review link visibility
- Public content updates
- Client Expo push registration, portal-only notification destinations, post-commit delivery, redacted payloads, duplicate prevention, and invalid-token deactivation
- Payment exclusion
- No business state stored in browser local storage
I expect the checks to finish with no system-check errors, no migration changes, and all Operations tests passing.
After starting the server, I test the application in this order.
- I open /.
- I click Services, Projects, Process, and Contact.
- I submit a test contact form.
- I verify that a new lead appears in /dashboard/leads/ as the Owner.
- I resize the browser to a mobile width and confirm there is no horizontal overflow.
- I log in at /accounts/login/.
- I open /dashboard/ as the Owner.
- I confirm the fixed navigation and staff account area remain visible.
- I open Leads, Clients, Tasks, Estimates, Projects, Calendar, Time, Documents, Media, and Content.
- I assign a lead and change its status.
- I convert the lead into a client.
- I create an estimate and edit its line items.
- I send the estimate.
- I create or use a client invite.
- I create a project after the estimate is accepted.
- I assign an employee and toggle a milestone.
- I add a project update and choose whether it is client-visible.
- I upload an internal document and a client-visible document.
- I add a task and schedule event.
- I review messages and reply to a client.
- I create an employee invitation from /dashboard/team/.
- I copy the generated link.
- I open the link in a private browser window.
- I create the employee username and password.
- I sign in and verify that the employee lands in /team/.
- I confirm the employee sees only assigned projects, tasks, schedules, and time entries.
- I update a task.
- I add an internal project update.
- I clock in and clock out.
- I confirm that the employee cannot open /dashboard/, /gccad/, or another employee's information, and cannot create or edit schedules.
- I create or use a client invite from /dashboard/clients/.
- I copy the invite link.
- I open it in a separate private browser window.
- I create the client's login.
- I verify that the client lands in /portal/.
- I review the estimate and accept it.
- I verify that no payment screen or payment request appears.
- I check project status, milestones, client-visible updates, media, and documents.
- I send a message to staff.
- I verify that the client cannot view another client's project or internal records.
- I open /gccad/ as the Owner.
- I confirm the Grand Coast logo and colors.
- I use the admin search for a known lead, project, estimate, or client.
- I click the result.
- I verify that it opens the relevant Operations dashboard or Team page.
- I open the native user and group pages when I need low-level account administration.
I check at least one desktop size and one narrow mobile size. I verify:
- The navy sidebar remains usable.
- The staff account section stays visible in the Operations navigation.
- Tables and split panes do not create accidental page-wide horizontal scrolling.
- The mobile navigation can collapse and reopen.
- Forms remain readable and submit buttons remain reachable.
- The client portal remains usable as a future mobile webview.
- Public galleries and project fallback artwork scale correctly.
- The Unfold login, home, list, and form pages do not clip.
I verify that:
- Anonymous visitors can view public pages but not staff or client workspaces.
- Only the active Owner superuser can access /dashboard/ and /gccad/.
- Managers, Office, and Field employees can access only the permitted /team/ records.
- Clients cannot access /dashboard/, /team/, or /gccad/.
- Employees cannot access another employee's schedule, access controls, assignments, or private records.
- Public pages expose no Operations or Client Portal destination links.
- Managers can view the full Team calendar but cannot create or edit its schedule events.
- Office and Field employees see only their personally assigned calendar events.
- Internal project updates, media, and documents are protected.
- Client-only files require authentication and project ownership.
- Invite links are hashed, expire, and cannot be reused.
- Forms use CSRF protection.
- Administration security events are visible only to active staff superusers, and security block/review actions are CSRF-protected and audit-logged.
- Failed administration authentication attempts capture no credential secrets and alert the configured administrator email recipients when enabled.
- Administration IP blocks and administrator-login locks are reversible and do not affect employee, client, or public routes.
- Forwarded client-IP headers are ignored unless the connecting proxy address is listed in
ADMIN_TRUSTED_PROXY_IPS. - Estimate totals are recalculated server-side.
- Accepted estimates cannot be edited in place.
- No business data is stored in localStorage.
- No Stripe, payment, webhook, or card request is made.
- No Supabase request is made unless I explicitly enable production storage settings.
I keep the current GoDaddy website untouched while I review this Django application with the owner.
Before deployment, I would:
- Set a strong production DJANGO_SECRET_KEY.
- Turn DJANGO_DEBUG off.
- Set the correct ALLOWED_HOSTS.
- Configure the production database.
- Decide whether to enable Supabase S3-compatible storage.
- Configure HTTPS and secure cookie settings.
- Run collectstatic.
- Create real staff accounts and remove any local presentation accounts.
- Upload real project photography and documents.
- Confirm the owner approves the content, roles, workflows, and client-facing language.
- Deploy the Django application separately from the existing GoDaddy site.
- Keep payment processing deferred until the business requirements are discussed and approved.
For static assets in a deployment environment, I run:
.\venv\Scripts\python.exe manage.py collectstatic --noinputThis is now a real backend-powered construction-management foundation, but it is still intentionally focused on the agreed first phase:
- Local SQLite for development
- Local filesystem media by default
- Optional Supabase S3-compatible storage configuration
- Manual copying of onboarding invites and manager-issued reset links
- Internal calendar, recurring employee scheduling, and employee notification inbox
- Informational estimate deposits
- No payment collection
- No contact-form email notifications
- No external calendar synchronization
- No real construction photography added yet
- No payroll or compliance timekeeping
That boundary lets me demonstrate the complete Grand Coast workflow now while keeping the application ready for the owner's decisions about production hosting, communications, photography, documents, payments, and future integrations.
The non-AI code path is now covered by the full Operations suite and the disposable simulation. Before production rollout, I still need staging validation, backup review, a selected real-project pilot, and real iOS simulator/device testing of camera, photo-library, document-picker, interrupted-upload, and offline-retry behavior. Production also requires the usual HTTPS, secrets, object-storage, database, backup, and deployment safeguards.
The current Operations suite has 164 passing tests. AI remains disabled until the real staging pilot completes successfully.
The repository includes a disposable certification runner for the complete non-AI Operations workflow. It is intentionally separate from the normal development database and media directory. It creates temporary users and records, runs the inquiry-to-warranty lifecycle, checks permissions and financial redaction, exercises protected uploads, and writes a JSON report.
Certification must never use production credentials, production databases, production storage, real client documents, or real client contact information. The runner refuses to operate unless simulation mode, the execution loop, the storage smoke gate, and GCC_AI_ENABLED=false are all set.
I need:
- Python dependencies installed in website/venv.
- Django migrations available locally.
- PowerShell 5.1 or newer.
- Docker Desktop with a local S3-compatible emulator for the Emulator and Both modes.
- Provider credentials for a separate private development bucket for Provider and Both modes.
- Node.js/npm for browser smoke tests.
- A browser installed and available to Playwright CLI for the Browser option.
The application itself can run with local SQLite and local media, but the release certification gate uses both storage targets. The local emulator is deterministic and the real development bucket catches provider-specific behavior.
The launcher defaults to MinIO at 127.0.0.1:9000 with the following development-only values:
docker run --name gcc-certification-minio -p 9000:9000 -p 9001:9001 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /data --console-address ":9001"The launcher creates the gcc-certification bucket when it does not exist and uses a unique certification/run-id/ object prefix. I stop and remove this container only after confirming that no other local project uses it:
docker stop gcc-certification-minio
docker rm gcc-certification-minioDo not point the emulator variables at a shared or production endpoint.
The provider target must be a private, development-only S3-compatible bucket. The access key must be limited to that bucket and must not be a production service key. Set these values only in the current PowerShell process, a local secret manager, or an ignored environment file:
$env:GCC_STORAGE_SMOKE_ENABLED = "true"
$env:USE_SUPABASE_STORAGE = "true"
$env:SUPABASE_S3_ENDPOINT = "https://<development-s3-endpoint>"
$env:SUPABASE_S3_ACCESS_KEY = "<development-only-key>"
$env:SUPABASE_S3_SECRET_KEY = "<development-only-secret>"
$env:SUPABASE_STORAGE_BUCKET = "gcc-development-private"
$env:SUPABASE_S3_REGION = "<development-region>"The bucket must reject anonymous reads. The certification suite checks that an unsigned object URL cannot retrieve a file and that Django's protected delivery view remains the authorization boundary.
The certification runner also sets:
GCC_STORAGE_PREFIX=certification/<run-id>/<mode>
The prefix is validated against traversal and unsafe characters, and is removed during cleanup unless -Keep is supplied. Do not manually substitute a production bucket or a production prefix.
From the repository root:
cd website
.\run_operations_certification.ps1 -StorageMode EmulatorThe final local gate uses both storage targets:
.\run_operations_certification.ps1 -StorageMode BothTo retain the disposable database, media, report, and browser artifacts for inspection:
.\run_operations_certification.ps1 -StorageMode Both -Browser -KeepThe command prints the retained temporary root and generated pilot IDs. It does not print passwords, cookies, session values, storage secrets, or API credentials. The report removes secret-shaped fields before it is written.
The runner creates these disposable identities:
Owner, Office, Project Manager, Sales, Field, Subcontractor, Client,
Unauthorized user
The scenario covers:
Inquiry -> Lead -> Site Visit -> Estimate -> Agreement -> Deposit ->
Readiness -> Construction -> Field Reports -> Media -> Inspection failure
and correction -> Selection -> Procurement -> Change Order -> Milestone ->
Payment -> Closeout -> Warranty
It also checks retry idempotency, stale updates, calendar conflicts, notification failure isolation, protected files, direct URL/API boundaries, financial redaction, command-center attention items, and weekly action-list data.
The browser runner uses Playwright CLI rather than a test framework. It logs in with the generated disposable accounts and checks the command center, project Operations hub, weekly review, calendar, field view, client portal, financial redaction, protected documents, and direct unauthorized URLs.
Run it through the certification launcher:
.\run_operations_certification.ps1 -StorageMode Emulator -Browser -KeepIf a browser is missing, install a browser supported by the local Playwright CLI and rerun the command. The launcher does not silently download a browser or weaken the test when none is installed.
The browser target is localhost by default. A remote target is rejected unless the explicit staging guard is present:
$env:GCC_CERTIFICATION_TARGET = "staging"
.\run_browser_smoke.ps1 -BaseUrl "https://<staging-domain>" -ProjectId "<generated-staging-project-uuid>" -ReportPath "$env:TEMP\gcc-browser-report.json" -ArtifactDirectory "$env:TEMP\gcc-browser-artifacts" -AllowRemoteThe remote run must use staging-only accounts. Never pass a production URL, production cookie, or production credential to this command.
| Role | Must be able to use | Must not be able to use |
|---|---|---|
| Owner | Command Center, all assigned company operations, financials, weekly review | Nothing outside the owner�s authorized company scope |
| Office | Leads, clients, messages, scheduling, documents, permitted payment records | Restricted internal financial details outside its policy |
| Project Manager | Assigned project Operations, readiness, schedules, field activity, budgets, subcontractors | Unassigned projects and unrelated financial records |
| Sales | Leads, appointments, estimates, follow-ups | Project costs, margins, internal notes, vendor details |
| Field | Today�s work, scope, tasks, plans, photos, daily reports, materials, problems | Financials, margins, internal vendor data, unrelated projects |
| Subcontractor | Assigned work package and explicitly shared project records | Other projects, client-private records, financials, internal notes |
| Client | Own project progress, decisions, selections, documents, change orders, payment schedule | Internal costs, margins, vendor details, internal notes, other projects |
| Unauthorized | Publicly permitted pages only | Operations, client portal, project APIs, documents, and media |
Each role is tested both through visible navigation and through direct URLs/API requests. Hiding a link is not considered authorization.
Native media is a thin capability bridge around the Django WebView workflow; there is no SwiftUI screen architecture. Use a retained local certification run and a disposable Field account.
- Start the retained server on the LAN address, not only 127.0.0.1:
$env:GCC_SIMULATION_MODE = "true"
$env:GCC_AI_ENABLED = "false"
$env:GCC_NATIVE_MEDIA_ENABLED = "true"
$env:GCC_DATABASE_PATH = "<retained-certification-root>\db.sqlite3"
$env:GCC_MEDIA_ROOT = "<retained-certification-root>\media"
$env:DJANGO_DEBUG = "true"
.\venv\Scripts\python.exe manage.py runserver 0.0.0.0:8001- Open the LAN URL in the existing Expo WebView build or use a temporary HTTPS tunnel that points only to this disposable server.
- Log in as the generated Field user.
- Verify camera photo capture, photo-library selection, video capture, and document picker.
- Cancel each picker and confirm the page remains usable.
- Try an invalid extension, oversized file, unsafe filename, and an upload to an unrelated project. Each must be rejected server-side.
- Interrupt the network during upload, reconnect, and confirm the private retry queue submits exactly one authorized attachment.
- Confirm progress, retry, and failure states are visible.
- Confirm the app does not persist passwords, session secrets, recovery tokens, or API credentials in plaintext storage.
- Confirm the client, subcontractor, and unrelated Field accounts cannot retrieve the uploaded file.
The physical iPhone is authoritative for camera and video behavior. Desktop and mobile-web tests prove server authorization and responsive behavior but do not replace the device pass.
Before any staging migration:
- Make a timestamped database backup.
- Record the migration number and application commit.
- Copy the staging media/object-storage backup or verify the provider�s versioned backup policy.
- Restore into a separate disposable database and storage prefix.
- Run Django checks, migration checks, protected-file checks, and the certification read-only verification against the restore.
- Record the restore time, row/file counts, and any missing object.
A backup is not accepted merely because it exists; the restore must be readable and the protected delivery rules must still hold.
Staging is the deployment-infrastructure rehearsal, not the first functional test. I create separate staging resources:
- Staging database
- Private staging object-storage bucket
- Staging-only storage credentials
- Email sink or test mail provider
- Staging-only push configuration
- HTTPS domain and trusted host configuration
- Selected pilot users and one pilot project
I keep AI disabled:
$env:GCC_AI_ENABLED = "false"
$env:GCC_EXECUTION_LOOP_ENABLED = "true"
$env:GCC_EXECUTION_LOOP_PROJECT_IDS = "<staging-pilot-project-uuid>"
$env:GCC_EXECUTION_LOOP_USER_IDS = "<owner>,<manager>,<office>,<field>,<client>"After a verified backup, I run:
.\venv\Scripts\python.exe manage.py check --deploy
.\venv\Scripts\python.exe manage.py makemigrations --check --dry-run
.\venv\Scripts\python.exe manage.py migrate --plan
.\venv\Scripts\python.exe manage.py collectstatic --noinputI then run the same certification workflow against the staging deployment. The remote browser run requires GCC_CERTIFICATION_TARGET=staging and -AllowRemote. I verify HTTPS, secure cookies, CSRF, signed/protected storage delivery, email links, push notifications, audit events, authorization failures, failed deliveries, logs, backup, and restore before expanding the pilot.
The staging pilot sequence is:
- Generate or seed only disposable staging records.
- Allowlist one project and selected Owner, Office, Project Manager, Field, Subcontractor, and Client users.
- Run the lifecycle from inquiry through warranty.
- Review the JSON report, audit history, notifications, storage cleanup, financial calculations, and authorization failures.
- Fix and rerun any failed check.
- Expand to additional staging projects only after the pilot is clean.
No certification command accepts a production URL. Production requires a separate release decision and a fresh backup/restore verification.
The non-AI release gate is complete only when all of these have evidence:
Django checks
Migration dry-run
Full Operations test suite
Full lifecycle simulation
Permission matrix
Private storage emulator tests
Real development-bucket smoke test
Browser role matrix
Financial and calendar tests
Notification failure isolation
Protected-file tests
Mobile dependency checks
iOS media smoke checklist
Backup and restore verification
Staging pilot report
The certification report must show:
- All checks passed.
- No secret leakage.
- No duplicate lifecycle records.
- No cross-project access.
- No financial redaction failures.
- No protected-file bypass.
- Storage cleanup completed, or the retained path is recorded.
- Browser artifacts generated when browser mode was requested.
- Temporary database, media, and emulator prefix removed unless -Keep was explicitly supplied.
Artifact locations:
- Disposable certification root: the path printed by the runner when -Keep is supplied.
- JSON lifecycle report: certification-report.json in that root.
- Browser report: browser-report.json in that root.
- Browser screenshots and CLI artifacts: the browser-artifacts directory in that root.
Troubleshooting:
- Missing browser: install a local supported browser, then rerun with -Browser. Do not mark browser certification passed without artifacts.
- Missing MinIO/Docker: start the local emulator or run a Provider-only development-bucket smoke test; the Both gate still requires both targets.
- Missing provider credentials: confirm the endpoint, bucket, region, and bucket-scoped development key in the current process. Never copy production secrets.
- Upload failures: inspect the sanitized report and Django audit events; check content signature, size, protected delivery, prefix, and bucket policy.
- Retained temporary data: inspect the printed root, then remove that exact generated directory after review. Do not use a wildcard or a workspace root for cleanup.
- No remote browser access: confirm GCC_CERTIFICATION_TARGET=staging and use -AllowRemote only with a staging HTTPS URL.
AI remains disabled for every development, certification, and staging run.
The following accounts are seeded only in the disposable manual-certification database. They are intended to make the complete manual workflow testable without manually creating employees or accepting invitation links.
These credentials are fake, temporary, and must never be used in staging, production, or with real client information.
| Name | Role | Login email / username | Password | Login location |
|---|---|---|---|---|
| Manual Owner | Owner / Admin | [email protected] |
GccManual-Owner-2026! |
/gccad/ |
| Mike Rivera | Manager | [email protected] |
GccManual-Manager-2026! |
/accounts/login/ |
| Olivia Bennett | Office | [email protected] |
GccManual-Office-2026! |
/accounts/login/ |
| Ethan Cole | Field | [email protected] |
GccManual-Field-2026! |
/accounts/login/ |
| Sam Lopez / Harbor Electrical LLC | Subcontractor | [email protected] |
GccManual-Subcontractor-2026! |
/accounts/login/ |
| Taylor Unassigned | Unassigned Field test user | [email protected] |
GccManual-Unassigned-2026! |
/accounts/login/ |
The Owner account is a superuser and enters through the protected administration
gate at /gccad/. Managers, Office, Field, Clients, and Subcontractors use
the normal account login flow. The [email protected] account is an
active Field user intentionally left unassigned for project-boundary tests.
The temporary server is available at:
http://127.0.0.1:8001/
The disposable database is:
C:\Users\isaia\AppData\Local\Temp\gcc-manual-c98a3fc4e2864860ab697369c4484dbe\db.sqlite3
The database is configured with:
- AI disabled (
GCC_AI_ENABLED=false). - Execution-loop testing enabled.
- Local email and push delivery disabled.
- No pending employee invitations.
- Only Owner, Manager, Office, and Field internal roles.
- An active Harbor Electrical LLC subcontractor portal identity.
The clean workflow state is:
Leads: 0
Estimates: 0
Projects: 0
Tasks: 0
Site visits: 0
Pending invites: 0
Clients: 0
The client portal identity is intentionally not pre-seeded. Create a dummy Client record through the Operations workspace, create its client portal invite, and complete that invite with a temporary password. Use the same client email for the website inquiry so lead conversion can reuse the client record instead of creating a duplicate.
- Open
/gccad/and sign in as[email protected]. - Open the Operations Command Center.
- Create a dummy Client record and client portal invite through Operations.
- Complete the client invite in a separate browser profile and establish a temporary client password.
- Submit a website inquiry using that same client email.
- Convert the lead and create the estimate, agreement, deposit, and project.
- In Project Operations, assign:
[email protected]as Project Manager.[email protected]as Office staff.[email protected]as Field staff.
- Log in separately as each employee and verify their scoped workspace.
- Use the Client account to approve selections, change orders, and review shared project information.
- Complete readiness, construction, field reports, inspections, payments, closeout, and warranty testing.
Refresh or sign out and back in if a browser session was open before the
accounts were seeded. The temporary credentials and database are disposable;
delete the exact generated gcc-manual-* directory after testing and never
delete a broad temporary or workspace directory.
Grand Coast does not connect to or synchronize with a third-party estimate service. That service remains responsible for detailed estimates, signatures, invoices, payment collection, reminders, and its own customer notifications. Grand Coast stores only the client, a protected estimate link, a lightweight status, and the confirmation history needed to continue its own workflow.
The estimate link/status layer is automatic. No estimate-specific environment variables, pilot IDs, or allowlists are required. Once an authorized staff member creates an estimate record, the estimate link and status actions are available according to the normal server-side permissions.
AI remains disabled during this phase:
$env:GCC_AI_ENABLED = "false"The normal flow is: create the client and lead; open Estimates in Operations; enter only the client and the secure estimate link; then either send the link through your usual email/text process, use the optional "Send link to client" action in GCC, or publish it to the client portal. Update the lightweight status in GCC when the client responds, then continue the agreement, deposit, project, construction, portal-update, and closeout workflows. Do not enter provider credentials, payment credentials, API keys, or detailed estimate line items into GCC.
Owner, Manager, and Office users can change the manual status. Field users and clients can see only the permitted lightweight status surface. Clients can open an estimate link only after staff explicitly publishes that link. Invoice and payment links are handled the same way: GCC records only links and confirmed status, while the detailed commercial document remains in the selected service.
The public homepage can show up to five cached Google Places reviews in the section immediately below the hero. Google supplies the returned relevance order; GCC does not rewrite review text. The existing review-link CTA remains available separately.
Keep the Google Places credentials only in website/.env:
GCC_GOOGLE_PLACES_API_KEY=your_server_side_key
GCC_GOOGLE_PLACE_ID=your_grand_coast_place_id
GCC_GOOGLE_PLACES_TIMEOUT_SECONDS=8The API key is read only by Django. It is never placed in HTML, browser JavaScript, the mobile environment, or database content. The Owner opens Content Studio and selects “Sync Google reviews” after the values are configured. A successful sync replaces the cache and records the last-sync time; a failed request leaves the last successful reviews visible. If no cache exists, the carousel stays hidden and the existing Google review link is still available.
Run the migration and an initial sync from the website directory:
.\venv\Scripts\python.exe manage.py migrate
.\venv\Scripts\python.exe manage.py sync_google_reviewsSchedule the sync command daily in the deployment environment. The Places API returns a maximum of five reviews per request, and each displayed card includes Google attribution plus a direct “Read on Google” link.
The existing admin/superuser account can optionally use the Expo WebView as the Owner
workspace. This does not create a new Owner role, does not expose the /gccad/ admin
catalog in the app, and does not change normal browser login behavior.
Enable it only for a development or staging test session:
$env:GCC_MOBILE_OWNER_ACCESS_ENABLED = "true"
$env:GCC_MOBILE_OWNER_PUSH_ENABLED = "true"
$env:GCC_AI_ENABLED = "false"The mobile app opens /accounts/login/?mobile=1. The owner must still complete the
existing administrator PIN and authenticator verification steps when those protections
are enabled. Only after the factors succeed is the Django session created, and the app
lands at /dashboard/ (the Command Center). The app redirects any /gccad/ navigation
back to /dashboard/; browser access to /gccad/ remains available through its existing
protected administration gate.
The mobile owner flags are disabled by default. Keep the server-side flags authoritative; the optional mobile setting below only controls whether the native shell attempts owner push registration before the server accepts it:
EXPO_PUBLIC_MOBILE_OWNER_PUSH_ENABLED=trueOwner push registration is accepted only for a marked Grand Coast mobile WebView while
GCC_MOBILE_OWNER_PUSH_ENABLED=true. Push links are permission-checked Operations paths
and never open /gccad/. Logout deactivates the registered device. Passwords, PINs,
authenticator codes, session cookies, recovery tokens, and push credentials are not
stored in native plaintext storage or written to logs.
- Leave
GCC_MOBILE_OWNER_ACCESS_ENABLED=falseand verify the superuser is rejected by the mobile login form. - Enable the access flag, sign in through the Expo app, and verify the owner lands on the Command Center.
- If enabled, verify the PIN page appears before a session exists; then verify the authenticator page appears before a session exists.
- Open the app drawer and verify Operations links work while
/gccad/redirects to the Command Center. - Enable owner push, register a device, open an Operations notification, and verify the
destination is internal and not
/gccad/. - Log out and verify the device is deactivated.
- Repeat the same checks with the flags disabled and confirm employee and client login behavior is unchanged.
Do not enable these flags in production until HTTPS, secure cookies, Turnstile, PIN/TOTP,
push credentials, audit events, and logout behavior have been verified in staging. The
same superuser credentials can still access /gccad/ in a browser by design; restricting
one credential cryptographically to only one app would require a separate scoped account
or device-attestation system.