10 Commits

Author SHA1 Message Date
Scott Idem
bd5759f037 docs(readme): update build/deploy section for new script names and env tiers
Replace stale deploy:staging/deploy:prod references with current
build:docker:*, deploy:remote:*, and .env.dev/test/prod file names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:05:18 -04:00
Scott Idem
872e381c00 Added missing required var for API path 2026-03-26 17:54:00 -04:00
Scott Idem
64402e8e2a chore(scripts): rename deploy:* → build:docker:*, add deploy:remote:*
- deploy:dev/test/prod → build:docker:dev/test/prod to distinguish
  local Docker builds from remote server deploys
- Add deploy:remote:test and deploy:remote:prod — SSH to linode.oneskyit.com
  and run deploy.sh on the server
- Trailing whitespace cleanup in .env.*.default files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 17:18:12 -04:00
Scott Idem
88b11b8318 Renaming files 2026-03-26 16:09:53 -04:00
Scott Idem
65e0477761 refactor(build): replace staging/cp env hack with vite --mode per-environment
- Rename .env.staging → .env.dev (and .default template)
- Add .env.test.default for the test tier (test-api.oneskyit.com)
- build:staging → build:dev/test/prod using vite --mode <name>
- deploy:staging → deploy:dev; add deploy:test
- Dockerfile: ARG BUILD_MODE=dev; explicit .env.runtime copy per mode
- .dockerignore: rewritten (deduped); allow .env.dev/.env.test/.env.prod
- .gitignore: track .env.dev.default and .env.test.default
- Remove dead PUBLIC_AE_* imports from ae_stores.ts (ACCOUNT_ID, EVENT_ID,
  NO_ACCOUNT_ID_TOKEN, SPONSORSHIP_CFG_ID); sponsorship_cfg_id defaults to null
- Strip dead vars from .env.prod.default template (AE_CFG_ID, AE_APP_NODE_PORT,
  ACCOUNT_ID, EVENT_ID, SPONSORSHIP_CFG_ID, NO_ACCOUNT_ID_TOKEN)
- GUIDE__Development.md: build:staging → build:dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 16:07:31 -04:00
Scott Idem
98736ae1bc chore(env): scrub real account IDs from .env.staging.default comments
The staging default template had real OSIT account_id and event_id values
in inline comments. These are not secrets but shouldn't be in a committed
template — they'd be misleading on any non-OSIT deployment.

Replaced with plain XXXX placeholders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:08:18 -04:00
Scott Idem
7308a4773d docs(api): add V3 user actions section and clarify response shape
Added section 7 covering /v3/action/user/ endpoints: authenticate, verify_password,
change_password, new_auth_key, email_auth_key_url — including the legacy→V3
migration table and auth key one-time-use behavior.

Also clarified the response shape note to explicitly list all response types
(GET single, GET list, POST create, PATCH, search) that use the V3 envelope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:05:39 -04:00
Scott Idem
99541f0f9d fix(api): add explicit fetch CORS options and response header debug logging
Added mode, credentials, redirect, and cache options to the GET fetchOptions
object. These were previously left to browser defaults, which vary by environment
and can produce opaque CORS failures that are hard to diagnose. Being explicit
avoids environment-dependent surprises.

Also added a try/catch around response.headers logging (log_lvl >= 1) so header
dumps don't throw in environments that restrict header access.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 14:05:31 -04:00
Scott Idem
f950c22a59 fix(clip-video): correct false 'Clipped' state on network failure + error UI
get_object() returns false on network failure; the .then() handler was
running with result=false and accessing result.hosted_file_id (evaluates
to undefined, valid JS key, no throw) so all success state was set even
though the request failed.

- Guard result in .then(): if !result.hosted_file_id → set status='error'
- Add 'Failed — Retry?' button state in error branch
- Raise client-side AbortController timeout 300s → 1800s (30 min)
- Add comment explaining root cause (get_object returns false, not throw)

Root cause of the connection drop is proxy_send_timeout or NAT hairpin
timeout (both default 60s) — not a frontend issue; tracked separately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 12:16:07 -04:00
Scott Idem
b63f8eed0c Work on IDAA and Novi auth 2026-03-25 21:13:27 -04:00
16 changed files with 386 additions and 171 deletions

View File

