Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ VITE_WEEKLY_AI_ENDPOINT=
# GitHub secret WEEKLY_AI_TOKEN must match worker WEEKLY_SUMMARY_TOKEN
VITE_WEEKLY_AI_TOKEN=

# Optional Web Push (same Cloudflare Worker — see AGENTS.md)
VITE_VAPID_PUBLIC_KEY=
VITE_PUSH_API_BASE=

# Optional Firebase sync + share links (Spark free tier — see docs/FIREBASE.md)
VITE_ENABLE_FIREBASE=0
VITE_ENABLE_FIREBASE_SYNC=0
Expand Down
4 changes: 4 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
# VITE_WEEKLY_AI_ENDPOINT=http://127.0.0.1:8787/weekly-summary
# VITE_WEEKLY_AI_TOKEN="dev-local-token"

# Web Push (optional — same worker origin as weekly AI)
# VITE_VAPID_PUBLIC_KEY=<public key from npx web-push generate-vapid-keys>
# VITE_PUSH_API_BASE=http://127.0.0.1:8787

# Firebase (optional) — project men-tell-prod
# VITE_ENABLE_FIREBASE=1
# VITE_ENABLE_FIREBASE_SYNC=1
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
VITE_ENABLE_WEEKLY_AI_SUMMARY: ${{ vars.VITE_ENABLE_WEEKLY_AI_SUMMARY }}
VITE_WEEKLY_AI_ENDPOINT: ${{ vars.VITE_WEEKLY_AI_ENDPOINT }}
VITE_WEEKLY_AI_TOKEN: ${{ secrets.WEEKLY_AI_TOKEN }}
VITE_VAPID_PUBLIC_KEY: ${{ vars.VITE_VAPID_PUBLIC_KEY }}
VITE_PUSH_API_BASE: ${{ vars.VITE_PUSH_API_BASE }}
VITE_ENABLE_FIREBASE: ${{ vars.VITE_ENABLE_FIREBASE }}
VITE_ENABLE_FIREBASE_SYNC: ${{ vars.VITE_ENABLE_FIREBASE_SYNC }}
VITE_ENABLE_SHARE_LINKS: ${{ vars.VITE_ENABLE_SHARE_LINKS }}
Expand Down
14 changes: 12 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,23 @@ In-app page: [`src/features/legal/PrivacyPolicyPage.tsx`](src/features/legal/Pri

- `npm run dev:debug` / `build:debug` use Dexie DB **`mentell-debug`** and localStorage keys prefixed `mentell.debug-data.*` via [`storageScope.ts`](src/shared/storage/storageScope.ts). Production `npm run dev` uses **`mentell`** — seed/clear in the debug panel does not touch prod journal data on the same origin.
- Theme (`mentell.theme`) and debug toggles (`mentell.debug.*`) stay unscoped.
- Debug builds skip PWA registration in [`main.tsx`](src/main.tsx).
- Debug builds use [`public/dev-push-sw.js`](public/dev-push-sw.js) (push-only, no Workbox) when push env vars are set — not vite-plugin-pwa dev SW. Debug panel → **notifications**: permission, subscribe, `/push/test`, status readout ([`debugNotifications.ts`](src/features/debug/debugNotifications.ts)).

### Debug mode Firebase

- [`DebugAuthProvider`](src/shared/firebase/DebugAuthProvider.tsx): in-memory auth, signs out any prod session, auto sandbox sign-in (anonymous + displayName `DEBUGGER`, or `VITE_DEBUG_FIREBASE_CUSTOM_TOKEN` for fixed uid `DEBUGGER`). Standard sign-in UI is hidden.
- See [`docs/FIREBASE.md`](docs/FIREBASE.md) for optional custom token setup.

### Notifications and package delivery

- Settings → **Features**: `disableNotifications`, **Package delivery** (weekday + local time, default Monday 9:00), **Timezone** (device IANA, for push). Synced via Firestore `meta/settings` when cloud sync is on.
- Permission prompts: [`maybeRequestNotificationPermission`](src/pwa/notifications.ts) on Settings open and after letter submit, only when notifications are not disabled and permission is `default`.
- Delivery schedule: [`packageDelivery.ts`](src/features/packages/packageDelivery.ts) + [`generateDuePackages`](src/features/packages/packageGenerator.ts) create weekly packages only after the configured instant; [`runPackageDeliveryAndNotify`](src/features/packages/runPackageDelivery.ts) shows an OS notification when new packages appear while the tab is open.
- Visible-tab poll in [`App.tsx`](src/App.tsx) every 60s runs delivery while the tab is focused.
- **Web Push (optional, tab closed):** `VITE_VAPID_PUBLIC_KEY` + `VITE_PUSH_API_BASE` on the same worker. [`src/pwa/sw.ts`](src/pwa/sw.ts) + [`pushSubscribe.ts`](src/pwa/pushSubscribe.ts); worker cron every 15m, `PUSH_KV`, optional `FIREBASE_SERVICE_ACCOUNT_JSON` for synced users (package-ready vs generic EST reminder).

**Push operator setup:** `npx web-push generate-vapid-keys` → `wrangler kv namespace create PUSH_KV` (+ `--preview`) → paste ids in `worker/wrangler.jsonc` → `wrangler secret put VAPID_PUBLIC_KEY|VAPID_PRIVATE_KEY|FIREBASE_SERVICE_ACCOUNT_JSON` → `npm run worker:deploy`. GitHub **Variables:** `VITE_VAPID_PUBLIC_KEY`, `VITE_PUSH_API_BASE` (worker origin, no trailing slash). Details: [`worker/README.md`](worker/README.md).

### Motion

- **Route transitions:** Wrap route trees in [`AnimatedRoutes`](src/shared/motion/AnimatedRoutes.tsx) (fade + slight vertical slide). Keep [`AppLegalFooter`](src/components/AppLegalFooter.tsx) **outside** the animated wrapper so the footer does not re-animate on every tab.
Expand All @@ -107,7 +117,7 @@ In-app page: [`src/features/legal/PrivacyPolicyPage.tsx`](src/features/legal/Pri
### Non-obvious notes

- There are no automated tests (no test framework configured). Verify changes via lint, type checking, and manual browser testing.
- The app uses Vite 8 with `vite-plugin-pwa`; the service worker is generated at build time only (not during dev). PWA features cannot be tested with `npm run dev`.
- The app uses Vite 8 with `vite-plugin-pwa` (`injectManifest` + `devOptions.enabled` for local SW). For push, set `VITE_VAPID_PUBLIC_KEY` + `VITE_PUSH_API_BASE` in `.env.local` and **restart** Vite after changing env. Use `http://127.0.0.1:8787` (not `https://`) for local worker. Fallback: `npm run build && npm run preview`.
- **`npm run build` appears to hang** after printing `✓ built in …` — the process is still running **PWA/service-worker generation** with no further stdout for 20–40s. Use `npm run build:check` when you only need a quick compile verification.
- Use `npm run dev -- --host 0.0.0.0` to expose the dev server on all interfaces (needed in cloud/container environments).
- Debug mode: `npm run dev:debug` or `npm run build:debug` enables a debug panel via Vite's `--mode debug` with isolated storage (see **Debug mode storage** above).
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.6.3
1.7.2
13 changes: 12 additions & 1 deletion docs/FIREBASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,18 @@ Add `localhost` under **Authorized domains** if you test email links locally.

Add **Repository variables** (Settings → Actions → Variables) matching `.env.local` when enabling cloud features in production builds. Update [`.github/workflows/gh-pages.yml`](../.github/workflows/gh-pages.yml) — already wired for optional vars.

**Wrangler:** unchanged. Weekly AI still uses `WEEKLY_AI_TOKEN` secret only.
**Wrangler:** unchanged for weekly AI (`WEEKLY_AI_TOKEN` secret only).

## Web Push + Firestore (optional)

When `VITE_ENABLE_FIREBASE_SYNC=1` and push env vars are set, the Cloudflare Worker can read Firestore server-side to send “package ready” pushes to signed-in users.

1. GCP → IAM → Service Accounts → create e.g. `mentell-push-cron`.
2. Grant **Cloud Datastore User** (Firestore read).
3. Keys → JSON → `wrangler secret put FIREBASE_SERVICE_ACCOUNT_JSON` (worker only; never in the client bundle).
4. Client subscribe sends the user’s Firebase ID token; cron queries `users/{uid}/entries` and `users/{uid}/packages`.

Non-synced users still receive generic weekly reminders on **Eastern Time** if they subscribed with push enabled. No change to [`firestore.rules`](../firestore.rules) for end users.

## Share links

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "mentell",
"private": true,
"version": "1.6.3",
"version": "1.7.2",
"type": "module",
"scripts": {
"sync:assets": "node scripts/sync-assets.mjs",
Expand Down
48 changes: 48 additions & 0 deletions public/dev-push-sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* Minimal push-only service worker for npm run dev:debug (no Workbox precache). */
self.addEventListener('install', () => {
self.skipWaiting()
})

self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})

self.addEventListener('push', (event) => {
let data
try {
data = event.data?.json()
} catch {
data = undefined
}
const title = data?.title ?? 'Mentell'
const body = data?.body ?? 'Your weekly package may be ready.'
const scopePath = new URL(self.registration.scope).pathname
const icon = `${scopePath}asset/mentell-icon.png`.replace(/\/{2,}/g, '/')
event.waitUntil(
self.registration.showNotification(title, {
body,
icon,
tag: 'mentell-package',
}),
)
})

self.addEventListener('notificationclick', (event) => {
event.notification.close()
const scopePath = new URL(self.registration.scope).pathname.replace(/\/$/, '')
const target = `${scopePath}/week`.replace(/\/{2,}/g, '/') || '/week'
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
for (const client of clients) {
if ('focus' in client) {
client.focus()
if ('navigate' in client && typeof client.navigate === 'function') {
return client.navigate(target)
}
return
}
}
return self.clients.openWindow(target)
}),
)
})
37 changes: 34 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { Notepad } from './features/notes/Notepad'
import { StickyDock } from './features/stickies/StickyDock'
import { StickyLayer } from './features/stickies/StickyLayer'
import { DebugPanel } from './features/debug/DebugPanel'
import { generateDuePackages } from './features/packages/packageGenerator'
import { runPackageDeliveryAndNotify } from './features/packages/runPackageDelivery'
import { maybeRequestNotificationPermission } from './pwa/notifications'
import { isWebPushConfigured, syncPushSubscription } from './pwa/pushSubscribe'
import { loadAppSettings } from './shared/settings/appSettings'
import { ScoreTicker } from './features/score/ScoreTicker'
import { ScoreBurst } from './features/score/ScoreBurst'
import { Shoppe } from './features/shop/Shoppe'
Expand All @@ -40,7 +43,32 @@ function App() {
} | null>(null)

useEffect(() => {
generateDuePackages()
void runPackageDeliveryAndNotify()
if (!loadAppSettings().disableNotifications && isWebPushConfigured()) {
void syncPushSubscription()
}
}, [])

const auth = useAuthOptional()
const authUid = auth?.user?.uid
useEffect(() => {
if (authUid && !loadAppSettings().disableNotifications && isWebPushConfigured()) {
void syncPushSubscription()
}
}, [authUid])

useEffect(() => {
const tick = () => {
if (document.visibilityState === 'visible') {
void runPackageDeliveryAndNotify()
}
}
const id = window.setInterval(tick, 60_000)
document.addEventListener('visibilitychange', tick)
return () => {
window.clearInterval(id)
document.removeEventListener('visibilitychange', tick)
}
}, [])

useEffect(() => {
Expand Down Expand Up @@ -428,7 +456,10 @@ function HomePlaceholder({
streakAtSubmit: award.nextStreak,
})

await generateDuePackages()
await runPackageDeliveryAndNotify()
if (!loadAppSettings().disableNotifications) {
void maybeRequestNotificationPermission()
}
onScoreChange(award.totalDelta, award.hint, { deferOverlay: true })
setSubmitting(true)
}}
Expand Down
Loading