EV Charging is a lightweight internal web app for managing EV charger usage and reservations at Company.
- Charge now: view chargers, start a session, end session.
- Reserve: book next available slots and manage upcoming reservations.
- Check-in: check in near the reservation start to auto-start charging.
- Now (default): operational view for starting a session immediately.
- Reserve: shows next available slots and the user’s reservations (listed above available slots for faster access).
The web app can serve multiple UI versions from the same Apps Script project.
Routing order (first match wins):
- URL override:
?v=2loads v2. - Allowlist:
ui_v2_allowlistcontains the user email. - Global default:
ui_versionisv2. - Fallback: v1.
Config keys (set in the config sheet):
ui_version:v1orv2.ui_v2_allowlist: comma-separated emails.
Use this after pushing v2 UI changes.
- Open the web app URL with
?v=2(or add yourself toui_v2_allowlist). - Confirm each charger card shows a single primary action button.
- Verify the availability summary block shows the correct state:
- Free: “Available now” with walk‑up timing if applicable.
- In use/overdue: shows end time.
- Reserved: shows start time and whether it’s your reservation.
- On mobile width, verify:
- Buttons are easy to tap (≥ 48px height).
- Card content is legible without zoom.
- Bottom nav bar background is white; the active tab is dark charcoal (not orange). Orange is reserved for action buttons only.
- Inactive tabs are light gray with dark text — visually distinct from both the active tab and action buttons.
- Verify walk‑up countdown updates for free chargers.
- Confirm selecting a card still highlights it and reveals details.
- Open the Reserve tab with v2 enabled.
- Confirm layout shows two panels: “My reservation” and “Available today”.
- Verify slot groups display by time window (“Next 2 hours”, “Later today”, “Tonight”).
- Ensure each slot row shows time range, charger name, and a single “Reserve” action.
- On mobile width, ensure panels stack in a single column (no side-by-side shrinking).
- On mobile width, ensure the reserve action is full-width and easy to tap.
Mobile (90%+ of users) uses a bottom tab bar and a sticky action bar for primary actions. The bottom tab bar uses a dark charcoal active state (#1d2939) to visually separate navigation from the orange CTA buttons used throughout the content. Additional mobile features:
- My Status Banner: always-visible strip above the board showing the user’s active session (with "I’ve moved my car"), check-in-eligible reservation (with "Check in"), or upcoming reservation.
- Compact header: single-line title + refresh icon; subtitle/helper text hidden to save vertical space.
- Bottom-sheet confirm dialog: action confirmations slide up from the bottom on touch devices.
- Skeleton loading: placeholder cards shown on first load before data arrives.
- Auto-refresh: board reloads automatically if the tab was backgrounded for more than 60 seconds.
- Notice auto-dismiss: success and info toasts clear after 4 seconds; errors persist.
- Walk-up priority labels: user-outcome language ("You’re eligible" / "Opens to all at…") replaces internal vocabulary.
Tabs created by initSheets():
chargers: charger configuration and active session reference.sessions: active/complete charging sessions.reservations: upcoming reservations, check-in, and no-show data.config: key/value settings.strikes: per-user no-show strike records.suspensions: temporary bans issued when a user exceeds the strike threshold.
charger_id: unique identifier (string/number).name: display name shown in the UI.max_minutes: maximum session length for this charger.active_session_id: current active session ID (blank if free).
session_id: unique session ID (UUID).charger_id: charger used for the session.user_id: user email of the driver.user_name: user display name.start_time: session start timestamp.end_time: session end timestamp (computed).status:active,overdue, orcomplete.active: boolean flag for active session.overdue: boolean flag for overdue session.complete: boolean flag for completed session.reminder_10_sent: boolean for 10‑minute reminder.reminder_5_sent: boolean for 5‑minute reminder.reminder_0_sent: boolean for expiration reminder.overdue_last_sent_at: timestamp for last overdue reminder.ended_at: timestamp when session ended.
reservation_id: unique reservation ID (UUID).charger_id: charger reserved.user_id: user email of the reserver.user_name: user display name.start_time: reservation start timestamp (rounded up).end_time: reservation end timestamp (computed).status:active,checked_in,canceled, orno_show.checked_in_at: timestamp when user checked in (auto-starts session).no_show_at: timestamp when reservation was released.no_show_strike_at: timestamp when a no-show strike was recorded.reminder_5_before_sent: boolean for 5-minute pre-start reminder.reminder_5_after_sent: boolean for 5-minute post-start reminder.created_at: timestamp created.updated_at: last update timestamp.canceled_at: timestamp when canceled.
Key/value settings (strings). Common keys:
allowed_domain: Workspace domain allowed to access.admin_emails: comma-separated list of admin emails.overdue_repeat_minutes: repeat cadence for overdue reminders.session_move_grace_minutes: grace period (in minutes) after session end before overdue enforcement.slack_webhook_url: optional Slack webhook URL.slack_webhook_channel: optional Slack channel override.slack_bot_token: optional Slack bot token for DM sending.reservation_advance_days: max days ahead to reserve.reservation_max_upcoming: max upcoming reservations per user.reservation_max_per_day: max reservations per user per day.reservation_gap_minutes: minimum gap between reservations on same charger.reservation_rounding_minutes: rounding increment for reservation start times.reservation_checkin_early_minutes: earliest check-in window.reservation_early_start_minutes: how early a reserved slot can start if the charger is free.reservation_late_grace_minutes: no-show grace window.reservation_open_hour: earliest local hour for same-day booking.reservation_open_minute: earliest local minute for same-day booking.walkup_net_new_window_minutes: walk-up priority window for net-new users after a slot opens.walkup_returning_window_minutes: additional walk-up window for returning users before full open access.reminder_10_enabled: set totrueto enable the 10-minute session-end warning (default off).reminder_5_enabled: set totrueto enable the 5-minute session-end warning (default off).
- Same-day only.
- Max upcoming reservations per user is enforced (default 3).
- Max reservations per day is enforced (default 1).
- No overlapping or near-overlapping reservations on the same charger.
- Slot length equals charger
max_minutes. - Start times must match a configured slot start.
- Early start: if the charger is free, a user can start their reservation up to
reservation_early_start_minutesearly (default 90). - Prior reservation protection: early starts are blocked while a prior reservation is still within its no-show grace window.
- No-show: after
reservation_late_grace_minutes(default 30), an unused reservation is released and can receive a strike. - Checked-in reservations: ending a session will auto-mark the matching reservation as
complete. - If a checked-in reservation has no matching active session, the UI offers to clear the checked-in reservation.
- Walk-up sessions are limited by slot boundaries (not just the next reservation time).
- If a reservation exists for the current slot, walk-up opens after
reservation_late_grace_minutes. - Priority windows after a slot opens: net-new only for
walkup_net_new_window_minutes. - Then returning users for
walkup_returning_window_minutes. - Everyone after both windows elapse.
- Deploy the Apps Script web app to only your Workspace domain (e.g.,
example.com). - The server checks the active user’s email domain against
allowed_domainto restrict access. - Admin actions are gated by
admin_emailsand hidden in the UI by default.
This provides a lightweight SSO-like gate when used inside an Enterprise Google Workspace.
Never commit secrets or identifiers that grant access. Keep them in Script Properties or local .clasp.json only.
Do not commit:
- API keys or tokens
- Spreadsheet IDs or URLs
.clasp.json- OAuth credentials
Recommended practices:
- Keep
.clasp.jsonin.gitignore - Use
.envfiles locally if needed (and ignore them) - Review
git statusbefore pushing - Avoid debug endpoints that expose script IDs or sheet URLs
Slack DM or webhook for reminders and no-show notices, with email fallback if Slack is unavailable.
- Force end active sessions.
- Reset chargers.
Admin actions are hidden under overflow menus in the UI.
apps-script/Code.gs: server logic, reminders, reservations, availability, notifications.apps-script/index_v2.html: layout.apps-script/script_v2.html: UI logic.apps-script/styles_v2.html: styles.
flowchart LR
user[UserBrowser] --> webApp[AppsScriptWebApp]
webApp --> sheet[GoogleSheetStore]
webApp --> slack[SlackWebhook]
webApp --> reminders[TimeDrivenTrigger]
- Authenticate Workspace users and enforce domain access.
- Read/write Sheets data for chargers, sessions, and reservations.
- Compute availability and enforce reservation rules.
- Send reminder/notification messages via Slack or email fallback.
- User loads the web app (
doGet) and the UI fetches data withgetBoardData(). - Actions (start session, reserve, check-in) call server APIs via
google.script.run. - Server reads/writes Sheets and returns updated state for the UI to render.
- Background trigger runs
sendReminders()and releases no-show reservations.
Apps Script web app deployed within the Google Workspace domain. The reminder trigger runs sendReminders() periodically (recommended every 5 minutes).
Before any recommended push to production, run routine backend logic checks using the CLI to validate core flows (sessions, reservations, reminders).
The CLI runs the same backend rules as apps-script/Code.gs, backed by a local JSON store. This makes it easy to test reservation/session logic without Google Apps Script.
Quick start:
npm run cli -- seed
npm run cli -- board --user alice@example.comCommon commands:
npm run cli -- initnpm run cli -- seednpm run cli -- boardnpm run cli -- start-session <chargerId>npm run cli -- end-session <sessionId>npm run cli -- reserve <chargerId> <startTimeIso>npm run cli -- update-reservation <reservationId> <chargerId> <startTimeIso>npm run cli -- cancel-reservation <reservationId>npm run cli -- check-in <reservationId>npm run cli -- next-slotnpm run cli -- availabilitynpm run cli -- timeline <chargerId> [dateIso]npm run cli -- calendar [startDateIso] [days]npm run cli -- send-reminders
Defaults:
- Store path:
data/store.json(override with--store). - User email:
user@example.com(override with--user).
Recommended pre‑deploy logic check:
npm run cli -- seed
npm run cli -- boardPolicy logic check:
npm run policy-checkSee SETUP.md for step-by-step setup and configuration.