@@ -1,45 +1,47 @@
.svelte-kit
.vite
node_modules
dist
build
.cache
.DS_Store
.vscode
.idea
.git
# Build artifacts and local state
.svelte-kit/
.vite/
node_modules/
build/
dist/
.cache/
# VCS and IDE
.git/
.gitignore
coverage
tests
documentation
backups
.vscode/
.idea/
# OS junk
.DS_Store
.directory
# Logs and temp files
*.log
*.bak
*.tgz
.env
.env.*
npm-debug.log
package-lock.json.bak
yarn-error.log
/.cache
/.parcel-cachenode_modules/
build/
.svelte-kit/
.git/
.env
.env.*
!.env.staging
!.env.prod
npm_deploy/
test-results/
test_results/
documentation/
backups/
*.log
*.bak
.claude/
.vscode/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Test output and dev-only dirs
tests/
test-results/
test_results/
coverage/
documentation/
backups/
.claude/
# Deployment artifacts
npm_deploy/
package-lock.json.bak
# Env files: exclude all live secrets, allow only the per-environment files needed for Docker builds.
# .env.local is workstation-only and must never enter a container image.
.env
.env.*
!.env.dev
!.env.test
!.env.prod

19
.env.dev.default Normal file
View File

@@ -0,0 +1,19 @@
# One Sky IT's Aether Framework and System — DEV template (dev-*.oneskyit.com)
# Copy to .env.dev and fill in real values.
# Aether API access
PUBLIC_AE_API_PROTOCOL=https
PUBLIC_AE_API_SERVER=dev-api.oneskyit.com
PUBLIC_AE_API_BAK_SERVER=test-api.oneskyit.com
PUBLIC_AE_API_PORT=443
PUBLIC_AE_API_PATH=
PUBLIC_AE_API_SECRET_KEY=XXXX
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
# Separate from the main API key — has limited permissions (no account_id required).
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here

View File

@@ -1,9 +1,5 @@
# One Sky IT's Aether Framework and System — PRODUCTION template
# One Sky IT's Aether Framework and System — PROD template (api.oneskyit.com)
# Copy to .env.prod and fill in real values.
# AE_CFG_ID: 1=Default, 5=Home Dev, 7=Live Testing/Prod
# Shared config record (controls SMTP, API routing, external keys from DB)
AE_CFG_ID=7
# Aether API access
PUBLIC_AE_API_PROTOCOL=https
@@ -13,16 +9,10 @@ PUBLIC_AE_API_PORT=443
PUBLIC_AE_API_PATH=
PUBLIC_AE_API_SECRET_KEY=XXXX
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
# Separate from the main API key — has limited permissions (no account_id required).
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
# SvelteKit app config
AE_APP_NODE_PORT=3001
# Default demo/client context (set to the target account for this deployment)
PUBLIC_AE_ACCOUNT_ID=XXXX
PUBLIC_AE_EVENT_ID=XXXX
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX

View File

@@ -1,28 +0,0 @@
# One Sky IT's Aether Framework and System — STAGING / HOME DEV template
# Copy to .env.staging and fill in real values.
# AE_CFG_ID: 1=Default, 5=Home Dev, 7=Live Testing/Prod
# Shared config record (controls SMTP, API routing, external keys from DB)
AE_CFG_ID=5
# Aether API access
PUBLIC_AE_API_PROTOCOL=https
PUBLIC_AE_API_SERVER=dev-api.oneskyit.com
PUBLIC_AE_API_BAK_SERVER=test-api.oneskyit.com
PUBLIC_AE_API_PORT=443
PUBLIC_AE_API_PATH=
PUBLIC_AE_API_SECRET_KEY=XXXX
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
# Separate from the main API key — has limited permissions (no account_id required).
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN=Nothing_to_see_here
# SvelteKit app config
AE_APP_NODE_PORT=3001
# Default demo/client context (set to the target account for this deployment)
PUBLIC_AE_ACCOUNT_ID=XXXX # OSIT = _XY7DXtc9MY
PUBLIC_AE_EVENT_ID=XXXX # OSIT = pjrcghqwert
PUBLIC_AE_SPONSORSHIP_CFG_ID=XXXX

18
.env.test.default Normal file
View File

@@ -0,0 +1,18 @@
# One Sky IT's Aether Framework and System — TEST template (test-api.oneskyit.com)
# Copy to .env.test and fill in real values.
# Aether API access
PUBLIC_AE_API_PROTOCOL=https
PUBLIC_AE_API_SERVER=test-api.oneskyit.com
PUBLIC_AE_API_BAK_SERVER=api.oneskyit.com
PUBLIC_AE_API_PORT=443
PUBLIC_AE_API_PATH=
PUBLIC_AE_API_SECRET_KEY=XXXX
PUBLIC_AE_API_CRUD_SUPER_KEY=XXXX
# Bootstrap key: used only for the unauthenticated site-domain lookup on first load.
# Separate from the main API key — has limited permissions (no account_id required).
PUBLIC_AE_BOOTSTRAP_KEY=XXXX
PUBLIC_AE_NO_ACCOUNT_ID=No_Account_ID_Here

