diff --git a/.dockerignore b/.dockerignore index 615d60f..cde22da 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ npm-debug.log* .env .env.* docs +data images *.md Dockerfile diff --git a/.gitignore b/.gitignore index f1a90a0..8f55d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# saved settings and name suggestions from local runs +data/ + # Logs logs *.log diff --git a/CLAUDE.md b/CLAUDE.md index 9101256..6b39733 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,9 @@ Context for Claude Code sessions in this repo. Keep this short. Longer notes liv ## Project -A help desk dashboard for ManageEngine ServiceDesk Plus Cloud (`ithelp.mainecc.edu`). It's a stateless Node/Express server, shipped as the Docker image `v0idf1sh/sdp-dashboard`, running behind Nginx Proxy Manager with an MCCS IP allow list. Each user signs in with their own Zoho account; the session is an encrypted cookie. The central region (KVCC, EMCC, CMCC, MCCS) is configured through `.env`. +A help desk dashboard for ManageEngine ServiceDesk Plus Cloud (`ithelp.mainecc.edu`). It's a Node/Express server, shipped as the Docker image `v0idf1sh/sdp-dashboard`, running behind Nginx Proxy Manager with an MCCS IP allow list. Each user signs in with their own Zoho account; the session is an encrypted cookie. `.env` sets the defaults (central region: KVCC, EMCC, CMCC, MCCS); each user can override region, timing, weather and hidden panels in Settings, saved on the server by Zoho email in `DATA_DIR` (a Docker volume). -- **Goal and next steps:** per-browser settings and panel modules. See `docs/ROADMAP.md`. +- **Goal and next steps:** per-user settings (built, needs live checks) and panel modules. See `docs/ROADMAP.md`. - **Decisions, API gotchas and known issues:** `docs/NOTES.md`. Read it before touching any SDP query. - **Latest session summary and handoff:** `SESSION.md`. @@ -26,10 +26,13 @@ Releases: push a `v*-prod*` tag. `.gitea/workflows/docker.yml` builds and pushes - `src/server.js`: reads `.env`, holds the region `settings` and `timing`, and starts Express. - `src/auth.js`: Zoho web OAuth (authorization URL, code exchange, renewal, revoke, user info). - `src/session.js`: the encrypted session cookie and the `requireSession` middleware, which renews the access token when it's due. -- `src/routes.js`: `/auth/*` (login, callback, logout) and `/api/*` (session, closed, workload, weather; all need a sign-in). +- `src/routes.js`: `/auth/*` (login, callback, logout) and `/api/*` (session, settings, names, closed, workload, weather; all need a sign-in). +- `src/settings.js`: per-user settings: validation, `LIMITS`, merging over the `.env` defaults. `src/store.js` is the JSON file store under it. +- `src/names.js`: shared cache of site/group/technician names for the settings suggestions. +- `src/places.js`: the weather locations people can pick. - `src/tickets.js`: all SDP queries (closed feed, status table, closed today, unassigned). A 401 from SDP is rethrown so the route signs the user out. -- `src/weather.js`: OpenWeatherMap, cached for 10 minutes and shared by all users. -- `src/public/index.html`: the whole front end in one file (inline CSS and JS, no build step). It runs the polling timers and keeps the closed feed. +- `src/weather.js`: OpenWeatherMap, cached for 10 minutes per zip and shared by all users. +- `src/public/index.html`: the whole front end in one file (inline CSS and JS, no build step). It runs the polling timers, keeps the closed feed, and has the settings dialog. - `Dockerfile`, `docker-compose.yml` (example for the VPS), `.gitea/workflows/docker.yml`. ## Working agreements diff --git a/Dockerfile b/Dockerfile index 121bc23..50ad547 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,10 @@ RUN npm ci --omit=dev && npm cache clean --force COPY src ./src -# no volume: sessions live in each user's browser, so the container keeps no state +# sessions live in each user's browser; the only state is saved settings and name +# suggestions in /app/data, which compose mounts as a volume. Made here so node owns it. +RUN mkdir -p /app/data && chown node:node /app/data +ENV DATA_DIR=/app/data USER node EXPOSE 3000 diff --git a/README.md b/README.md index b7ec942..6acfcaf 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The old **Non-Browser Application** client (device-code sign-in) is no longer us ### 2. `.env` file -Copy `src/env.example` to a file named `.env` and fill it in. dotenv reads `.env` from the folder you start the server in, so keep `.env` in the project root and start the server from there (see step 3). +Copy `src/env.example` to a file named `.env` and fill it in. The region, timing and weather settings are the defaults everyone starts with; each person can change their own in **Settings** (see [Settings](#settings)). dotenv reads `.env` from the folder you start the server in, so keep `.env` in the project root and start the server from there (see step 3). | Setting | Required | What it does | | --- | --- | --- | @@ -33,7 +33,8 @@ Copy `src/env.example` to a file named `.env` and fill it in. dotenv reads `.env | `GROUP_NAMES` | No | Only include requests in these support groups, e.g. `Customer Support`. Leave it empty for all groups. | | `UNASSIGNED_EXCLUDED_STATUSES` | No | Statuses the Unassigned requests counter leaves out. Defaults to `STATUS_NAMES`. | | `WORKLOAD_MINUTES` | No | How often the status table and unassigned count refresh, in minutes. Defaults to 5. | -| `OWM_API_KEY`, `OWM_ZIP_CODE` | No | OpenWeatherMap key and zip code for the weather card. Leave either one empty to hide the card. A free key from is enough. New keys can take a couple of hours to start working. | +| `OWM_API_KEY`, `OWM_ZIP_CODE` | No | OpenWeatherMap key and the default zip code for the weather card. Without a key the card is hidden. People pick other locations from the list in `src/places.js`. A free key from is enough. New keys can take a couple of hours to start working. | +| `DATA_DIR` | No | Where saved settings (`settings.json`) and name suggestions (`names.json`) are kept. Defaults to `data/` in the project root; the Docker image uses `/app/data`. | | `PORT` | No | Port the server listens on. Defaults to 3000. Leave it alone in Docker. | | `TRUST_PROXY` | No | Which proxies to believe for the client's IP address in the logs. Defaults to private and loopback addresses, which covers NPM on the same Docker host. | @@ -87,7 +88,7 @@ docker compose pull docker compose up -d ``` -The container keeps no state, so it needs no volume, and restarting or upgrading it doesn't sign anyone out. +Sign-ins live in each browser's cookie, so restarting or upgrading the container doesn't sign anyone out. The only state is people's saved settings and the name suggestions, in the `sdp-data` volume mounted at `/app/data`. Keep that volume when upgrading; losing it only resets everyone to the `.env` defaults. The compose file publishes no ports. Docker's port publishing bypasses the host firewall, so `3000:3000` would let anyone reach the app directly and skip NPM's access list. Instead the container joins NPM's Docker network (`npm_default` by default; set `NPM_NETWORK` if yours is named differently, and check with `docker network ls`). @@ -100,7 +101,7 @@ The compose file publishes no ports. Docker's port publishing bypasses the host ## Signing in -Opening the dashboard shows a **Sign in with Zoho** button. It goes to Zoho's consent page, and Zoho sends you back signed in. Use the Zoho account you use for ServiceDesk Plus. The dashboard shows what that account can see, limited to the region in `.env`. +Opening the dashboard shows a **Sign in with Zoho** button. It goes to Zoho's consent page, and Zoho sends you back signed in. Use the Zoho account you use for ServiceDesk Plus. The dashboard shows what that account can see, limited to the region in that person's settings. The sign-in is kept in an encrypted cookie in that browser for `SESSION_DAYS` (7 by default), and the hourly Zoho access token is renewed automatically. **Sign out** clears the cookie and revokes the token at Zoho. If Zoho or SDP stops accepting the sign-in, the page goes back to the sign-in button. @@ -110,6 +111,7 @@ The sign-in is kept in an encrypted cookie in that browser for `SESSION_DAYS` (7 - **Per-user sign-in:** every request needs the user's own Zoho sign-in. There is no shared token, so a reachable page with no sign-in shows nothing. - **Tokens:** the Zoho tokens live only in the user's cookie, encrypted and authenticated with `SESSION_SECRET` (AES-256-GCM). The cookie is `HttpOnly`, so page scripts can't read it, and `SameSite=Lax`. The server doesn't store tokens or sessions. - **Read-only:** the only SDP scope is `SDPOnDemand.requests.READ`. +- **Settings:** saved on the server, keyed by the email Zoho returns at sign-in. They hold names and numbers only, never tokens. The server checks every value (plain names, list lengths, refresh intervals, weather locations from a fixed list). Changing the region can't show anyone more than SDP already lets them see, because every query uses their own token. ## The dashboard @@ -119,7 +121,18 @@ The sign-in is kept in an encrypted cookie in that browser for `SESSION_DAYS` (7 - **Recently closed**: the live closed-ticket feed, checked every minute. A ding plays for each new close. The sound is on by default, and the **Sound on close** toggle mutes it in that browser. - **Refresh now** reruns every check immediately. -The browser runs the checks, using the signed-in user's token, so every open dashboard makes its own SDP calls. That's one small query a minute and a recount every `WORKLOAD_MINUTES`. On first load it fetches the last 12 hours of closes to fill the feed. +## Settings + +**⚙ Settings** in the header, and a setup prompt on someone's first visit, let each person choose: + +- **Sites, groups and technicians** to watch. Empty means everyone. Suggestions come from names seen on recent tickets anywhere in SDP (refreshed at most hourly and shared by everyone). Any name can be typed, but it must match SDP's spelling; a name that differs only in case is saved with SDP's spelling. +- **Timing:** how often the status table recounts (2 to 60 minutes) and how long closed tickets stay in the feed (1 to 24 hours). The closed check itself always runs every minute. +- **Weather location**, from the list of campuses in `src/places.js`. +- **What's shown:** the stat tiles, weather card and closed feed can be hidden (the ding still plays with the feed hidden), and so can individual status columns. + +Settings are saved to the person's Zoho account on the server, so they follow them to any browser. Only the values that differ from `.env` are stored, so someone who never changed the region picks up later changes to `.env`. **Reset to defaults** goes back to `.env`. The closed and open status rules aren't per person; they stay in `.env`. + +The browser runs the checks, using the signed-in user's token, so every open dashboard makes its own SDP calls. That's one small query a minute and a recount at the person's chosen interval (`WORKLOAD_MINUTES` by default). On first load it fetches the feed's full length (12 hours by default) of closes. If a query fails, a red error box shows what SDP returned, and the other sections keep working. If the table ever shows **partial**, a query matched more tickets than it can page through (2,000). The server log (`docker compose logs`) then has the details, including SDP's total count. A very large total usually means SDP ignored the search criteria. @@ -134,9 +147,13 @@ If a query fails, a red error box shows what SDP returned, and the other section | `src/server.js` | Reads `.env`, then starts Express. | | `src/auth.js` | Zoho web sign-in: the authorization URL, code exchange, token renewal and revoke. | | `src/session.js` | The encrypted session cookie, and renewing the access token when it's due. | -| `src/routes.js` | `/auth/login`, `/auth/callback` and `/auth/logout`, plus `/api/session`, `/api/closed`, `/api/workload` and `/api/weather`, which need a sign-in. | +| `src/routes.js` | `/auth/login`, `/auth/callback` and `/auth/logout`, plus `/api/session`, `/api/settings`, `/api/names`, `/api/closed`, `/api/workload` and `/api/weather`, which need a sign-in. | +| `src/settings.js` | Each user's saved settings: validation, limits, and merging over the `.env` defaults. | +| `src/names.js` | The shared cache of site, group and technician names for the settings suggestions. | +| `src/store.js` | The small JSON file store both of those use. | +| `src/places.js` | The weather locations people can pick from. | | `src/tickets.js` | SDP API queries for the closed feed, the status table, closed today and unassigned requests. | -| `src/weather.js` | OpenWeatherMap current weather and forecast, cached for 10 minutes. | +| `src/weather.js` | OpenWeatherMap current weather and forecast, cached for 10 minutes per location. | | `src/public/index.html` | The dashboard page. It runs the checks and keeps the closed feed. | | `Dockerfile`, `docker-compose.yml` | The image, and an example of how to run it. | | `.gitea/workflows/docker.yml` | Builds and pushes the image on `v*-prod*` tags. | diff --git a/docker-compose.yml b/docker-compose.yml index 68185c4..666bfb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,9 +15,15 @@ services: # shared network instead, using the container name as the forward hostname. expose: - "3000" + # each user's saved settings and the shared name suggestions + volumes: + - sdp-data:/app/data networks: - proxy +volumes: + sdp-data: + networks: proxy: # the network Nginx Proxy Manager's container is on; check with `docker network ls` diff --git a/docs/NOTES.md b/docs/NOTES.md index 630dc1b..3994d65 100644 --- a/docs/NOTES.md +++ b/docs/NOTES.md @@ -34,11 +34,18 @@ Docs: