# Drink Calendar

A static drink and sober-day tracker for GitHub Pages, with an iOS-inspired
liquid-glass interface. This instance is configured for `willzjc@gmail.com` and
creates a new private spreadsheet in that account's Google Drive on first use.
There is no shared database, application server, analytics, or service account.

The account email is configuration, not authorization: **no sheet has been
created until Google OAuth is configured and you grant access.**

## Interface

The responsive glass panels, floating navigation, system typography, and safe
area spacing are designed for iPhone-sized screens as well as desktop browsers.
The interface follows the device's light/dark preference, respects reduced
motion and reduced transparency, and has an opaque fallback without backdrop
filter support. The four bottom navigation links scroll to real app sections.

On iOS, use Safari's **Share > Add to Home Screen** for an app-style standalone
window. This is still a web app, not a native iOS application. The manifest uses
relative URLs so repository-based GitHub Pages sites work. There is no service
worker, offline cache, or offline write queue.

## Run locally

Requires Node.js 22 or later. There are no npm dependencies to install.

```sh
npm start
```

Open <http://127.0.0.1:4173>. The calendar is visible before setup, but connecting
and saving require the Google configuration below. `file://` URLs are not
supported for Google authorization or JavaScript modules.

## One-time Google configuration

1. Create a project in [Google Cloud Console](https://console.cloud.google.com/).
2. Enable **Google Drive API** and **Google Sheets API** for that project.
3. In **Google Auth Platform**, configure the app's branding, audience, and
   contact details. Use an External audience for personal Google accounts.
4. Add only `https://www.googleapis.com/auth/drive.file` under Data Access.
   This is Google's recommended non-sensitive, per-file scope; the app does not
   ask to read all of your Drive or all your spreadsheets.
5. While the app is in Testing, add `willzjc@gmail.com` as a test user.
   Before offering it to everyone, configure the Production audience and
   complete any branding/domain checks or verification required by Google.
   Workspace administrators can also restrict third-party app access.
6. Create an OAuth client with the **Web application** type. Set **Authorized
   JavaScript origins** to the exact origins you will use, for example:
   - `http://127.0.0.1:4173`
   - `http://localhost:4173` if you also use that hostname
   - `https://YOUR-USERNAME.github.io`
   - Your custom domain's HTTPS origin, if applicable
7. Set the public client ID in [config.js](./config.js):

   ```js
   export const GOOGLE_CLIENT_ID = "YOUR-ID.apps.googleusercontent.com";
   ```

8. Reload and choose **Connect Google**. Sign in with `willzjc@gmail.com` and
   grant the requested file permission. First use creates the spreadsheet;
   later connections reopen it.

`GOOGLE_ACCOUNT_EMAIL` in [config.js](./config.js) supplies Google's login hint.
The app checks the account Google returns **before any spreadsheet or folder
lookup/creation** and rejects a different email. Set it to an empty string if
you later want the original multi-account mode, or change it to another email
for a different personal instance. A pinned account shows **Reconnect** instead
of **Switch account**.

Both this email and the OAuth client ID are public when the site or source is
public. The email check prevents accidental use of the wrong account; it is
not an access-control boundary against someone modifying a static app. Google
OAuth and the spreadsheet's Drive permissions protect the data.

Use an origin, not a repository URL with a path: for a site at
`https://name.github.io/drink-calendar/`, register `https://name.github.io`.
This app uses Google's popup token flow; it does not need a server callback or
client secret. Never add a client secret, service-account key, access token, or
refresh token to this project or to GitHub.

The client ID is public configuration, not a password. If Google refuses an
origin or account, check your OAuth client, test-user list, consent settings,
browser popup settings, and any Workspace administrator restrictions.

## GitHub Pages

1. Put this directory's contents at the root of a GitHub repository.
2. Configure the public OAuth client ID as above.
3. In the repository's **Settings > Pages**, select **GitHub Actions** as the
   source.
4. Push to `main` or `master`, or run **Test and deploy to GitHub Pages** manually.

The [workflow](./.github/workflows/pages.yml) checks JavaScript, runs the tests
in UTC and Australia/Sydney, and deploys only the static app and setup guide.
Pull requests run checks without deploying. All asset paths are relative, so
project sites under a repository path work without a build step.

## Where the data lives

```text
My Drive/
  data/                 # folder created by this app
    drink-calendar      # native Google spreadsheet; no file extension
      Entries           # worksheet
```

This is a Google Drive folder, **not** a local `~/data` directory. A `.gsheet`
file on a desktop is only a shortcut; an `.xlsx` file is an export.

The app labels its folder and spreadsheet using private Drive `appProperties`.
It rediscovers the same file after reloads, on other devices, and if you rename
or move the spreadsheet. Keep using the same Google Cloud application: file
permissions and private application properties are tied to it.

With `drive.file`, unrelated existing folders called `data` are not
automatically accessible. The app creates and reuses its own folder; another
folder with the same visible name may already exist. It does not request wider
Drive access to inspect your unrelated files.

The connected Google user owns the spreadsheet. No public link or sharing
permission is added. Google Workspace policies or your later sharing changes
can affect access; check the spreadsheet's sharing settings if needed.

## Logging and statistics

- Each date has one **daily total**, measured in standard drinks, with an
  optional note. Counts support one decimal place. An Australian standard drink
  contains 10 g of alcohol; serving sizes are not automatically converted.
- Save `0` to explicitly confirm an alcohol-free day. An unlogged day is
  unknown, never automatically counted as sober.
- The current streak counts consecutive confirmed zero-drink days ending
  today, or yesterday if today is still unlogged. An unlogged intervening day
  or a nonzero entry breaks the streak.
- Other totals refer to the displayed calendar month. Dates use the device's
  local calendar day rather than converting midnight to UTC. Future dates
  cannot be logged.
- Tap a calendar day to open a liquid-glass check-in dialog. Its saved outcome
  distinguishes **Alcohol-free**, **Drank**, and **Not logged**. Before connecting,
  the app shows **Not loaded**, rather than inventing an outcome.
- Choose **Alcohol-free** to set zero, or **Drank** and enter the daily total.
  Save changes in the dialog. Notes, drink totals, and saved outcome are all
  available there; Escape or the close button closes the dialog.
- **Select days** switches the calendar into multi-select mode. Tap dates to
  select/deselect them, navigate months without losing the selection, or use
  **Select month** to add all non-future dates in the displayed month.
- **Edit selected** opens a second glass dialog with the exact selected dates.
  Apply **Alcohol-free**, **Drank** with the same per-day drink count, or **Not
  logged**. A bulk total replaces each day's total; it is not added to it.
  The app refreshes before a bulk write to preserve each day's latest note,
  then appends all selected changes in one request. Concurrent writes still
  follow the last-appended-write-wins rule below.
- **Clear** removes the selection; **Done selecting** exits selection mode.
  Selection is also cleared on successful bulk saves and account/session reset.
- **Recent check-ins** shows the latest ten daily records. Its Edit buttons open
  the same daily dialog. The calendar can navigate older months.
- **Refresh** reloads the sheet without replacing unsaved form text. Select a
  day again to load its refreshed values into the form.

### Append-only storage

The `Entries` tab has six columns:

| event_id | date | drinks | notes | updated_at | operation |
|---|---|---|---|---|---|
| UUID | YYYY-MM-DD | number or blank | text | ISO timestamp | set or delete |

Each update appends a complete `set` event. Removing a check-in appends a
`delete` event and restores the day's unlogged state. **It does not erase earlier
rows or notes.** If you need to remove sensitive history, manage the source
spreadsheet and its Google Drive history directly.

For each date, the last appended event wins, using row order rather than device
timestamps. Identical duplicate event IDs are read idempotently. Notes are sent
using `valueInputOption=RAW` and rendered as text, never formulas or HTML.

Do not sort, reorder, rename, or remove the six source columns, or sort the
event rows: row order is significant. Use a separate tab for your own reports.
Corrupt headers and rows produce an explicit error instead of silently skipping
records or overwriting them.

## Sessions, privacy, and limitations

- Google access tokens and loaded records live only in memory. No records or
  tokens are stored in localStorage, cookies, or IndexedDB by this app.
  Google controls its own sign-in cookies.
- Tokens are short-lived. Reconnect when a session expires; there is no
  background access or stored refresh token.
- **Disconnect** clears the in-memory token, records, and form. It does not log
  you out of Google, revoke the grant, or delete your sheet. Revoke access in
  your [Google account connections](https://myaccount.google.com/connections)
  if desired.
- Switching accounts clears the previous account's local state before showing
  the account chooser. A canceled sign-in never restores the previous data.
- There is no offline queue. Failed writes are not automatically retried.
  If a save cannot be confirmed, the app keeps your form text and requires a
  successful refresh before another edit; a request might have reached Google
  even if the response was lost.
- Sheets has API quotas and is not a transactional database. Simultaneous
  changes to the same day use last-appended-write-wins, not automatic merging.
  First-time setup is serialized between tabs in browsers supporting Web
  Locks. Simultaneous first use on different devices can still create duplicate
  files: the app reports duplicates on reconnect instead of selecting one
  silently. Keep the intended file and move extra copies to Trash.
- This is designed for personal, low-volume use. It currently reads the full
  event log when opening, refreshing, or saving. Sheets API usage has no
  additional charge within Google's service limits; normal account storage
  and organizational policies still apply.
- Use **Open Google Sheet** to inspect or export your records. Keep your own
  backups. GitHub Pages hosts the application code, not your private records.
- Avoid deploying untrusted code under the same web origin: OAuth authorized
  origins are not restricted to one repository path.

This is a reflection tool, not medical advice. Stopping alcohol suddenly can
be dangerous for people who drink heavily or regularly; seek medical advice
before cutting down.

## Validation

```sh
npm run check
npm test
TZ=Australia/Sydney npm test
```

The dependency-free Node tests cover date boundaries, unknown versus sober
days, streaks, validation, event ordering/deletion, literal note storage, file
creation/recovery, pagination, sessions, API failures, unconfirmed writes,
personal-account checks, DOM/navigation targets, deployment assets, bulk
outcomes, per-day note preservation, and complete batch-write confirmations.
Google API calls in automated tests are mocked. To validate real authorization,
complete setup and connect a test account, save a zero-drink day and a nonzero
day, refresh/reload, switch accounts, and inspect the private sheet in Drive.

Official API references:

- [Browser token authorization](https://developers.google.com/identity/oauth2/web/guides/use-token-model)
- [Per-file Sheets authorization](https://developers.google.com/workspace/sheets/api/scopes)
- [Drive app properties and file search](https://developers.google.com/workspace/drive/api/guides/search-files)