3
.gitignore vendored
View File

@@ -8,7 +8,8 @@ node_modules
.env.*
!.env.example
!.env.prod.default
!.env.staging.default
!.env.test.default
!.env.dev.default
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -9,18 +9,32 @@ RUN npm install
# Copy the rest of the source code.
COPY . .
# Build Argument to determine build environment (staging, prod, or production).
ARG BUILD_MODE=staging
# Build Argument to determine build environment (dev, test, prod).
ARG BUILD_MODE=dev
ENV NODE_ENV=production
# Sync the SvelteKit project to generate ./.svelte-kit/tsconfig.json
RUN npx svelte-kit sync
# Perform the build based on the BUILD_MODE argument.
# Each script uses vite --mode <name>, which reads .env.<name> directly — no cp hack needed.
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
npm run build:prod; \
elif [ "$BUILD_MODE" = "test" ]; then \
npm run build:test; \
else \
npm run build:staging; \
npm run build:dev; \
fi
# Copy the source env file to .env.runtime for the deploy stage.
# PUBLIC_* vars are baked into the JS bundle by vite; non-PUBLIC vars (AE_CFG_ID,
# AE_APP_NODE_PORT) are read by the Node server at runtime and need this file.
RUN if [ "$BUILD_MODE" = "prod" ] || [ "$BUILD_MODE" = "production" ]; then \
cp .env.prod .env.runtime; \
elif [ "$BUILD_MODE" = "test" ]; then \
cp .env.test .env.runtime; \
else \
cp .env.dev .env.runtime; \
fi
# Stage 2: Final runtime image
@@ -35,8 +49,8 @@ COPY --from=builder /app/package-lock.json .
# Install only production dependencies.
RUN npm install --omit=dev
# Copy the resulting .env.production file to .env.
COPY --from=builder /app/.env.production .env
# Copy the runtime env file (non-PUBLIC vars for the Node server).
COPY --from=builder /app/.env.runtime .env
# SvelteKit (via adapter-node) defaults to port 3000.
EXPOSE 3000

View File

@@ -123,32 +123,45 @@ Developer sandbox pages — not for production use.
# How to build and deploy SvelteKit:
The deployment is now fully integrated into the unified **Aether Docker Environment** (`aether_container_env`). The application is built directly from source inside a clean Docker container, ensuring consistent environment handling across staging and production.
The deployment is fully integrated into the unified **Aether Docker Environment** (`aether_container_env`). The application is built inside a clean Docker container using `vite build --mode <env>`, which reads the corresponding `.env.<env>` file for `PUBLIC_` variables.
### Deployment Commands
## Environments
Run these commands from the root of the `aether_app_sveltekit` project:
| Environment | Env file | Vite mode | API server |
| ----------- | ----------- | --------- | ------------------------- |
| dev | `.env.dev` | `dev` | `dev-api.oneskyit.com` |
| test | `.env.test` | `test` | `test-api.oneskyit.com` |
| prod | `.env.prod` | `prod` | `api.oneskyit.com` |
## Commands (from `aether_app_sveltekit/`)
#### 1. Deploy to Staging (Dev Workstation)
This triggers an autonomous build inside the Docker container using your `.env.staging` file and restarts the service.
```bash
npm run deploy:staging
```
# Active development — Vite HMR, no Docker
npm run dev
#### 2. Deploy to Production
This builds the image using production flags (using `.env.prod`) and restarts the production container.
```bash
npm run deploy:prod
# Build Vite output only (no Docker)
npm run build:dev
npm run build:test
npm run build:prod
# Build Docker image and restart container locally
npm run build:docker:dev
npm run build:docker:test
npm run build:docker:prod
# Deploy to remote server (SSH → linode.oneskyit.com → deploy.sh)
npm run deploy:remote:test
npm run deploy:remote:prod
```
### Technical Details
- **Unified Orchestration**: All services (API, UI, Redis) are managed via `~/OSIT_dev/aether_container_env/docker-compose.yml`.
- **Dockerfile**: Uses a multi-stage build. Stage 1 (builder) installs dependencies and builds the app using the `BUILD_MODE` argument passed from Docker Compose. Stage 2 (runtime) creates the final lightweight image.
- **Dockerfile**: Multi-stage build. Stage 1 (builder) runs `vite build --mode $BUILD_MODE` using `.env.$BUILD_MODE`. Stage 2 (runtime) creates the final lightweight Node image.
- **Environment Handling**:
- `PUBLIC_` variables are baked into the image during the build step.
- Private runtime variables are passed via the orchestration's `.env` file.
- **Networking**: The frontend communicates with the backend via the high-speed internal Docker network (`http://ae_api:5005`).
- `PUBLIC_` variables are baked into the image at build time via the `.env.<mode>` file.
- Private runtime variables are passed via the Docker Compose `.env` file in `aether_container_env/`.
- **Remote deploy**: `aether_container_env/deploy.sh` handles git pull + Docker build + restart on the server. Triggered via `npm run deploy:remote:*`.
---
@@ -229,8 +242,9 @@ npm install @tiptap/extension-link @tiptap/extension-bullet-list @tiptap/extensi
The application uses standard SvelteKit `.env` files for build-time configuration (specifically for `PUBLIC_` prefixed variables).
- **`.env.staging`**: Used by `npm run deploy:staging`.
- **`.env.prod`**: Used by `npm run deploy:prod`.
- **`.env.dev`**: Used by `npm run build:docker:dev` and `npm run build:dev`.
- **`.env.test`**: Used by `npm run build:docker:test` and `npm run build:test`.
- **`.env.prod`**: Used by `npm run build:docker:prod` and `npm run build:prod`.
- **`.env.local`**: Used during local development (`npm run dev`).
**Note:** Runtime variables (like private API keys or DB credentials) are managed in the deployment directory's `.env` file and passed to the containers via Docker Compose.
@@ -248,16 +262,16 @@ npm run dev
npm run dev -- --open
```
## Deployment (Production/Staging)
To create a production or staging version and deploy it to the local containers:
## Deployment
```bash
# For Staging/Dev
npm run deploy:staging
# Build Docker image locally and restart container
npm run build:docker:dev
npm run build:docker:prod
# For Production
npm run deploy:prod
# Deploy to remote server (linode.oneskyit.com)
npm run deploy:remote:test
npm run deploy:remote:prod
```
These commands use the multi-stage **Dockerfile** to build the app in a clean environment and automatically restart the corresponding Docker containers.

View File

@@ -540,6 +540,23 @@ ae_loc.idaa_loc = { novi_uuid: 'test-uuid-value', ... };
- [Naming Conventions](./AE__Naming_Conventions.md)
- [Playwright Test README](../tests/README.md)
---
## IDAA Novi Groups and Moderators
IDAA Couples Meeting = "e9e162f0-3d03-4241-9682-340135ec3fb8"
"Gregory X Boehm" "00ee764c-7559-496b-9d18-40d3e9092c0c"
"Kee B. PARK" "24ab3297-bfce-473c-9311-4b31e3a8974f"
"Laura Lander" "ac697456-61fe-4f7d-a8b8-d04866032320"
"Nancy J Duff-Boehm" "5c7c09bc-4f23-432c-bfd9-87a66b548502"
"Owen Lander" "9671a2c4-ff95-48c2-bcde-5c6eba95cded"
"Susan Park" "4a9f94c5-d766-4808-ab76-117c9e43903a"
"Student/Resident Meeting Moderators" "d76d2c00-962d-40f6-a2e8-ed9c85594d96"
"Melissa Eve Valasky" "182d1db3-caa9-41bc-b04a-2facc6859aeb"
"Steven L. Klein" "5724aad7-6d89-47e7-8943-966fd22911bd"
---
**Document Status:** ✅ Current

View File

@@ -73,7 +73,7 @@ Modify data in the system.
> [!IMPORTANT]
> **V3 responses always use random string IDs — never database integers.**
After a successful `POST` create or any `GET`, the response contains:
All V3 responses — `POST` create, `GET` single, `GET` list, search, and `PATCH` update — contain:
| Field | Type | Use |
| :--- | :--- | :--- |
@@ -262,7 +262,150 @@ Frontend guidance:
---
## 7. Event Exhibit Tracking Export (Leads Export)
## 7. User Actions (`/v3/action/user/`)
Stateful user account operations that are not standard CRUD. All require `x-aether-api-key`.
> [!IMPORTANT]
> **Migration from legacy `/user/*` routes:** The table below maps each legacy endpoint to its V3 replacement. Run both in parallel during transition; remove legacy routes once traffic logs confirm they are quiet.
>
> | Legacy | V3 Replacement |
> |---|---|
> | `GET /user/authenticate` | `POST /v3/action/user/authenticate` |
> | `POST /user/verify_password` | `POST /v3/action/user/verify_password` |
> | `PATCH /user/{id}/change_password` | `POST /v3/action/user/{id}/change_password` |
> | `GET /user/{id}/new_auth_key` | `GET /v3/action/user/{id}/new_auth_key` |
> | `GET /user/{id}/email_auth_key_url` | `GET /v3/action/user/{id}/email_auth_key_url` |
> | `GET /user/lookup` | `POST /v3/crud/user/search` |
> | `GET /user/lookup_email` | `POST /v3/crud/user/search` |
> | `GET /user/lookup_username` | `POST /v3/crud/user/search` |
### A. Authenticate
Authenticate a user by **username + password** or **user_id + auth_key**.
- **Method:** `POST`
- **Path:** `/v3/action/user/authenticate`
- **Auth:** `x-aether-api-key` + `x-account-id` (scopes username lookups to the correct account)
- **Security improvement:** Credentials are in the **POST body**, not query params — safe from URL logging.
**Request body:**
```json
{ "username": "scott", "password": "MyPassword123!" }
```
or:
```json
{ "user_id": "<user_id_random>", "auth_key": "<one_time_key>", "valid_email": true }
```
- `valid_email` (optional `bool`): if `true`, marks `email_verified = true` on success.
- `inc_user_role_list` (optional query param, default `false`): include role list in the returned user object.
**Response on success:** Full user object (same shape as `GET /v3/crud/user/{id}`).
**Errors:** `400` missing credentials, `403` wrong password or account disabled, `404` user not found.
> **Auth key flow:** Auth keys are one-time-use — the key is cleared from the DB immediately on successful authentication. Request a new one via `GET /v3/action/user/{id}/new_auth_key`.
---
### B. Verify Password
Check a user's current password without changing it.
- **Method:** `POST`
- **Path:** `/v3/action/user/verify_password`
- **Auth:** `x-aether-api-key` + `x-account-id`
**Request body:**
```json
{ "user_id": "<user_id_random>", "current_password": "MyPassword123!" }
```
or use `"username"` instead of `"user_id"` to look up by username within the account.
**Response:** `data: true` on match. `403` on mismatch, `404` if user not found.
---
### C. Change Password
Change a user's password. Optionally verify the current password first.
- **Method:** `POST`
- **Path:** `/v3/action/user/{user_id}/change_password`
- **Auth:** `x-aether-api-key` + `x-account-id`
**Request body:**
```json
{ "new_password": "NewPassword456!", "current_password": "MyPassword123!" }
```
- `new_password` is required (minimum 10 characters).
- `current_password` is optional. If provided, it is verified before the change is applied. Omit it for admin-driven resets.
**Response:** `data: true` on success. `403` if `current_password` provided but wrong.
---
### D. Generate New Auth Key
Generate a fresh one-time-use auth key for the user and write it to the DB.
- **Method:** `GET`
- **Path:** `/v3/action/user/{user_id}/new_auth_key`
- **Auth:** `x-aether-api-key` + `x-account-id`
**Response:**
```json
{ "data": { "auth_key": "<new_key>" } }
```
The returned key can then be passed to `/authenticate` (as `auth_key`) or embedded in a login URL. The user record must have `allow_auth_key = true` for key-based authentication to work.
---
### E. Email Auth Key URL
Generate a new auth key and email a one-time login link to the user's email address.
- **Method:** `GET`
- **Path:** `/v3/action/user/{user_id}/email_auth_key_url`
- **Auth:** `x-aether-api-key` + `x-account-id`
**Query Parameters:**
| Parameter | Type | Default | Description |
|---|---|---|---|
| `root_url` | `string` | `null` | Base URL the login link is built from. |
| `key_param_name` | `string` | `auth_key` | Query param name used for the auth key in the generated link. |
**Response:** `data: true` on success (email sent). `500` if delivery failed (check account email config and that the user account is enabled with `allow_auth_key = true`).
---
### F. User Lookups via V3 CRUD Search
The three legacy lookup routes (`lookup`, `lookup_email`, `lookup_username`) are replaced by standard V3 CRUD search:
```typescript
// Look up by user_id (Vision ID)
POST /v3/crud/user/search
{ "and": [{ "field": "id_random", "op": "eq", "value": "<user_id>" }] }
// Look up by email
POST /v3/crud/user/search
{ "and": [{ "field": "email", "op": "eq", "value": "user@example.com" }] }
// Look up by username
POST /v3/crud/user/search
{ "and": [{ "field": "username", "op": "eq", "value": "scott" }] }
```
Results are automatically scoped to the `x-account-id` provided in the request.
---
## 9. Event Exhibit Tracking Export (Leads Export)
Allows an exhibitor to download all lead-capture records for their exhibit as a CSV or XLSX file.
@@ -330,7 +473,7 @@ const url = URL.createObjectURL(blob);
---
## 8. Troubleshooting 403 Forbidden
## 10. Troubleshooting 403 Forbidden
If you receive a 403 on a valid ID:
1. Verify `x-aether-api-key` is correct.

View File

@@ -10,7 +10,7 @@
- **Zero Tolerance:** If a task introduces even a single svelte-check warning or error, it must not be merged. Resolve all warnings before committing.
2. **Type Safety:** Ensure interfaces in `src/lib/types/ae_types.ts` match backend schemas.
3. **Reactivity Check:** Verify Svelte 5 runes (`$state`, `$derived`) are not creating race conditions with Dexie `liveQuery`.
4. **Build Check:** For major changes, run `npm run build:staging` to ensure no SSR or build-time failures.
4. **Build Check:** For major changes, run `npm run build:dev` to ensure no SSR or build-time failures.
5. **Integration Tests:** For changes to badge print, event layouts, or auth/store logic, run the relevant Playwright test file(s):
```bash
npx playwright test tests/event_badge_render.test.ts tests/event_badge_attendee_workflow.test.ts

View File

@@ -7,8 +7,9 @@
"scripts": {
"dev": "vite dev",
"build": "vite build",
"build:prod": "cp .env.prod .env.production && vite build",
"build:staging": "cp .env.staging .env.production && vite build",
"build:dev": "vite build --mode dev",
"build:test": "vite build --mode test",
"build:prod": "vite build --mode prod",
"preview": "vite preview",
"test": "npm run test:integration && npm run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
@@ -17,9 +18,12 @@
"format": "prettier --write .",
"test:integration": "playwright test",
"test:unit": "vitest",
"deploy:staging": "docker compose -f ../aether_container_env/docker-compose.yml build ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
"deploy:prod": "docker compose -f ../aether_container_env/docker-compose.yml build --build-arg BUILD_MODE=prod ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d --remove-orphans ae_app",
"compose:down": "docker compose -f ../aether_container_env/docker-compose.yml --profile database down"
"build:docker:dev": "docker compose -f ../aether_container_env/docker-compose.yml build ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
"build:docker:test": "docker compose -f ../aether_container_env/docker-compose.yml build --build-arg BUILD_MODE=test ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d ae_app",
"build:docker:prod": "docker compose -f ../aether_container_env/docker-compose.yml build --build-arg BUILD_MODE=prod ae_app && docker compose -f ../aether_container_env/docker-compose.yml up -d --remove-orphans ae_app",
"compose:down": "docker compose -f ../aether_container_env/docker-compose.yml --profile database down",
"deploy:remote:test": "ssh linode.oneskyit.com 'bash /srv/env/test_aether/deploy.sh test'",
"deploy:remote:prod": "ssh linode.oneskyit.com 'bash /srv/env/prod_aether/deploy.sh prod'"
},
"devDependencies": {
"@eslint/js": "^9.39.1",

View File

@@ -173,7 +173,13 @@ export const get_object = async function get_object({
const fetchOptions: RequestInit = {
method: 'GET',
headers: headers_cleaned,
signal: controller.signal
signal: controller.signal,
// Be explicit about CORS behavior and redirect handling to avoid
// environment-dependent defaults that can cause opaque failures.
mode: 'cors',
credentials: 'omit',
redirect: 'follow',
cache: 'no-store'
};
if (log_lvl > 1) {
@@ -259,6 +265,14 @@ export const get_object = async function get_object({
console.log(
`Response: status=${response.status} statusText=${response.statusText} url=${response.url} attempt=${attempt}`
);
try {
console.log(
'Response headers:',
Object.fromEntries(response.headers.entries())
);
} catch (e) {
/* ignore header read errors */
}
}
if (log_lvl > 1) {
console.log('Response:', response);

View File

@@ -147,7 +147,7 @@ function handle_clip_video(event: Event) {
api_cfg: $ae_api,
endpoint: endpoint,
params: params,
timeout: 300000, // 5 minutes
timeout: 1800000, // 30 minutes — clip_video runs ffmpeg which can take 5-15 min for long recordings
// return_blob: true,
// filename: event.target.new_filename.value,
// auto_download: false,
@@ -157,12 +157,24 @@ function handle_clip_video(event: Event) {
.then(function (result) {
console.log(result);
// WHY: get_object() returns false/null on network failure (CORS drop,
// timeout, Gunicorn worker killed). Without this guard, result.hosted_file_id
// is undefined — JS uses it as an object key without throwing, so all the
// success state below would run and show "Clipped" on a failed request.
if (!result || !result.hosted_file_id) {
console.log('clip_video: request failed or returned no result');
$ae_sess.files.processed_file_kv[hosted_file_id].submit_status =
'error';
$ae_loc.files.processed_file_kv[hosted_file_id].submit_status =
'error';
submit_status = 'error';
clip_complete = false;
return false;
}
video_clip_file_kv[result.hosted_file_id] = {};
video_clip_file_kv[result.hosted_file_id] = result;
// $ae_loc.files.video_clip_file_kv[result.hosted_file_id] = {};
// $ae_loc.files.video_clip_file_kv[result.hosted_file_id] = result;
$ae_sess.files.processed_file_kv[hosted_file_id].submit_status =
'clipped';
$ae_sess.files.processed_file_kv[hosted_file_id].clip_complete =
@@ -176,13 +188,6 @@ function handle_clip_video(event: Event) {
submit_status = 'clipped';
clip_complete = true;
// let file_blob = new Blob([result.data]);
// // console.log(file_blob);
// let file_obj_url = window.URL.createObjectURL(file_blob); // The img src
// // const url = window.URL.createObjectURL(new Blob([result.data]));
// download_clip_src = file_obj_url;
// // download_filename = file_obj_url;
return true;
});
}
@@ -363,21 +368,25 @@ function handle_clip_video(event: Event) {
<button
type="submit"
class="btn btn-lg btn-primary preset-tonal-primary border-primary-500 hover:preset-filled-primary-500 border transition-colors"
class="btn btn-lg btn-primary border transition-colors"
class:preset-tonal-primary={submit_status !== 'error'}
class:border-primary-500={submit_status !== 'error'}
class:hover:preset-filled-primary-500={submit_status !== 'error'}
class:preset-tonal-error={submit_status === 'error'}
class:border-error-500={submit_status === 'error'}
disabled={submit_status == 'clipping'}>
<!-- {#await ae_promises[hosted_file_id]} -->
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipping'}
<LoaderCircle size="1em" class="m-1 animate-spin" />
<span class="highlight">Clipping...</span>
{:else if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'error'}
<span class="fas fa-exclamation-triangle m-1"></span>
Failed Retry?
{:else if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipped'}
<Check size="1em" class="m-1" />
Clipped
{:else}
<!-- {#if ae_promises[hosted_file_id]} -->
{#if $ae_loc.files.processed_file_kv[hosted_file_id] && $ae_loc.files.processed_file_kv[hosted_file_id].submit_status == 'clipped'}
<Check size="1em" class="m-1" />
Clipped
{:else}
<Scissors size="1em" class="m-1" />
Clip Video
{/if}
<Scissors size="1em" class="m-1" />
Clip Video
{/if}
<!-- <span class="fas fa-cut m-1"></span>
Clip Video -->

View File

@@ -18,10 +18,10 @@ import {
PUBLIC_AE_API_SECRET_KEY,
PUBLIC_AE_API_CRUD_SUPER_KEY,
PUBLIC_AE_NO_ACCOUNT_ID,
PUBLIC_AE_NO_ACCOUNT_ID_TOKEN,
PUBLIC_AE_ACCOUNT_ID,
PUBLIC_AE_EVENT_ID,
PUBLIC_AE_SPONSORSHIP_CFG_ID
// PUBLIC_AE_NO_ACCOUNT_ID_TOKEN,
// PUBLIC_AE_ACCOUNT_ID,
// PUBLIC_AE_EVENT_ID,
// PUBLIC_AE_SPONSORSHIP_CFG_ID
} from '$env/static/public';
const api_server_fqdn = PUBLIC_AE_API_SERVER; // 'api.oneskyit.com'
@@ -34,9 +34,9 @@ const api_crud_super_key = PUBLIC_AE_API_CRUD_SUPER_KEY;
// const ae_account_id = PUBLIC_AE_ACCOUNT_ID;
const ae_account_id: null | string = null;
const ae_no_account_id = PUBLIC_AE_NO_ACCOUNT_ID;
const ae_no_account_id_token = PUBLIC_AE_NO_ACCOUNT_ID_TOKEN;
const ae_event_id = PUBLIC_AE_EVENT_ID;
const ae_sponsorship_cfg_id = PUBLIC_AE_SPONSORSHIP_CFG_ID;
// const ae_no_account_id_token = PUBLIC_AE_NO_ACCOUNT_ID_TOKEN;
// const ae_event_id = PUBLIC_AE_EVENT_ID;
// const ae_sponsorship_cfg_id = PUBLIC_AE_SPONSORSHIP_CFG_ID;
// Loose bag-of-anything type used throughout the store. Once stores are broken into
// typed domain files, individual structs will have proper interfaces instead.
@@ -174,7 +174,7 @@ const ae_app_local_data_defaults: key_val = {
posts: {},
sponsorships: {
cfg_id: ae_sponsorship_cfg_id,
cfg_id: null,
for_type: null,
for_id: null,
@@ -350,7 +350,7 @@ const slct_obj_template: key_val = {
sponsorship_id: null,
sponsorship_obj: {},
sponsorship_obj_li: [],
sponsorship_cfg_id: ae_sponsorship_cfg_id,
sponsorship_cfg_id: null,
sponsorship_cfg_obj: {},
sponsorship_cfg_obj_li: [],
post_id: null,

View File

@@ -626,15 +626,14 @@ async function get_novi_group_moderators(
const requestOptions = { method: 'GET', headers: headers };
let allModeratorsRaw: any[] = [];
if (log_lvl) {
console.log('Jitsi: Fetching Novi group moderators...');
}
console.log('[DEBUG] Novi Moderator Group Fetch:');
console.log(' Group GUIDs:', group_guid_li);
console.log(' Novi API Root URL:', api_root_url);
for (const group_guid of group_guid_li) {
const url = `${api_root_url}/groups/${group_guid}/members?pageSize=200`;
console.log(
`Jitsi: Fetching moderator list for group ${group_guid} from: ${url}`
);
console.log(`[DEBUG] Fetching moderator list for group:`, group_guid);
console.log(' Request URL:', url);
try {
const response = await fetch(url, requestOptions);
@@ -647,42 +646,40 @@ async function get_novi_group_moderators(
groupModList = result.Results;
else if (Array.isArray(result.Members))
groupModList = result.Members;
else
else {
console.warn(
`Jitsi: Moderator list format unexpected for group ${group_guid}.`,
`[DEBUG] Moderator list format unexpected for group ${group_guid}. Raw result:`,
result
);
}
allModeratorsRaw = allModeratorsRaw.concat(groupModList);
if (log_lvl) {
console.log(
`Jitsi: Fetched ${groupModList.length} moderators from group ${group_guid}. Total: ${allModeratorsRaw.length}`
);
console.log(
`[DEBUG] Group ${group_guid}: fetched ${groupModList.length} moderators. Total so far: ${allModeratorsRaw.length}`
);
if (groupModList.length > 0) {
console.log(`[DEBUG] First 3 moderator objects:`, groupModList.slice(0, 3));
}
} else {
console.warn(
`Jitsi: Failed to fetch moderators for group ${group_guid}. Status: ${response.status}`
`[DEBUG] Failed to fetch moderators for group ${group_guid}. Status: ${response.status}`
);
}
} catch (error) {
console.error(
`Jitsi: Error fetching moderators for group ${group_guid}:`,
`[DEBUG] Error fetching moderators for group ${group_guid}:`,
error
);
}
}
if (allModeratorsRaw.length === 0) {
console.warn(
'Jitsi: No moderators found across all specified Novi groups.'
);
console.warn('[DEBUG] No moderators found across all specified Novi groups.');
}
if (log_lvl > 1) {
console.log(
'Jitsi: Fetched all raw moderators (combined):',
allModeratorsRaw
);
console.log('[DEBUG] Combined moderator list count:', allModeratorsRaw.length);
if (allModeratorsRaw.length > 0) {
console.log('[DEBUG] Example moderator IDs:', allModeratorsRaw.slice(0, 5).map(m => m?.UniqueID || m?.UniqueId || m?.DuesPayerUniqueID || m?.id || ''));
}
const modIdSet = new Set(
@@ -698,6 +695,7 @@ async function get_novi_group_moderators(
.filter(Boolean)
.map((id: string) => String(id).toLowerCase().trim())
);
console.log('[DEBUG] Final moderator ID set:', Array.from(modIdSet));
return modIdSet;
}