diff --git a/CHANGELOG.md b/CHANGELOG.md index 35bc57fe..7a704cac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- New views for Kids, History, Search, Subscriptions, and Docs. (PR #2129, PR #2153 by PR0M3TH3AN) +- Comprehensive test suite for Nostr features and UI components. (PR #2153 by PR0M3TH3AN) - Provider-agnostic S3 multipart upload helpers to support large file uploads and bucket management. (PR #1620 by PR0M3TH3AN) ### Changed +- Performance improvements for documentation scroll spy. (PR #2129 by PR0M3TH3AN) +- Optimized Nostr login speed. (PR #2153 by PR0M3TH3AN) +- Updated `nostr-tools`, `floating-ui`, and `crypto-helpers` dependencies. (PR #2153 by PR0M3TH3AN) - Major repository update and synchronization (Commit `b429b1c` by thePR0M3TH3AN) ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e64cecf9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,467 @@ +# CLAUDE.md — AI Assistant Guide for bitvid + +This document provides context and guidance for AI assistants working with the bitvid codebase. It explains the project structure, development workflows, conventions, and key architectural decisions. + +--- + +## Project Overview + +**bitvid** is a decentralized video sharing platform built on the Nostr protocol. It operates as a static client — no backend server, no custody of user keys, no server-side signing. All authentication and state management happens client-side or via connected Nostr signers. + +### Key Technologies +- **Nostr**: Decentralized identity and event protocol (NIP-07, NIP-33, NIP-44, NIP-46, NIP-51, NIP-71, etc.) +- **WebTorrent**: P2P video streaming fallback +- **Tailwind CSS**: Utility-first styling with custom design tokens +- **Playwright**: E2E and visual regression testing +- **Node.js 20+**: Development environment + +### License +GPL-3.0-or-later (see `LICENSE`) + +--- + +## Codebase Structure + +``` +bitvid/ +├── js/ # Main application JavaScript +│ ├── app.js # Main orchestrator (bitvidApp) +│ ├── nostr/ # Nostr protocol implementation +│ │ ├── client.js # Core Nostr client with signer adapters +│ │ ├── nip71.js # NIP-71 video note parsing +│ │ ├── sessionActor.js # Anonymous session actors for telemetry +│ │ └── adapters/ # NIP-07, NIP-46, nsec signer adapters +│ ├── services/ # Business logic services +│ │ └── playbackService.js # Video playback orchestration +│ ├── ui/ # UI components and controllers +│ ├── state/ # State management (cache.js) +│ ├── utils/ # Utility functions +│ ├── constants.js # Feature flags and WSS trackers +│ ├── config.js # Re-exports from instance config +│ ├── magnetUtils.js # Magnet URI helpers +│ ├── nostrEventSchemas.js # Event schema definitions (source of truth) +│ └── nostrClientFacade.js # Main Nostr client entry point +├── config/ +│ └── instance-config.js # Deployment configuration +├── css/ +│ ├── tokens.css # Design token definitions +│ ├── tailwind.source.css # Tailwind input file +│ └── tailwind.generated.css # Generated (gitignored) +├── tests/ # Test suites +│ ├── unit/ # Unit tests +│ ├── e2e/ # End-to-end tests +│ ├── visual/ # Visual regression tests +│ ├── nostr/ # Nostr-specific tests +│ └── moderation/ # Moderation feature tests +├── docs/ # Documentation +│ ├── nostr-event-schemas.md # Event schema reference +│ ├── playback-fallback.md # Playback architecture +│ ├── moderation/ # Moderation system docs +│ └── nips/ # NIP reference documentation +├── views/ # HTML view templates +├── components/ # HTML component templates +├── content/ # Markdown content (docs, roadmap) +├── torrent/ # WebTorrent integration +├── scripts/ # Build and utility scripts +└── .github/workflows/ # CI/CD configuration +``` + +--- + +## Essential Files to Know + +| File | Purpose | +|------|---------| +| `AGENTS.md` | AI agent guide with architectural rules and mission | +| `KNOWN_ISSUES.md` | Pre-existing test failures and environmental quirks | +| `CONTRIBUTING.md` | Developer setup and contribution guidelines | +| `js/app.js` | Main application orchestrator (`bitvidApp`) | +| `js/nostr/client.js` | Core Nostr client with signer registry | +| `js/nostrEventSchemas.js` | **Source of truth** for all Nostr event schemas | +| `js/constants.js` | Feature flags and runtime configuration | +| `config/instance-config.js` | Deployment-specific settings | +| `docs/nostr-event-schemas.md` | Event schema documentation | + +--- + +## Development Workflow + +### Initial Setup + +```bash +# Clone and install +git clone https://github.com/PR0M3TH3AN/bitvid.git +cd bitvid +npm ci # Use ci for exact lockfile deps + +# Build (generates Tailwind CSS) +npm run build + +# Start local server +python -m http.server 8000 +# or +npx serve +``` + +### Before Every PR + +```bash +# Required checks +npm run format # Format CSS/HTML/MD +npm run lint # All lint checks +npm run test:unit # Or use shards for faster feedback + +# Recommended for UI changes +npm run test:visual +``` + +### Available Scripts + +| Script | Purpose | +|--------|---------| +| `npm run build` | Full build (Tailwind + dist) | +| `npm run build:css` | Regenerate Tailwind only | +| `npm run format` | Format with Prettier | +| `npm run lint` | Run all linting checks | +| `npm run lint:css` | CSS token validation | +| `npm run lint:inline-styles` | Block inline styles | +| `npm run lint:tokens` | Design token enforcement | +| `npm run test:unit` | Full unit test suite | +| `npm run test:unit:shard1` | Shard 1/3 (faster local dev) | +| `npm run test:unit:shard2` | Shard 2/3 | +| `npm run test:unit:shard3` | Shard 3/3 | +| `npm run test:dm:unit` | Direct message unit tests | +| `npm run test:dm:integration` | DM integration tests | +| `npm run test:e2e` | Headless E2E tests | +| `npm run test:visual` | Visual regression tests | +| `node scripts/agent/validate-events.mjs` | Validate Nostr event schemas | + +--- + +## CI/CD Pipeline + +The CI workflow (`.github/workflows/ci.yml`) runs on every push to `main` and all PRs: + +| Job | Timeout | Description | +|-----|---------|-------------| +| `build` | 45 min | Lint, build, visual regression | +| `unit-tests` | 30 min | Sharded unit tests (3 shards) | +| `dm-unit-tests` | 20 min | Direct message unit tests | +| `dm-integration-tests` | 30 min | DM integration tests | +| `e2e-headless` | 45 min | Playwright E2E tests | + +**Concurrency**: `cancel-in-progress: true` — new commits cancel running jobs. + +--- + +## Code Conventions + +### Logging + +**Always use the logger utility**, never call `console.*` directly: + +```javascript +import { logger } from './utils/logger.js'; + +logger.user.error('User-facing error'); // Production visible +logger.dev.log('Debug info'); // Dev mode only (IS_DEV_MODE) +``` + +### Styling Rules + +1. **Token-first**: Use design tokens from `css/tokens.css`, never raw hex colors +2. **No inline styles**: `style=` attributes and `element.style` are blocked by lint +3. **Semantic tokens**: Use `bg`, `text`, `accent`, `border`, `danger`, etc. +4. **Theme scopes**: Toggle themes via `data-theme` attribute, not code + +### Nostr Event Schemas + +All Nostr events must be defined in `js/nostrEventSchemas.js`: + +```javascript +import { buildVideoPostEvent, NOTE_TYPES } from './nostrEventSchemas.js'; + +const event = buildVideoPostEvent({ + pubkey: hexPubkey, + created_at: Math.floor(Date.now() / 1000), + dTagValue: 'video-id', + content: { + version: 3, + title: 'My Video', + videoRootId: 'video-id', + url: 'https://example.com/video.mp4' + } +}); +``` + +### Magnet Handling + +**Critical rules** — magnets are fragile: + +```javascript +import { safeDecodeMagnet, normalizeAndAugmentMagnet } from './magnetUtils.js'; + +// Always decode first +const raw = safeDecodeMagnet(userInput); + +// Then normalize with hints +const enriched = normalizeAndAugmentMagnet(raw, { wsHint, xsHint }); + +// NEVER use new URL() on magnets — it corrupts hashes! +``` + +- WSS trackers only (from `js/constants.js`) +- Persist raw magnet strings, decode only at playback +- Include HTTPS `ws=`/`xs=` hints when available + +### Architecture Pattern + +**Separation of concerns**: + +- `bitvidApp` (js/app.js) — Orchestrator, manages app-wide state +- UI Controllers — Handle specific UI components (e.g., `ProfileModalController`) +- Services — Business logic (e.g., `playbackService.js`) +- State — Central truth in `js/state/cache.js` + +**Data flow**: +1. User action triggers controller event handler +2. Controller invokes callback provided by `bitvidApp` +3. `bitvidApp` executes logic, updates central state +4. `bitvidApp` notifies controller to re-render + +--- + +## Feature Flags + +Feature flags in `js/constants.js` gate experimental behavior: + +| Flag | Default | Purpose | +|------|---------|---------| +| `URL_FIRST_ENABLED` | Depends on config | Try URL before magnet | +| `FEATURE_WATCH_HISTORY_V2` | true | Watch history system | +| `FEATURE_PUBLISH_NIP71` | false | NIP-71 video publishing | +| `FEATURE_SEARCH_FILTERS` | Dev mode only | Advanced search | +| `FEATURE_TRUST_SEEDS` | true | Baseline trust seeds | + +Toggle at runtime: +```javascript +import { setUrlFirstEnabled } from './constants.js'; +setUrlFirstEnabled(false); // Disable URL-first playback +``` + +--- + +## Nostr Integration + +### Key NIPs Used + +| NIP | Purpose | +|-----|---------| +| NIP-07 | Browser extension signing | +| NIP-33 | Parameterized replaceable events (videos) | +| NIP-04 | Legacy DM encryption | +| NIP-44 | Modern encryption | +| NIP-46 | Remote signing | +| NIP-51 | Lists (mute, admin) | +| NIP-56 | Moderation reports | +| NIP-71 | Video events | + +### Signer Adapters + +```javascript +import { nostrClient } from './nostrClientFacade.js'; +import { setActiveSigner } from './nostr/client.js'; + +// Register a signer +setActiveSigner({ + pubkey, + signEvent: (event) => signer.sign(event), + nip04Encrypt: (target, text) => signer.nip04.encrypt(target, text), + nip04Decrypt: (actor, cipher) => signer.nip04.decrypt(actor, cipher), + // Optional: nip44Encrypt, nip44Decrypt +}); +``` + +### NIP-33 Addressing (Important!) + +Video events are addressed by `kind + pubkey + d-tag`: + +```javascript +// CORRECT: Use the d-tag for addressing +const dTag = event.tags.find(t => t[0] === 'd')?.[1]; + +// WRONG: Don't use logical IDs like videoRootId for relay lookups +``` + +--- + +## Testing Guidelines + +### Before Starting Work + +1. Read `KNOWN_ISSUES.md` for pre-existing failures +2. Run relevant test shard to verify baseline + +### Running Tests + +```bash +# Fast feedback during development +npm run test:unit:shard1 + +# Full suite before PR +npm run test:unit + +# Visual changes +npm run test:visual + +# DM features +npm run test:dm:unit && npm run test:dm:integration + +# E2E journeys +npm run test:e2e +``` + +### Test Artifacts + +Failed visual tests store artifacts in `artifacts/test-results/`: +- Screenshots +- Videos +- Traces + +View with: `./scripts/show-artifacts.sh` + +--- + +## Common Tasks + +### Adding a New Nostr Event Type + +1. Define schema in `js/nostrEventSchemas.js` +2. Add builder function +3. Document in `docs/nostr-event-schemas.md` +4. Add tests + +### Creating UI Components + +1. Create controller in `js/ui/` +2. Wire callbacks through `bitvidApp` +3. Read state via provided getters, never directly +4. Use design tokens for styling + +### Modifying Playback Behavior + +1. Check `js/services/playbackService.js` for stream handling +2. Update `js/constants.js` feature flags if needed +3. Test with both URL and magnet sources +4. Document rollback steps in PR + +--- + +## Troubleshooting + +### Build Issues + +```bash +# Browserslist warning +npx update-browserslist-db@latest + +# Tailwind not regenerating +rm css/tailwind.generated.css && npm run build:css +``` + +### Test Hangs + +- Unit tests may hang after completion — use `Ctrl+C` +- Use shards for faster local runs +- Set `UNIT_TEST_TIMEOUT_MS=120000` for slow tests + +### Lint Failures + +- **Inline styles**: Move to CSS classes or tokens +- **Hex colors**: Use design tokens +- **Design tokens**: Use `theme()` or existing utilities + +--- + +## Release Channels + +- **Main**: Production track — preserve UX, atomic commits, feature flags default off +- **Unstable**: Experimentation — gate risky features behind flags + +### Emergency Response + +If a change breaks playback or magnet handling: +1. Revert immediately +2. Note rollback in PR +3. Document in AGENTS.md + +--- + +## Key Documentation + +| Document | What It Covers | +|----------|---------------| +| `AGENTS.md` | AI agent rules, mission, architectural decisions | +| `CONTRIBUTING.md` | Setup, PR process, DCO | +| `KNOWN_ISSUES.md` | Pre-existing test failures | +| `docs/nostr-event-schemas.md` | Event definitions | +| `docs/playback-fallback.md` | URL-first strategy | +| `docs/moderation/README.md` | Moderation system | +| `docs/logging.md` | Logger usage | + +--- + +## Quick Reference + +### Import Patterns + +```javascript +// Nostr client +import { nostrClient } from './nostrClientFacade.js'; + +// View events (analytics) +import { recordVideoView } from './nostrViewEventsFacade.js'; + +// Watch history +import { updateWatchHistoryListWithDefaultClient } from './nostrWatchHistoryFacade.js'; + +// Event building +import { buildVideoPostEvent } from './nostrEventSchemas.js'; + +// Feature flags +import { URL_FIRST_ENABLED, setUrlFirstEnabled } from './constants.js'; + +// Logger +import { logger } from './utils/logger.js'; +``` + +### Content Schema v3 + +```javascript +{ + version: 3, + title: string, // Required + url?: string, // HTTPS video URL + magnet?: string, // WebTorrent magnet + thumbnail?: string, + description?: string, + mode: 'live' | 'dev', + isPrivate: boolean, + deleted: boolean, + videoRootId: string // Stable identifier +} +``` + +Validation: Must have `title` + at least one of `url` or `magnet`. + +--- + +## Final Notes + +1. **Read AGENTS.md first** — it contains mission-critical architectural decisions +2. **Check KNOWN_ISSUES.md** — avoid investigating pre-existing failures +3. **Use sharded tests** — faster feedback during development +4. **Never commit generated CSS** — `css/tailwind.generated.css` is gitignored +5. **Token-first styling** — no raw colors, no inline styles +6. **Keep magnets raw** — decode only at playback time +7. **Document rollback steps** — especially for playback changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1883c89f..7441c5a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ Automated agents contributing to this repository should follow these rules: - **Commit Messages**: Use the convention `type(scope): description (agent)` (e.g., `fix(ai): formatting (agent)` or `docs(ai): update quickstart (agent)`). - **Reference Issues**: Link to relevant issues in the PR description. - **Review AGENTS.md**: Always consult `AGENTS.md` for specific architectural guidelines (like URL-first playback, token-first design system) and constraints before starting work. -- **Efficiency**: Prefer running targeted or sharded tests (e.g., `npm run test:unit:shard1`) to conserve resources during iteration. +- **Efficiency**: Prefer running targeted or sharded tests (e.g., `npm run test:unit:shard1`, `shard2`, or `shard3`) to conserve resources during iteration. ## Submitting a Pull Request @@ -92,20 +92,24 @@ This workflow uses the `force-cancel` endpoint to ensure stalled runs are termin To set up the project locally: -1. **Install Dependencies**: +1. **Prerequisites**: + - **Node.js**: v22 or higher. + - **NPM**: v10 or higher (included with Node 22). + +2. **Install Dependencies**: Use `npm ci` to ensure you get the exact dependencies from `package-lock.json`. ```bash npm ci ``` -2. **Start the Application**: +3. **Start the Application**: ```bash npm start ``` - This builds the project and starts a local server. + This command runs a full production build (generating `dist/` and `css/tailwind.generated.css`) and starts a local server. **Or, build manually:** @@ -116,13 +120,20 @@ To set up the project locally: (The build command populates the `dist/` directory, which you can then serve locally.) -3. **Run Tests**: + **For CSS-only changes:** + If you are only iterating on styles, you can run the faster CSS build: + + ```bash + npm run build:css + ``` + +4. **Run Tests**: ```bash npm run test:unit ``` - _Note: Running the full suite (`npm run test:unit`) is resource-intensive, runs sequentially, and may time out in some environments. We strongly recommend using sharded runs for local development to save time: `npm run test:unit:shard1`._ + _Note: Running the full suite (`npm run test:unit`) is resource-intensive, runs sequentially, and may time out in some environments. We strongly recommend using sharded runs for local development to save time: `npm run test:unit:shard1`, `shard2`, or `shard3`._ You can also run end-to-end and visual tests: @@ -131,6 +142,12 @@ To set up the project locally: npm run test:visual ``` + To update visual regression baselines: + + ```bash + npm run test:visual:update + ``` + For Direct Message features, run: ```bash @@ -138,7 +155,13 @@ To set up the project locally: npm run test:dm:integration ``` -4. **Format & Lint**: + To aggregate telemetry from test logs: + + ```bash + npm run telemetry:aggregate + ``` + +5. **Format & Lint**: ```bash npm run format @@ -146,9 +169,9 @@ To set up the project locally: ``` - **Format**: Targets CSS, HTML, Markdown, and config files. (Note: JavaScript files are not currently auto-formatted by this command). - - **Lint**: Checks for CSS errors, hex color usage, inline styles, and design token compliance. (Note: There is no ESLint configuration for JavaScript logic; this step focuses on style and design system guards). + - **Lint**: Checks for CSS errors, hex color usage, inline styles, design tokens, and Tailwind guards. (Note: There is no ESLint configuration for JavaScript logic; this step focuses on style and design system guards). -5. **Git Hooks (Optional)**: +6. **Git Hooks (Optional)**: We provide a script to set up a git pre-commit hook that runs linting and CSS builds automatically before you commit. ```bash ./scripts/setup-pre-commit.sh diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 56b6b338..6573fb68 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -3,32 +3,12 @@ ## Tests ### Unit Tests -- `npm run test:unit` consistently times out in the CI environment due to resource constraints. Targeted testing (`test:dm:unit`, `test:dm:integration`) is recommended. +- `npm run test:unit` may time out in resource-constrained environments (like CI) when running the full suite serially. CI workflows use sharding (`test:unit:shardX`) to mitigate this. Targeted testing (`test:dm:unit`, `test:dm:integration`) is recommended for local development focus. (Last checked: 2025-02-23) + +### Skipped Tests +The following tests are skipped due to flakiness or environmental issues and should be investigated: + +- `tests/e2e/popover.spec.ts`: `keeps the bottom-right grid menu inside the viewport` (Visual regression layout issue: Floating UI `flip` middleware fails to respect viewport boundary in constrained height scenarios. Verified 2025-02-23, fix attempt failed.) ### Visual Regression Tests (`test:visual`) - **Artifact Retention**: Visual tests are configured to retain screenshots, traces, and videos on failure to assist with debugging. These artifacts can be found in `artifacts/test-results/`. -- **Moderation UI (`tests/visual/moderation.spec.ts`)**: The "Show anyway" button and related moderation badges fail to render in the test environment, causing `toBeVisible()` assertions to fail. The logic for creating the button appears correct (`allowOverride` is true), but the button element is not found in the DOM during the test execution. This might be due to a race condition in the test fixture's state initialization or an environment-specific rendering issue. -- **Sidebar Layout (`tests/visual/overlay-layers.spec.ts`)**: The `mobile sidebar shares desktop rail behavior` test fails. The expected margin for the app container does not match the computed margin in the test environment (diff of ~45px). Attempts to fix this by adjusting CSS margin calculations were incomplete and reverted to avoid instability. -- **Kitchen Sink (`tests/visual/kitchen-sink.spec.ts`)**: Minor pixel mismatch (width 1292 vs expected 1280) in snapshot comparison. - -### E2E Tests (`test:e2e`) -- **Modals (`tests/e2e/ui-modals.spec.ts`)**: - - `application-form` and `profile-modal` fail with 404 errors for requested resources (likely related to how `http-server` serves relative paths or nested components in the test runner). - - Several modals (`upload-modal`, `edit-video-modal`, etc.) timeout waiting for close actions or visibility checks. This might be due to the `reducedMotion` application interacting poorly with visibility checks or genuine performance issues in the test runner. - - `login-modal` fails to find the `[data-nsec-error]` element (it expects it to be visible but it remains hidden). -- **Popover (`tests/e2e/popover.spec.ts`)**: Fails with `ReferenceError: getPanelWithTriggerMetrics is not defined`. This helper function seems to be missing or not imported correctly in the test file. - -## CSS / Design System -- The global CSS (`css/tailwind.source.css`) was updated to fix a linter error by using `calc(theme("screens.md") - var(--breakpoint-edge-offset))` instead of raw pixels. - -## Retrospective & Future Improvements - -### Challenges -- **Generated Artifacts**: The build process modifies `css/tailwind.generated.css`, which creates noise in diffs and potential merge conflicts. It is recommended to ignore this file in version control or enforce a check that it matches the build output without committing it manually in every PR. -- **Test Environment Isolation**: Debugging visual test failures (like the Sidebar Layout and Moderation UI) was difficult without access to the live browser UI or artifacts. The reproduction script failed due to environment restrictions on launching browsers. -- **Test Flakiness**: E2E tests for modals are prone to timeouts, likely due to animation/transition handling or resource loading delays in the CI environment. Using `applyReducedMotion` consistently helps but doesn't solve all timing issues. - -### Suggestions -- **Commit Hooks**: Implement a pre-commit hook that runs the linter and potentially the build to ensure generated files are in sync, or configure CI to fail if they are not. -- **Dockerized Testing**: Running tests in a consistent Docker container with Xvfb properly configured would help reproduce environment-specific failures locally. -- **Visual Testing improvements**: Ensure that test fixtures (like `moderation/fixtures/index.html`) correctly force UI updates after state injection to avoid race conditions. diff --git a/LINT_ISSUES.md b/LINT_ISSUES.md deleted file mode 100644 index 68701f53..00000000 --- a/LINT_ISSUES.md +++ /dev/null @@ -1,20 +0,0 @@ -# Lint Issues Report - -## Summary -Automated linting and formatting checks were run. No auto-fixable formatting issues were found. However, one linting error persists which requires manual intervention. - -## Issues - -### css/tailwind.source.css - -The `npm run lint:tokens` check failed with the following error: - -``` -css/tailwind.source.css:5757 → 767.98px - @media (max-width: 767.98px) { -``` - -**Recommendation:** -This raw measurement (`767.98px`) should be moved into `css/tokens.css` as a design token, or the linter configuration (`scripts/check-design-tokens.mjs`) should be updated to whitelist this specific value if it represents a standard responsive breakpoint for mobile layout fixes. - -Note: This error currently prevents `npm run lint` from completing successfully, meaning subsequent checks (`lint:tailwind-brackets`, `lint:tailwind-colors`) are skipped in the standard run script. These subsequent checks were run manually and passed. diff --git a/ONBOARDING_REPORT.md b/ONBOARDING_REPORT.md index 8076bd6b..16704e93 100644 --- a/ONBOARDING_REPORT.md +++ b/ONBOARDING_REPORT.md @@ -1,44 +1,29 @@ -# Onboarding Report +# Developer Onboarding Audit - 2025-02-18 -## Execution Summary +This document summarizes the findings from the developer onboarding audit. -| Command | Status | Notes | -| :--- | :--- | :--- | -| `npm ci` | ✅ Passed | Installed 315 packages. | -| `npm run build:css` | ✅ Passed | CSS built successfully. Warning: `Browserslist: caniuse-lite is outdated`. | -| `npm run format` | ✅ Passed | No formatting changes needed. | -| `npm run lint` | ✅ Passed | All lint checks passed. | -| `npm run test:unit` | ✅ Fixed | `tests/hashtag-preferences.test.mjs` and `tests/nostr-login-permissions.test.mjs` failures were fixed in this PR. | +## Steps Executed -## Detailed Findings & Fixes +1. `npm ci`: Successful. +2. `npm run build:css`: Successful. +3. `npm run format`: Successful. -### Unit Test Failures & Timeouts +## Environment -1. **`tests/hashtag-preferences.test.mjs`**: Failed due to a race condition in the mock `fetchListIncrementally` implementation. - * **Fix Applied**: Updated the mock to return the event consistently. Added `process.exit(0)` to prevent hanging. +- **Node.js Version:** v22.22.0 +- **Dependencies:** Installed successfully via `npm ci`. +- **CSS Build:** Tailwind CSS bundle generated successfully. -2. **`tests/nostr-login-permissions.test.mjs`**: Failed due to incorrect permission request logic and timed out due to open handles. - * **Fix Applied**: Updated `js/nostr/client.js` to request `DEFAULT_NIP07_PERMISSION_METHODS` instead of `CORE_METHODS`. Refactored test file to use `describe`/`it`/`after` with explicit `process.exit(0)`. +## Findings -3. **`tests/nostr-private-key-signer.test.mjs`**: Timed out during CI. - * **Fix Applied**: Added `after(() => setTimeout(() => process.exit(0), 100))` to force exit. +- **Setup Reliability:** The onboarding steps are robust and reliable. No errors were encountered. +- **Documentation:** `CONTRIBUTING.md` and `README.md` provide clear instructions. +- **Dev Container:** The project includes a valid `.devcontainer/devcontainer.json` which uses `mcr.microsoft.com/devcontainers/javascript-node:22`. This is recommended for a consistent development environment. +- **Node Version:** While `package.json` does not explicitly enforce a Node.js version, the `.devcontainer` configuration implies a dependency on Node 22. Adding an `engines` field to `package.json` would improve environment consistency. +- **Build Commands:** The README suggests `npm run build:css` for CSS verification, which works as expected. For full builds, `npm run build` or `npm start` should be used. +- **Warnings:** A warning about `browserslist` (`caniuse-lite is outdated`) was observed during `npm run build:css`. This is documented in `CONTRIBUTING.md` under Troubleshooting. -4. **`tests/nostr-send-direct-message.test.mjs`**: Susceptible to similar hangs. - * **Fix Applied**: Added `after(() => setTimeout(() => process.exit(0), 100))` to force exit. +## Recommendations implemented -5. **`tests/nostr-service-access-control.test.mjs`**: Caused hangs due to mock tool cleanup. - * **Fix Applied**: Added `setTimeout(() => process.exit(0), 100)` in `after` hook. - -### Test Runner Robustness - -* **`scripts/run-unit-tests.mjs`**: Updated to explicitly call `process.exit(0)` upon successful completion of all tests. This acts as a global safeguard against lingering open handles (e.g., from `nostr-tools` relays or timers) causing CI timeouts. - -### Browserslist Warning - -The build command emitted: `Browserslist: caniuse-lite is outdated`. -* **Action Taken**: Ran `npx update-browserslist-db@latest`. - -### Documentation - -* Updated `CONTRIBUTING.md` to recommend sharded tests (`npm run test:unit:shard1`). -* Clarified in `CONTRIBUTING.md` that `npm run format` does not target JavaScript files. +- Added `"engines": { "node": ">=22" }` to `package.json` to align with `.devcontainer` and ensure environment consistency. +- Updated `CONTRIBUTING.md` to explicitly mention the Node.js version requirement and clarify build commands. diff --git a/README.md b/README.md index a98c8d66..91d96ca8 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,6 @@ Pick the flow that matches your source material. Supported S3 upload modes inclu 1. **Browser-held S3 keys (trusted operator only):** Enter your S3 credentials in the Storage tab to upload directly from the browser. > **Security Warning:** This mode requires storing encrypted credentials in the browser's IndexedDB. While keys are encrypted at rest, they are decrypted in memory during use. Use this mode only on self-hosted, trusted deployments where you control the environment. Do not enter high-value credentials on public or untrusted instances. 2. **Manual upload via provider console:** Upload your file to your storage provider (e.g., R2, S3) manually, then paste the public URL into the upload form. -3. **Operator-provided presigned manifests:** Use a presigned JSON manifest prepared externally to authorize the upload without exposing long-lived credentials to the browser. **Upload File (direct S3 upload)**: If you are using **Mode 1** (Browser-held keys), enter your credentials in the guided form or Storage tab. Optionally expand the **Advanced options** accordion to override pathing or access controls, then drop media files for bitvid to upload through the S3 API. The modal tracks progress, applies your metadata selections, auto-fills the primary `imeta` variant once the upload completes, and publishes the resulting URL back into the note automatically. @@ -156,7 +155,7 @@ To run **bitvid** locally: npm start ``` - This command builds the project (generating `css/tailwind.generated.css`) and starts a local server. + This command runs a full production build (generating `dist/` and `css/tailwind.generated.css`) and starts a local server. 4. Open the site in your browser: @@ -164,13 +163,15 @@ To run **bitvid** locally: http://localhost:3000 ``` + _(Note: The default port is 3000, but `npx serve` may select a different port if 3000 is in use. Check your terminal output.)_ + _Note: If you prefer manual steps, you can run `npm run build` followed by `npx serve dist`._ #### Quick Reference - **Run unit tests**: `npm run test:unit` -- **Format code**: `npm run format` (CSS, HTML, MD, Config) -- **Lint code**: `npm run lint` (Styles, Tokens, Hex - no JS logic) +- **Format code**: `npm run format` (CSS, HTML, MD, Config - no JS logic) +- **Lint code**: `npm run lint` (Styles, Tokens, Hex, Tailwind guards - no ESLint/logic linting) ### Dev Container @@ -182,9 +183,9 @@ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full developer guide. **Verify your work:** -- **Run unit tests**: `npm run test:unit` (Required before PRs). _Tip: Use `npm run test:unit:shard1` for faster local feedback._ -- **Format code**: `npm run format` (Required before PRs) -- **Lint code**: `npm run lint` (Checks for CSS, hex colors, inline styles) +- **Run unit tests**: `npm run test:unit` (Required before PRs). _Tip: Use `npm run test:unit:shard1`, `shard2`, or `shard3` for faster local feedback._ +- **Format code**: `npm run format` (Required before PRs - only formats CSS, HTML, MD, and Config) +- **Lint code**: `npm run lint` (Checks for CSS, hex colors, inline styles, design tokens, and Tailwind guards - no ESLint/logic linting) **Other commands:** @@ -192,6 +193,8 @@ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full developer guide. - **Run DM integration tests**: `npm run test:dm:integration` - **Run headless E2E tests**: `npm run test:e2e` - **Run visual regression tests**: `npm run test:visual` +- **Update visual baselines**: `npm run test:visual:update` +- **Aggregate telemetry**: `npm run telemetry:aggregate` - **Cancel CI runs**: See [`docs/cancelling-ci-runs.md`](docs/cancelling-ci-runs.md) for a script to clear pending workflows. ### Docs navigation & TOC updates @@ -288,7 +291,7 @@ package scripts to keep formatting, linting, and generated output consistent: ```bash npm run format # normalize CSS/HTML/MD/Config sources with Prettier + tailwindcss plugin npm run lint:css # enforce design token usage and guard against raw hex colors - npm run build # run the Tailwind build locally (output remains gitignored) + npm run build # run the full production build (generates dist/ and css/tailwind.generated.css) ``` - **No hard-coded colors:** Follow the token-first rules in `AGENTS.md`—reach @@ -302,7 +305,7 @@ npm run format # format CSS/HTML/MD with Prettier + tailwindcss plugi npm run lint # run CSS, hex color, inline-style, design-token, and Tailwind color/bracket guards in one pass npm run lint:css # enforce token usage and forbid raw hex colors npm run lint:inline-styles # fail CI if inline style attributes or element.style usage slip in -npm run build # run the Tailwind build (delegates to npm run build:css) +npm run build # run the full production build (cleans dist/, runs build:css, and copies assets) npm run build:beacon # bundle torrent/dist assets and re-run the inline-style guard npm run build:beacon:bundle # bundle beacon assets without running the guard (rarely needed) ``` diff --git a/apply_fix.py b/apply_fix.py new file mode 100644 index 00000000..c4c847e1 --- /dev/null +++ b/apply_fix.py @@ -0,0 +1,130 @@ + +import os + +filepath = 'js/dmDecryptor.js' + +with open(filepath, 'r') as f: + content = f.read() + +search_block = """ for (const decryptor of ordered) { + for (const remotePubkey of remoteCandidates) { + try { + const plaintext = await decryptor.decrypt(remotePubkey, ciphertext, { + event, + stage: "content", + remotePubkey, + }); + + if (typeof plaintext === "string") { + const resolvedScheme = + normalizeScheme(hints.algorithms?.[0]) || + normalizeScheme(decryptor.scheme) || + ""; + return buildDecryptResult({ + ok: true, + event, + message: { + ...cloneEvent(event), + content: plaintext, + }, + plaintext, + recipients, + senderPubkey, + actorPubkey, + decryptor, + scheme: resolvedScheme, + }); + } + } catch (error) { + errors.push({ + scheme: decryptor.scheme || "", + source: decryptor.source || "", + stage: "content", + remotePubkey, + error, + }); + } + } + } + + return buildDecryptResult({ + ok: false, + event, + actorPubkey, + errors, + });""" + +replace_block = """ const attemptDecryption = async (decryptor, remotePubkey) => { + try { + const plaintext = await decryptor.decrypt(remotePubkey, ciphertext, { + event, + stage: "content", + remotePubkey, + }); + + if (typeof plaintext === "string") { + const resolvedScheme = + normalizeScheme(hints.algorithms?.[0]) || + normalizeScheme(decryptor.scheme) || + ""; + return buildDecryptResult({ + ok: true, + event, + message: { + ...cloneEvent(event), + content: plaintext, + }, + plaintext, + recipients, + senderPubkey, + actorPubkey, + decryptor, + scheme: resolvedScheme, + }); + } + throw new Error("Decrypted content was not a valid string."); + } catch (error) { + throw { + scheme: decryptor.scheme || "", + source: decryptor.source || "", + stage: "content", + remotePubkey, + error, + }; + } + }; + + const attempts = []; + for (const decryptor of ordered) { + for (const remotePubkey of remoteCandidates) { + attempts.push(attemptDecryption(decryptor, remotePubkey)); + } + } + + try { + return await Promise.any(attempts); + } catch (aggregateError) { + const aggregatedErrors = aggregateError.errors || []; + errors.push(...aggregatedErrors); + + return buildDecryptResult({ + ok: false, + event, + actorPubkey, + errors, + }); + }""" + +if search_block in content: + new_content = content.replace(search_block, replace_block) + with open(filepath, 'w') as f: + f.write(new_content) + print("Successfully replaced content.") +else: + print("Search block not found.") + # Debug: print what we have roughly where it should be + start_idx = content.find("for (const decryptor of ordered) {") + if start_idx != -1: + print("Found start of block, but full block mismatch.") + print("Existing content snippet:") + print(content[start_idx:start_idx+500]) diff --git a/artifacts/error-aggregates.json b/artifacts/error-aggregates.json index 2b9699de..f331df79 100644 --- a/artifacts/error-aggregates.json +++ b/artifacts/error-aggregates.json @@ -1,13 +1,24 @@ [ { - "fingerprint": "e1da49a7", - "title": "load defers permission-required decrypts until explicitly enabled", + "fingerprint": "00257328", + "title": "Load Test Bottleneck: Cryptographic Bottleneck (Avg Sign Time 5.87ms > 5ms)", "count": 1, "sources": [ - "unit-test:test_unit.log" + "load-test:load-report-20260204.json" ], - "severity": "High", - "stack": "TestContext. (file://$REPO/tests/hashtag-preferences.test.mjs:300:14)\n async Test.run (node:internal/test_runner/test:1054:7)\n async Test.processPendingSubtests (node:internal/test_runner/test:744:7)\n ...\n# Subtest: interest and disinterest lists remain exclusive", - "owner": "QA Team" + "severity": "Medium", + "stack": "Bottleneck: Cryptographic Bottleneck (Avg Sign Time 5.87ms > 5ms)\nRemediation: Client-side event signing is slow. Review cryptographic library (nostr-tools) usage or client hardware capabilities.", + "owner": "P2P Team" + }, + { + "fingerprint": "bc06bea0", + "title": "Load Test Bottleneck: Cryptographic Bottleneck (Avg Sign Time 5.53ms > 5ms)", + "count": 1, + "sources": [ + "load-test:load-report.json" + ], + "severity": "Medium", + "stack": "Bottleneck: Cryptographic Bottleneck (Avg Sign Time 5.53ms > 5ms)\nRemediation: Client-side event signing is slow. Review cryptographic library (nostr-tools) usage or client hardware capabilities.", + "owner": "P2P Team" } ] \ No newline at end of file diff --git a/artifacts/load-report-20260204.json b/artifacts/load-report-20260204.json new file mode 100644 index 00000000..b5d3b1ac --- /dev/null +++ b/artifacts/load-report-20260204.json @@ -0,0 +1,392 @@ +{ + "meta": { + "clients": 100, + "duration": 60, + "targetRatePerClient": 0.5 + }, + "results": { + "totalSent": 2900, + "totalReceivedByObserver": 2958, + "totalErrors": 0, + "errorRate": 0, + "avgLatencyMs": 8.5, + "maxLatencyMs": 42, + "avgSignTimeMs": 5.869976568218849, + "hot_functions": [ + { + "name": "Event Signing (Client)", + "avgTimeMs": 5.869976568218849, + "calls": 2900 + } + ] + }, + "bottlenecks": [ + "Cryptographic Bottleneck (Avg Sign Time 5.87ms > 5ms)" + ], + "metrics": { + "timestamps": [ + "2026-02-04T19:06:10.186Z", + "2026-02-04T19:06:11.827Z", + "2026-02-04T19:06:12.831Z", + "2026-02-04T19:06:14.256Z", + "2026-02-04T19:06:15.314Z", + "2026-02-04T19:06:16.285Z", + "2026-02-04T19:06:17.281Z", + "2026-02-04T19:06:18.280Z", + "2026-02-04T19:06:19.280Z", + "2026-02-04T19:06:20.280Z", + "2026-02-04T19:06:21.281Z", + "2026-02-04T19:06:22.284Z", + "2026-02-04T19:06:23.281Z", + "2026-02-04T19:06:24.282Z", + "2026-02-04T19:06:25.282Z", + "2026-02-04T19:06:26.282Z", + "2026-02-04T19:06:27.288Z", + "2026-02-04T19:06:28.283Z", + "2026-02-04T19:06:29.290Z", + "2026-02-04T19:06:30.284Z", + "2026-02-04T19:06:31.289Z", + "2026-02-04T19:06:32.283Z", + "2026-02-04T19:06:33.288Z", + "2026-02-04T19:06:34.283Z", + "2026-02-04T19:06:35.303Z", + "2026-02-04T19:06:36.287Z", + "2026-02-04T19:06:37.287Z", + "2026-02-04T19:06:38.288Z", + "2026-02-04T19:06:39.288Z", + "2026-02-04T19:06:40.288Z", + "2026-02-04T19:06:41.288Z", + "2026-02-04T19:06:42.288Z", + "2026-02-04T19:06:43.289Z", + "2026-02-04T19:06:44.289Z", + "2026-02-04T19:06:45.289Z", + "2026-02-04T19:06:46.289Z", + "2026-02-04T19:06:47.290Z", + "2026-02-04T19:06:48.290Z", + "2026-02-04T19:06:49.290Z", + "2026-02-04T19:06:50.291Z", + "2026-02-04T19:06:51.292Z", + "2026-02-04T19:06:52.292Z", + "2026-02-04T19:06:53.292Z", + "2026-02-04T19:06:54.292Z", + "2026-02-04T19:06:55.292Z", + "2026-02-04T19:06:56.292Z", + "2026-02-04T19:06:57.292Z", + "2026-02-04T19:06:58.293Z", + "2026-02-04T19:06:59.292Z", + "2026-02-04T19:07:00.292Z", + "2026-02-04T19:07:01.293Z", + "2026-02-04T19:07:02.293Z", + "2026-02-04T19:07:03.293Z", + "2026-02-04T19:07:04.294Z", + "2026-02-04T19:07:05.293Z", + "2026-02-04T19:07:06.302Z", + "2026-02-04T19:07:07.293Z", + "2026-02-04T19:07:08.294Z" + ], + "cpu": [ + 1.2891, + 75.4605, + 1.1246, + 110.9914, + 13.9201, + 98.5274, + 6.3944, + 64.1486, + 6.0738, + 63.4177, + 6.2085, + 66.0653, + 5.9001, + 62.525, + 5.7544, + 69.5987, + 6.6041, + 60.1274, + 6.8777, + 66.1227, + 6.6576, + 61.7225, + 6.5692, + 62.586, + 10.3549, + 69.6933, + 5.8595, + 62.5952, + 5.8479, + 69.4633, + 5.9227, + 59.5779, + 5.9434, + 58.9378, + 5.7777, + 65.1409, + 5.7253, + 58.8193, + 6.3755, + 64.0124, + 6.9732, + 61.4041, + 5.724, + 58.3382, + 5.5851, + 58.7335, + 6.0968, + 59.5833, + 5.6418, + 60.6467, + 5.742, + 60.2272, + 5.617, + 59.7526, + 6.6313, + 63.0884, + 5.8035, + 61.0101 + ], + "memory": [ + 14.21099853515625, + 12.053665161132812, + 13.577583312988281, + 11.293220520019531, + 13.9764404296875, + 13.597808837890625, + 12.902915954589844, + 17.456069946289062, + 15.924842834472656, + 13.108901977539062, + 11.620040893554688, + 16.124671936035156, + 14.659011840820312, + 11.750862121582031, + 17.709686279296875, + 14.97283935546875, + 15.023307800292969, + 17.9583740234375, + 18.069061279296875, + 13.631416320800781, + 13.709136962890625, + 16.736785888671875, + 16.66020965576172, + 12.300872802734375, + 19.781394958496094, + 15.29693603515625, + 13.89215087890625, + 18.39887237548828, + 16.973892211914062, + 14.178886413574219, + 12.703651428222656, + 17.223411560058594, + 15.726631164550781, + 12.830970764160156, + 18.700477600097656, + 15.878158569335938, + 14.343246459960938, + 18.87677764892578, + 17.446983337402344, + 14.474784851074219, + 13.017341613769531, + 17.500404357910156, + 16.13226318359375, + 13.164146423339844, + 19.092933654785156, + 16.134384155273438, + 14.735084533691406, + 19.123802185058594, + 17.696151733398438, + 14.804244995117188, + 13.346839904785156, + 17.821983337402344, + 16.359207153320312, + 13.470359802246094, + 19.442214965820312, + 16.46966552734375, + 15.026473999023438, + 19.590980529785156 + ], + "latency": [ + 12, + 10, + 11, + 11, + 42, + 11, + 6, + 7, + 6, + 7, + 7, + 10, + 6, + 7, + 6, + 6, + 11, + 7, + 14, + 8, + 13, + 6, + 12, + 7, + 23, + 6, + 7, + 7, + 6, + 7, + 7, + 6, + 6, + 7, + 7, + 6, + 7, + 6, + 7, + 7, + 7, + 7, + 7, + 7, + 6, + 7, + 7, + 7, + 7, + 6, + 7, + 6, + 6, + 6, + 7, + 15, + 6, + 7 + ], + "throughput": [ + 1, + 101, + 1, + 101, + 11, + 91, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93, + 9, + 93 + ], + "avgSignTime": [ + 0, + 6.090444670000015, + 0, + 10.09357892, + 10.334544000000195, + 9.704539101123578, + 5.840137249999998, + 5.749988358695646, + 5.691442000000279, + 5.735636086956576, + 5.9182750000002216, + 5.890629923913069, + 5.588866250000137, + 5.850060869565309, + 5.676161750001484, + 5.6353527065217035, + 5.762921555554688, + 5.728557747252825, + 6.10907399999956, + 5.90219461538456, + 5.862068444444351, + 5.8458819999998, + 5.776876333333348, + 5.937595384615213, + 9.20721633333354, + 6.482018351648579, + 5.615201250000155, + 5.785851652173891, + 5.64396275000081, + 5.771455956521964, + 5.550119749998885, + 5.582937739129746, + 5.7709488749997035, + 5.643515130434737, + 5.749425124999107, + 6.210867510869914, + 5.636921000000257, + 5.659433999999949, + 6.089611750000586, + 5.987438336956668, + 6.957270124999013, + 5.891856141305059, + 5.555353749998176, + 5.548610880434985, + 5.590769124999497, + 5.557453586956794, + 5.865140124999016, + 5.646670826087163, + 5.6239324999996825, + 5.723344728261272, + 5.6432206250010495, + 5.7358725434777345, + 5.5553546250012005, + 5.647155880434224, + 5.598165125001287, + 5.917939250000058, + 5.63759575000131, + 5.651182891304653 + ] + }, + "remediation": [ + "Client-side event signing is slow. Review cryptographic library (nostr-tools) usage or client hardware capabilities." + ], + "requiresSecurityReview": true +} \ No newline at end of file diff --git a/artifacts/load-report.json b/artifacts/load-report.json index 557d68bc..3d9429a8 100644 --- a/artifacts/load-report.json +++ b/artifacts/load-report.json @@ -1,1342 +1,40 @@ { - "timestamp": "2026-01-30T19:23:23.380Z", "config": { - "duration": 600, - "clients": 1000, - "rate": 0.1, - "relay": "ws://localhost:8889", - "force": false + "clients": 100, + "duration": 10, + "rate": 5, + "relay": null, + "outputDir": "artifacts" }, - "metrics": { - "total_sent": 67937, - "total_received": 67937, - "errors": 0, - "throughput_sent": 113.22833333333334, - "throughput_recv": 113.22833333333334, - "latency_ms": { - "p50": 0.7058579999720678, - "p95": 10.000404999999773, - "p99": 17.838965999952052 - }, - "operation_times_ms": { - "build_avg": 0.05418018332126935, - "sign_avg": 5.689074433740087 - }, - "resource_usage": [ - { - "timestamp": 1769800408393, - "cpuPercent": 98.87, - "rss": 128929792, - "heapUsed": 27565784 - }, - { - "timestamp": 1769800413393, - "cpuPercent": 77.84, - "rss": 129880064, - "heapUsed": 34596048 - }, - { - "timestamp": 1769800418393, - "cpuPercent": 79.05, - "rss": 130666496, - "heapUsed": 24418720 - }, - { - "timestamp": 1769800423412, - "cpuPercent": 68.1, - "rss": 131616768, - "heapUsed": 26651824 - }, - { - "timestamp": 1769800428416, - "cpuPercent": 68.3, - "rss": 132272128, - "heapUsed": 32014632 - }, - { - "timestamp": 1769800433415, - "cpuPercent": 71.68, - "rss": 133439488, - "heapUsed": 39027624 - }, - { - "timestamp": 1769800438415, - "cpuPercent": 68.12, - "rss": 133963776, - "heapUsed": 31851440 - }, - { - "timestamp": 1769800443415, - "cpuPercent": 68.38, - "rss": 134881280, - "heapUsed": 30796224 - }, - { - "timestamp": 1769800448417, - "cpuPercent": 67.53, - "rss": 135467008, - "heapUsed": 37501864 - }, - { - "timestamp": 1769800453431, - "cpuPercent": 67.46, - "rss": 136384512, - "heapUsed": 30178232 - }, - { - "timestamp": 1769800458431, - "cpuPercent": 67.56, - "rss": 136908800, - "heapUsed": 35300656 - }, - { - "timestamp": 1769800463453, - "cpuPercent": 68.5, - "rss": 137564160, - "heapUsed": 40512944 - }, - { - "timestamp": 1769800468454, - "cpuPercent": 66.47, - "rss": 138350592, - "heapUsed": 34617536 - }, - { - "timestamp": 1769800473456, - "cpuPercent": 68.07, - "rss": 138874880, - "heapUsed": 34175320 - }, - { - "timestamp": 1769800478457, - "cpuPercent": 69.08, - "rss": 139206656, - "heapUsed": 43370360 - }, - { - "timestamp": 1769800483459, - "cpuPercent": 67.76, - "rss": 139993088, - "heapUsed": 31293608 - }, - { - "timestamp": 1769800488459, - "cpuPercent": 68.51, - "rss": 140517376, - "heapUsed": 39513552 - }, - { - "timestamp": 1769800493463, - "cpuPercent": 66.84, - "rss": 141565952, - "heapUsed": 44947080 - }, - { - "timestamp": 1769800498464, - "cpuPercent": 67.53, - "rss": 142221312, - "heapUsed": 40645208 - }, - { - "timestamp": 1769800503464, - "cpuPercent": 67.04, - "rss": 143007744, - "heapUsed": 34660104 - }, - { - "timestamp": 1769800508464, - "cpuPercent": 70.2, - "rss": 110510080, - "heapUsed": 19394888 - }, - { - "timestamp": 1769800513466, - "cpuPercent": 70.9, - "rss": 114966528, - "heapUsed": 22866984 - }, - { - "timestamp": 1769800518467, - "cpuPercent": 69.22, - "rss": 114900992, - "heapUsed": 21641624 - }, - { - "timestamp": 1769800523487, - "cpuPercent": 68.48, - "rss": 123289600, - "heapUsed": 21701520 - }, - { - "timestamp": 1769800528488, - "cpuPercent": 67.79, - "rss": 123289600, - "heapUsed": 21066832 - }, - { - "timestamp": 1769800533489, - "cpuPercent": 87.87, - "rss": 123453440, - "heapUsed": 24166680 - }, - { - "timestamp": 1769800538493, - "cpuPercent": 79.12, - "rss": 123715584, - "heapUsed": 20638592 - }, - { - "timestamp": 1769800543492, - "cpuPercent": 67.03, - "rss": 123715584, - "heapUsed": 21115008 - }, - { - "timestamp": 1769800548492, - "cpuPercent": 68.59, - "rss": 123846656, - "heapUsed": 26173776 - }, - { - "timestamp": 1769800553497, - "cpuPercent": 69.29, - "rss": 123977728, - "heapUsed": 21893240 - }, - { - "timestamp": 1769800558498, - "cpuPercent": 67.45, - "rss": 123977728, - "heapUsed": 23793616 - }, - { - "timestamp": 1769800563498, - "cpuPercent": 69.93, - "rss": 124370944, - "heapUsed": 24486152 - }, - { - "timestamp": 1769800568498, - "cpuPercent": 68.03, - "rss": 124497920, - "heapUsed": 28060720 - }, - { - "timestamp": 1769800573504, - "cpuPercent": 69.21, - "rss": 135639040, - "heapUsed": 19644112 - }, - { - "timestamp": 1769800578504, - "cpuPercent": 67.64, - "rss": 135753728, - "heapUsed": 23168840 - }, - { - "timestamp": 1769800583505, - "cpuPercent": 68.46, - "rss": 135753728, - "heapUsed": 23567648 - }, - { - "timestamp": 1769800588505, - "cpuPercent": 66.08, - "rss": 136015872, - "heapUsed": 30220536 - }, - { - "timestamp": 1769800593506, - "cpuPercent": 68.64, - "rss": 136146944, - "heapUsed": 29166240 - }, - { - "timestamp": 1769800598510, - "cpuPercent": 67.2, - "rss": 136278016, - "heapUsed": 31001224 - }, - { - "timestamp": 1769800603510, - "cpuPercent": 67.16, - "rss": 136916992, - "heapUsed": 27286488 - }, - { - "timestamp": 1769800608524, - "cpuPercent": 69.12, - "rss": 137011200, - "heapUsed": 27836360 - }, - { - "timestamp": 1769800613523, - "cpuPercent": 69.48, - "rss": 137089024, - "heapUsed": 29609712 - }, - { - "timestamp": 1769800618524, - "cpuPercent": 67.65, - "rss": 137318400, - "heapUsed": 31698560 - }, - { - "timestamp": 1769800623524, - "cpuPercent": 67.81, - "rss": 137449472, - "heapUsed": 35162608 - }, - { - "timestamp": 1769800628525, - "cpuPercent": 77.11, - "rss": 137572352, - "heapUsed": 24566720 - }, - { - "timestamp": 1769800633528, - "cpuPercent": 66.64, - "rss": 137572352, - "heapUsed": 31296984 - }, - { - "timestamp": 1769800638528, - "cpuPercent": 71.5, - "rss": 137633792, - "heapUsed": 33178264 - }, - { - "timestamp": 1769800643530, - "cpuPercent": 72.22, - "rss": 137555968, - "heapUsed": 29510312 - }, - { - "timestamp": 1769800648535, - "cpuPercent": 69.73, - "rss": 137687040, - "heapUsed": 20463504 - }, - { - "timestamp": 1769800653540, - "cpuPercent": 73.07, - "rss": 137482240, - "heapUsed": 22523016 - }, - { - "timestamp": 1769800658544, - "cpuPercent": 83.55, - "rss": 137613312, - "heapUsed": 21362464 - }, - { - "timestamp": 1769800663546, - "cpuPercent": 82.75, - "rss": 137613312, - "heapUsed": 30223376 - }, - { - "timestamp": 1769800668546, - "cpuPercent": 70.55, - "rss": 137613312, - "heapUsed": 23737448 - }, - { - "timestamp": 1769800673552, - "cpuPercent": 72.63, - "rss": 137646080, - "heapUsed": 28685832 - }, - { - "timestamp": 1769800678551, - "cpuPercent": 72.07, - "rss": 137760768, - "heapUsed": 29728088 - }, - { - "timestamp": 1769800683552, - "cpuPercent": 70.15, - "rss": 137883648, - "heapUsed": 26280344 - }, - { - "timestamp": 1769800688552, - "cpuPercent": 68.15, - "rss": 138022912, - "heapUsed": 23680208 - }, - { - "timestamp": 1769800693552, - "cpuPercent": 68.38, - "rss": 138059776, - "heapUsed": 35051736 - }, - { - "timestamp": 1769800698557, - "cpuPercent": 72.54, - "rss": 138141696, - "heapUsed": 22925320 - }, - { - "timestamp": 1769800703557, - "cpuPercent": 66.52, - "rss": 139005952, - "heapUsed": 25668264 - }, - { - "timestamp": 1769800708557, - "cpuPercent": 72.33, - "rss": 139034624, - "heapUsed": 29180936 - }, - { - "timestamp": 1769800713563, - "cpuPercent": 67.37, - "rss": 138342400, - "heapUsed": 19031016 - }, - { - "timestamp": 1769800718579, - "cpuPercent": 67.17, - "rss": 138051584, - "heapUsed": 23527264 - }, - { - "timestamp": 1769800723580, - "cpuPercent": 66.2, - "rss": 138063872, - "heapUsed": 19208208 - }, - { - "timestamp": 1769800728595, - "cpuPercent": 67.86, - "rss": 138063872, - "heapUsed": 29635016 - }, - { - "timestamp": 1769800733599, - "cpuPercent": 66.58, - "rss": 138063872, - "heapUsed": 29285568 - }, - { - "timestamp": 1769800738601, - "cpuPercent": 66.31, - "rss": 138063872, - "heapUsed": 18970344 - }, - { - "timestamp": 1769800743610, - "cpuPercent": 84.47, - "rss": 138162176, - "heapUsed": 30175352 - }, - { - "timestamp": 1769800748619, - "cpuPercent": 100.35, - "rss": 138162176, - "heapUsed": 31960160 - }, - { - "timestamp": 1769800753625, - "cpuPercent": 71.09, - "rss": 138162176, - "heapUsed": 19883048 - }, - { - "timestamp": 1769800758626, - "cpuPercent": 68.1, - "rss": 138162176, - "heapUsed": 32798488 - }, - { - "timestamp": 1769800763633, - "cpuPercent": 67.31, - "rss": 138043392, - "heapUsed": 33173864 - }, - { - "timestamp": 1769800768637, - "cpuPercent": 64.71, - "rss": 138436608, - "heapUsed": 21554176 - }, - { - "timestamp": 1769800773641, - "cpuPercent": 67.04, - "rss": 138457088, - "heapUsed": 28869680 - }, - { - "timestamp": 1769800778641, - "cpuPercent": 67.24, - "rss": 138457088, - "heapUsed": 23845072 - }, - { - "timestamp": 1769800783641, - "cpuPercent": 66.02, - "rss": 138457088, - "heapUsed": 35019256 - }, - { - "timestamp": 1769800788641, - "cpuPercent": 68.34, - "rss": 138178560, - "heapUsed": 18935936 - }, - { - "timestamp": 1769800793642, - "cpuPercent": 67.76, - "rss": 138178560, - "heapUsed": 28676728 - }, - { - "timestamp": 1769800798664, - "cpuPercent": 69.91, - "rss": 138178560, - "heapUsed": 30650856 - }, - { - "timestamp": 1769800803665, - "cpuPercent": 69.54, - "rss": 138280960, - "heapUsed": 22407576 - }, - { - "timestamp": 1769800808670, - "cpuPercent": 67.72, - "rss": 138178560, - "heapUsed": 26816608 - }, - { - "timestamp": 1769800813672, - "cpuPercent": 66.15, - "rss": 138178560, - "heapUsed": 31712016 - }, - { - "timestamp": 1769800818672, - "cpuPercent": 67.09, - "rss": 138293248, - "heapUsed": 29121264 - }, - { - "timestamp": 1769800823673, - "cpuPercent": 67.32, - "rss": 138293248, - "heapUsed": 29455488 - }, - { - "timestamp": 1769800828674, - "cpuPercent": 67.67, - "rss": 138293248, - "heapUsed": 23782504 - }, - { - "timestamp": 1769800833678, - "cpuPercent": 76.22, - "rss": 138293248, - "heapUsed": 30435008 - }, - { - "timestamp": 1769800838678, - "cpuPercent": 68.24, - "rss": 138178560, - "heapUsed": 22950416 - }, - { - "timestamp": 1769800843685, - "cpuPercent": 65.65, - "rss": 138178560, - "heapUsed": 32858264 - }, - { - "timestamp": 1769800848686, - "cpuPercent": 67.59, - "rss": 138178560, - "heapUsed": 32993384 - }, - { - "timestamp": 1769800853686, - "cpuPercent": 67.99, - "rss": 138588160, - "heapUsed": 20642960 - }, - { - "timestamp": 1769800858691, - "cpuPercent": 66.76, - "rss": 138588160, - "heapUsed": 17981320 - }, - { - "timestamp": 1769800863695, - "cpuPercent": 68.06, - "rss": 138850304, - "heapUsed": 24566168 - }, - { - "timestamp": 1769800868702, - "cpuPercent": 78.18, - "rss": 138850304, - "heapUsed": 25033424 - }, - { - "timestamp": 1769800873710, - "cpuPercent": 67.12, - "rss": 138850304, - "heapUsed": 23658768 - }, - { - "timestamp": 1769800878714, - "cpuPercent": 66.95, - "rss": 138850304, - "heapUsed": 22591632 - }, - { - "timestamp": 1769800883727, - "cpuPercent": 67.52, - "rss": 138870784, - "heapUsed": 29299880 - }, - { - "timestamp": 1769800888728, - "cpuPercent": 73.44, - "rss": 138870784, - "heapUsed": 33535984 - }, - { - "timestamp": 1769800893729, - "cpuPercent": 65.52, - "rss": 138870784, - "heapUsed": 33219584 - }, - { - "timestamp": 1769800898734, - "cpuPercent": 72.07, - "rss": 138870784, - "heapUsed": 27287960 - }, - { - "timestamp": 1769800903737, - "cpuPercent": 76.55, - "rss": 138870784, - "heapUsed": 33981696 - }, - { - "timestamp": 1769800908738, - "cpuPercent": 82.79, - "rss": 138870784, - "heapUsed": 21982736 - }, - { - "timestamp": 1769800913739, - "cpuPercent": 67.25, - "rss": 138870784, - "heapUsed": 34866968 - }, - { - "timestamp": 1769800918744, - "cpuPercent": 70.02, - "rss": 138870784, - "heapUsed": 32091032 - }, - { - "timestamp": 1769800923744, - "cpuPercent": 70.83, - "rss": 138870784, - "heapUsed": 33959656 - }, - { - "timestamp": 1769800928743, - "cpuPercent": 67.68, - "rss": 138870784, - "heapUsed": 31213960 - }, - { - "timestamp": 1769800933743, - "cpuPercent": 70.4, - "rss": 138870784, - "heapUsed": 21561336 - }, - { - "timestamp": 1769800938748, - "cpuPercent": 66.59, - "rss": 138870784, - "heapUsed": 24414992 - }, - { - "timestamp": 1769800943747, - "cpuPercent": 66.76, - "rss": 139395072, - "heapUsed": 20712904 - }, - { - "timestamp": 1769800948748, - "cpuPercent": 72.43, - "rss": 139489280, - "heapUsed": 20984928 - }, - { - "timestamp": 1769800953747, - "cpuPercent": 68.31, - "rss": 139489280, - "heapUsed": 25491832 - }, - { - "timestamp": 1769800958751, - "cpuPercent": 68.78, - "rss": 139489280, - "heapUsed": 26668904 - }, - { - "timestamp": 1769800963753, - "cpuPercent": 69.51, - "rss": 139489280, - "heapUsed": 27040168 - }, - { - "timestamp": 1769800968754, - "cpuPercent": 74.29, - "rss": 139489280, - "heapUsed": 24260336 - }, - { - "timestamp": 1769800973759, - "cpuPercent": 71.4, - "rss": 139489280, - "heapUsed": 26206168 - }, - { - "timestamp": 1769800978759, - "cpuPercent": 67.96, - "rss": 139489280, - "heapUsed": 25080296 - }, - { - "timestamp": 1769800983758, - "cpuPercent": 69.22, - "rss": 139489280, - "heapUsed": 33168920 - }, - { - "timestamp": 1769800988766, - "cpuPercent": 67.49, - "rss": 139489280, - "heapUsed": 28926152 - }, - { - "timestamp": 1769800993770, - "cpuPercent": 66.87, - "rss": 139489280, - "heapUsed": 35722208 - }, - { - "timestamp": 1769800998770, - "cpuPercent": 71.97, - "rss": 139489280, - "heapUsed": 29773024 - } - ], - "relay_resource_usage": [ - { - "timestamp": 1769800408467, - "cpuPercent": 18.3, - "memPercent": 1 - }, - { - "timestamp": 1769800413413, - "cpuPercent": 13.1, - "memPercent": 1 - }, - { - "timestamp": 1769800418405, - "cpuPercent": 10.6, - "memPercent": 1 - }, - { - "timestamp": 1769800423424, - "cpuPercent": 9.1, - "memPercent": 1 - }, - { - "timestamp": 1769800428436, - "cpuPercent": 8.1, - "memPercent": 1 - }, - { - "timestamp": 1769800433435, - "cpuPercent": 7.7, - "memPercent": 1.2 - }, - { - "timestamp": 1769800438453, - "cpuPercent": 7.3, - "memPercent": 1.2 - }, - { - "timestamp": 1769800443444, - "cpuPercent": 6.8, - "memPercent": 1.3 - }, - { - "timestamp": 1769800448457, - "cpuPercent": 6.5, - "memPercent": 1.3 - }, - { - "timestamp": 1769800453452, - "cpuPercent": 6.1, - "memPercent": 1.3 - }, - { - "timestamp": 1769800458476, - "cpuPercent": 5.9, - "memPercent": 1.3 - }, - { - "timestamp": 1769800463467, - "cpuPercent": 5.7, - "memPercent": 1.3 - }, - { - "timestamp": 1769800468474, - "cpuPercent": 5.5, - "memPercent": 1.3 - }, - { - "timestamp": 1769800473485, - "cpuPercent": 5.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800478480, - "cpuPercent": 5.2, - "memPercent": 1.4 - }, - { - "timestamp": 1769800483478, - "cpuPercent": 5.1, - "memPercent": 1.4 - }, - { - "timestamp": 1769800488478, - "cpuPercent": 5, - "memPercent": 1.4 - }, - { - "timestamp": 1769800493485, - "cpuPercent": 4.9, - "memPercent": 1.4 - }, - { - "timestamp": 1769800498482, - "cpuPercent": 4.9, - "memPercent": 1.4 - }, - { - "timestamp": 1769800503490, - "cpuPercent": 4.8, - "memPercent": 1.4 - }, - { - "timestamp": 1769800508476, - "cpuPercent": 4.7, - "memPercent": 1.4 - }, - { - "timestamp": 1769800513479, - "cpuPercent": 4.6, - "memPercent": 1.4 - }, - { - "timestamp": 1769800518478, - "cpuPercent": 4.6, - "memPercent": 1.4 - }, - { - "timestamp": 1769800523499, - "cpuPercent": 4.6, - "memPercent": 1.4 - }, - { - "timestamp": 1769800528501, - "cpuPercent": 4.5, - "memPercent": 1.4 - }, - { - "timestamp": 1769800533512, - "cpuPercent": 4.5, - "memPercent": 1.4 - }, - { - "timestamp": 1769800538504, - "cpuPercent": 4.5, - "memPercent": 1.4 - }, - { - "timestamp": 1769800543504, - "cpuPercent": 4.5, - "memPercent": 1.4 - }, - { - "timestamp": 1769800548504, - "cpuPercent": 4.4, - "memPercent": 1.4 - }, - { - "timestamp": 1769800553509, - "cpuPercent": 4.4, - "memPercent": 1.4 - }, - { - "timestamp": 1769800558525, - "cpuPercent": 4.4, - "memPercent": 1.4 - }, - { - "timestamp": 1769800563510, - "cpuPercent": 4.4, - "memPercent": 1.4 - }, - { - "timestamp": 1769800568510, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800573524, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800578534, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800583517, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800588517, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800593518, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800598571, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800603521, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800608536, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800613550, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800618569, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800623536, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800628536, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800633542, - "cpuPercent": 4.2, - "memPercent": 1.4 - }, - { - "timestamp": 1769800638563, - "cpuPercent": 4.2, - "memPercent": 1.4 - }, - { - "timestamp": 1769800643557, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800648562, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800653556, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800658573, - "cpuPercent": 4.3, - "memPercent": 1.4 - }, - { - "timestamp": 1769800663568, - "cpuPercent": 4.4, - "memPercent": 1.4 - }, - { - "timestamp": 1769800668569, - "cpuPercent": 4.4, - "memPercent": 1.4 - }, - { - "timestamp": 1769800673573, - "cpuPercent": 4.4, - "memPercent": 1.4 - }, - { - "timestamp": 1769800678574, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800683577, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800688564, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800693579, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800698569, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800703568, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800708577, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800713575, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800718594, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800723592, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800728607, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800733626, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800738613, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800743664, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800748673, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800753638, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800758645, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800763644, - "cpuPercent": 4.4, - "memPercent": 1.5 - }, - { - "timestamp": 1769800768649, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800773659, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800778653, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800783652, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800788653, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800793669, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800798676, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800803677, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800808682, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800813714, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800818684, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800823691, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800828686, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800833690, - "cpuPercent": 4.3, - "memPercent": 1.5 - }, - { - "timestamp": 1769800838690, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800843697, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800848715, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800853711, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800858710, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800863714, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800868713, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800873722, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800878727, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800883745, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800888739, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800893742, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800898760, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800903760, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800908750, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800913750, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800918756, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800923771, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800928755, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800933755, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800938774, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800943759, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800948760, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800953760, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800958782, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800963766, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800968766, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800973787, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800978770, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800983770, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800988777, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800993784, - "cpuPercent": 4.2, - "memPercent": 1.5 - }, - { - "timestamp": 1769800998799, - "cpuPercent": 4.2, - "memPercent": 1.5 - } - ], - "error_breakdown": {} + "timestamp": "2026-02-07T19:37:19.456Z", + "durationSec": 10.459, + "throughput": 114.35127641265896, + "totalSent": 1197, + "totalAccepted": 1196, + "totalRejected": 0, + "totalErrors": 0, + "latency": { + "p50": 1, + "p95": 2, + "p99": 10, + "min": 0, + "max": 25, + "avg": 0.7842809364548495 }, - "bottlenecks": [], - "remediation": [] + "signingTime": { + "avg": 5.981282806182139, + "max": 23.78902300000027 + }, + "resources": [ + { + "time": 5443, + "cpuUser": 4805459, + "cpuSystem": 365368, + "rss": 96088064, + "heapTotal": 31481856, + "heapUsed": 13615216 + } + ], + "topBottlenecks": [], + "proposedRemediation": [] } \ No newline at end of file diff --git a/artifacts/npm-audit.json b/artifacts/npm-audit.json index 61b513b8..bc7a36c7 100644 --- a/artifacts/npm-audit.json +++ b/artifacts/npm-audit.json @@ -11,12 +11,12 @@ "total": 0 }, "dependencies": { - "prod": 121, - "dev": 220, - "optional": 27, + "prod": 110, + "dev": 255, + "optional": 53, "peer": 0, "peerOptional": 0, - "total": 341 + "total": 391 } } } diff --git a/artifacts/npm-outdated.json b/artifacts/npm-outdated.json index e7b476cb..5c53a455 100644 --- a/artifacts/npm-outdated.json +++ b/artifacts/npm-outdated.json @@ -1,29 +1,22 @@ { "@playwright/test": { - "current": "1.58.0", - "wanted": "1.58.1", - "latest": "1.58.1", + "current": "1.58.1", + "wanted": "1.58.2", + "latest": "1.58.2", "dependent": "app", "location": "/app/node_modules/@playwright/test" }, - "autoprefixer": { - "current": "10.4.23", - "wanted": "10.4.24", - "latest": "10.4.24", - "dependent": "app", - "location": "/app/node_modules/autoprefixer" - }, "esbuild": { - "current": "0.25.12", - "wanted": "0.25.12", - "latest": "0.27.2", + "current": "0.27.2", + "wanted": "0.27.3", + "latest": "0.27.3", "dependent": "app", "location": "/app/node_modules/esbuild" }, "nostr-tools": { - "current": "2.20.0", - "wanted": "2.22.1", - "latest": "2.22.1", + "current": "2.19.4", + "wanted": "2.19.4", + "latest": "2.23.0", "dependent": "app", "location": "/app/node_modules/nostr-tools" }, @@ -35,9 +28,9 @@ "location": "/app/node_modules/pixelmatch" }, "playwright": { - "current": "1.58.0", - "wanted": "1.58.1", - "latest": "1.58.1", + "current": "1.58.1", + "wanted": "1.58.2", + "latest": "1.58.2", "dependent": "app", "location": "/app/node_modules/playwright" }, @@ -49,12 +42,19 @@ "location": "/app/node_modules/prettier-plugin-tailwindcss" }, "stylelint": { - "current": "16.26.1", + "current": "16.12.0", "wanted": "16.26.1", - "latest": "17.1.0", + "latest": "17.1.1", "dependent": "app", "location": "/app/node_modules/stylelint" }, + "stylelint-config-standard": { + "current": "36.0.1", + "wanted": "36.0.1", + "latest": "40.0.0", + "dependent": "app", + "location": "/app/node_modules/stylelint-config-standard" + }, "tailwindcss": { "current": "3.4.19", "wanted": "3.4.19", diff --git a/benchmarks/dm_decrypt_bench.js b/benchmarks/dm_decrypt_bench.js new file mode 100644 index 00000000..98a6965a --- /dev/null +++ b/benchmarks/dm_decrypt_bench.js @@ -0,0 +1,85 @@ + +import { decryptDM } from "../js/dmDecryptor.js"; + +// Valid 64-char hex strings +const WRAP_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001"; +const SEAL_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000002"; +const SENDER_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000003"; +const RECIPIENT_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000004"; + +const sealEvent = { + id: "seal-event", + pubkey: SEAL_PUBKEY, + content: "seal-ciphertext", + created_at: Math.floor(Date.now() / 1000), +}; + +const rumorEvent = { + id: "rumor-event", + pubkey: SENDER_PUBKEY, + content: "Hello World", + created_at: Math.floor(Date.now() / 1000), + tags: [["p", RECIPIENT_PUBKEY]], +}; + +const wrapEvent = { + kind: 1059, + pubkey: WRAP_PUBKEY, + content: "wrap-ciphertext", + created_at: Math.floor(Date.now() / 1000), + tags: [["p", RECIPIENT_PUBKEY]], +}; + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const createDecryptor = (name, shouldFail, delayMs) => { + return { + scheme: "nip44", + supportsGiftWrap: true, + decrypt: async (pubkey, ciphertext, context) => { + await delay(delayMs); + if (shouldFail) { + throw new Error(`${name} failed`); + } + + // If we are unwrapping the outer wrap, return the seal + if (context.stage === "wrap") { + return JSON.stringify(sealEvent); + } + // If we are unwrapping the seal, return the rumor + if (context.stage === "seal") { + return JSON.stringify(rumorEvent); + } + + return ""; + }, + }; +}; + +async function runBenchmark() { + console.log("Running DM Decrypt Benchmark..."); + + const decryptors = [ + createDecryptor("D1", true, 100), + createDecryptor("D2", true, 100), + createDecryptor("D3", false, 50), + ]; + + const context = { + actorPubkey: RECIPIENT_PUBKEY, + decryptors: decryptors, + }; + + const start = performance.now(); + const result = await decryptDM(wrapEvent, context); + const end = performance.now(); + + console.log(`Result OK: ${result.ok}`); + if (!result.ok) { + const errorMessages = result.errors.map(e => ({stage: e.stage, message: e.error.message})); + console.log("Errors:", JSON.stringify(errorMessages, null, 2)); + } + console.log(`Time taken: ${(end - start).toFixed(2)}ms`); +} + +runBenchmark(); diff --git a/benchmarks/feed_engine_bench.js b/benchmarks/feed_engine_bench.js new file mode 100644 index 00000000..c9b05679 --- /dev/null +++ b/benchmarks/feed_engine_bench.js @@ -0,0 +1,76 @@ + +import { createFeedEngine } from "../js/feedEngine/index.js"; +import { performance } from "perf_hooks"; + +// Mock logger +const logger = { + log: () => {}, + warn: () => {}, + error: () => {}, +}; + +// Polyfill localStorage to silence errors +if (typeof globalThis.localStorage === "undefined") { + globalThis.localStorage = { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + }; +} + +async function runBenchmark() { + const engine = createFeedEngine({ logger }); + const feedName = "bench-feed"; + + // Create items + const itemCount = 100000; + const items = []; + for (let i = 0; i < itemCount; i++) { + items.push({ + video: { id: `v${i}`, created_at: Date.now() }, + metadata: { some: "data" }, + }); + } + + // Define stages + const stages = []; + + // 10 stages that return null (no change) - STRICT PASS THROUGH + for (let i = 0; i < 10; i++) { + stages.push(async (items) => { + return null; + }); + } + + // 10 stages that return the same items array (identity) - STRICT PASS THROUGH + for (let i = 0; i < 10; i++) { + stages.push(async (items) => { + return items; + }); + } + + engine.registerFeed(feedName, { + source: async () => [...items], + stages: stages, + }); + + console.log(`Starting benchmark with ${itemCount} items and ${stages.length} stages...`); + + const iterations = 50; + const start = performance.now(); + + for (let i = 0; i < iterations; i++) { + await engine.runFeed(feedName); + if (i % 5 === 0) process.stdout.write("."); + } + + const end = performance.now(); + const totalTime = end - start; + const avgTime = totalTime / iterations; + + console.log("\n"); + console.log(`Total time: ${totalTime.toFixed(2)}ms`); + console.log(`Average time per run: ${avgTime.toFixed(2)}ms`); +} + +runBenchmark().catch(console.error); diff --git a/benchmarks/nip46_normalization_bench.js b/benchmarks/nip46_normalization_bench.js new file mode 100644 index 00000000..e9fb002f --- /dev/null +++ b/benchmarks/nip46_normalization_bench.js @@ -0,0 +1,28 @@ + +import { normalizeNip46CiphertextPayload } from "../js/nostr/nip46Client.js"; + +const ITERATIONS = 100; +const ARRAY_SIZE = 200; + +function runBenchmark() { + const largeArray = Array.from({ length: ARRAY_SIZE }, (_, i) => `string_${i}`); + + // Wrap in a structure that triggers the recursive coerce and then the array handling + const payload = { + data: largeArray + }; + + const start = performance.now(); + + for (let i = 0; i < ITERATIONS; i++) { + normalizeNip46CiphertextPayload(payload); + } + + const end = performance.now(); + const duration = end - start; + + console.log(`Time taken for ${ITERATIONS} iterations with array size ${ARRAY_SIZE}: ${duration.toFixed(2)}ms`); + console.log(`Average time per iteration: ${(duration / ITERATIONS).toFixed(2)}ms`); +} + +runBenchmark(); diff --git a/benchmarks/profile_cache_bench.js b/benchmarks/profile_cache_bench.js new file mode 100644 index 00000000..d50af766 --- /dev/null +++ b/benchmarks/profile_cache_bench.js @@ -0,0 +1,88 @@ +import { profileCache } from "../js/state/profileCache.js"; + +class LegacyProfileCache { + constructor() { + this.memoryCache = new Map(); + } + + set(pubkey, section, data) { + this.memoryCache.set(`${pubkey}:${section}`, data); + } + + clearMemoryCache(pubkey) { + const prefix = `${pubkey}:`; + for (const key of this.memoryCache.keys()) { + if (key.startsWith(prefix)) { + this.memoryCache.delete(key); + } + } + } +} + +function generatePubkey(i) { + return i.toString(16).padStart(64, '0'); +} + +function runBenchmark() { + const legacy = new LegacyProfileCache(); + + // Clean actual cache just in case + profileCache.memoryCache.clear(); + + const PUBKEY_COUNT = 1000; + const SECTIONS_PER_PUBKEY = 20; + const TARGET_PUBKEY = generatePubkey(500); // Middle one + + console.log(`Setting up caches with ${PUBKEY_COUNT} pubkeys x ${SECTIONS_PER_PUBKEY} sections...`); + + // Populate + for (let i = 0; i < PUBKEY_COUNT; i++) { + const pubkey = generatePubkey(i); + for (let s = 0; s < SECTIONS_PER_PUBKEY; s++) { + const section = `section-${s}`; + const data = { value: `data-${i}-${s}` }; + + // Legacy + legacy.set(pubkey, section, data); + + // Actual (Optimized) + // We use setMemoryDataForPubkey to bypass storage/logic overhead + profileCache.setMemoryDataForPubkey(pubkey, section, data); + } + } + + console.log(`Total entries: ${PUBKEY_COUNT * SECTIONS_PER_PUBKEY}`); + + // Measure Legacy + const startLegacy = performance.now(); + legacy.clearMemoryCache(TARGET_PUBKEY); + const endLegacy = performance.now(); + const timeLegacy = endLegacy - startLegacy; + + // Measure Optimized + const startOptimized = performance.now(); + profileCache.clearMemoryCache(TARGET_PUBKEY); + const endOptimized = performance.now(); + const timeOptimized = endOptimized - startOptimized; + + console.log(`\nResults:`); + console.log(`Legacy (Linear Scan): ${timeLegacy.toFixed(4)} ms`); + console.log(`Optimized (Nested Map): ${timeOptimized.toFixed(4)} ms`); + + if (timeLegacy > timeOptimized) { + const factor = timeLegacy / (timeOptimized || 0.001); // avoid div by zero + console.log(`Improvement: ~${factor.toFixed(1)}x faster`); + } else { + console.log("No improvement detected (dataset might be too small or optimized code has overhead)"); + } + + // Verification + if (profileCache.memoryCache.has(TARGET_PUBKEY)) { + console.error("FAIL: Optimized cache did not clear the pubkey!"); + process.exit(1); + } else { + console.log("PASS: Optimized cache cleared successfully."); + } +} + +runBenchmark(); diff --git a/benchmarks/profile_cache_storage_bench.js b/benchmarks/profile_cache_storage_bench.js new file mode 100644 index 00000000..feffb2a1 --- /dev/null +++ b/benchmarks/profile_cache_storage_bench.js @@ -0,0 +1,66 @@ +import { profileCache } from "../js/state/profileCache.js"; + +// Polyfill localStorage +const store = new Map(); +global.localStorage = { + getItem: (key) => store.get(key) || null, + setItem: (key, value) => store.set(key, value), + removeItem: (key) => store.delete(key), + clear: () => store.clear(), +}; + +// Polyfill requestIdleCallback +global.requestIdleCallback = (cb) => setTimeout(() => { + cb({ + timeRemaining: () => 50, + didTimeout: false + }); +}, 0); + +// Polyfill window (might be needed by logger or others) +if (typeof window === 'undefined') { + global.window = global; +} + +function generateLargeData() { + const data = []; + // Generate roughly 2MB of JSON data + for (let i = 0; i < 20000; i++) { + data.push({ id: i, content: "x".repeat(100) }); + } + return data; +} + +async function runBenchmark() { + const pubkey = "0".repeat(64); + const section = "watchHistory"; + const data = generateLargeData(); + + // Set active profile to ensure logic runs + profileCache.setActiveProfile(pubkey); + + console.log("Benchmarking setProfileData with large payload..."); + + // Warmup + profileCache.setProfileData(pubkey, section, [{id: 1}]); + + const start = performance.now(); + // We call it multiple times to amplify the effect, but a single call with 2MB is enough to show blocking + profileCache.setProfileData(pubkey, section, data); + const end = performance.now(); + + console.log(`Execution time (Synchronous part): ${(end - start).toFixed(4)} ms`); + + // Wait for idle callback + await new Promise(resolve => setTimeout(resolve, 50)); + + // Verify data is "stored" + const stored = localStorage.getItem(profileCache.getStorageKey(pubkey, section)); + if (stored) { + console.log("Data successfully stored in localStorage (length: " + stored.length + ")"); + } else { + console.log("Data NOT found in localStorage"); + } +} + +runBenchmark(); diff --git a/benchmarks/scroll_spy_bench.js b/benchmarks/scroll_spy_bench.js new file mode 100644 index 00000000..862b20d1 --- /dev/null +++ b/benchmarks/scroll_spy_bench.js @@ -0,0 +1,61 @@ + +import { performance } from 'perf_hooks'; + +// Mock data +const NUM_HEADINGS = 100; +const SCROLL_EVENTS = 10000; + +const headings = []; +for (let i = 0; i < NUM_HEADINGS; i++) { + headings.push({ + id: `heading-${i}`, + // Mock getBoundingClientRect + getBoundingClientRect: () => ({ + top: (i * 100) - currentScrollY, // Simple layout simulation + }) + }); +} + +let currentScrollY = 0; +let activeId = ""; + +// The function to benchmark (copied/adapted from js/docsView.js) +const updateActiveFromScroll = () => { + const offset = 96; + let nextId = headings[0]?.id || ""; + + // In the real code, this runs on every scroll (throttled by rAF) + // We simulate the logic here. + for (const heading of headings) { + const top = heading.getBoundingClientRect().top - offset; + if (top <= 0) { + nextId = heading.id; + } else { + break; + } + } + + if (nextId && nextId !== activeId) { + activeId = nextId; + // setActiveSection logic (simplified) + } +}; + +console.log(`Benchmarking legacy scroll spy logic...`); +console.log(`Headings: ${NUM_HEADINGS}`); +console.log(`Scroll events simulated: ${SCROLL_EVENTS}`); + +const start = performance.now(); + +for (let i = 0; i < SCROLL_EVENTS; i++) { + // Simulate scrolling down + currentScrollY += 5; + updateActiveFromScroll(); +} + +const end = performance.now(); +const duration = end - start; + +console.log(`Total execution time: ${duration.toFixed(2)}ms`); +console.log(`Average time per scroll event: ${(duration / SCROLL_EVENTS).toFixed(4)}ms`); +console.log(`Note: This benchmark only measures JS execution time. Real browser performance is worse due to layout thrashing (getBoundingClientRect).`); diff --git a/benchmarks/sorters_bench.js b/benchmarks/sorters_bench.js new file mode 100644 index 00000000..502a92b4 --- /dev/null +++ b/benchmarks/sorters_bench.js @@ -0,0 +1,45 @@ + +import { createExploreDiversitySorter } from "../js/feedEngine/sorters.js"; + +const NUM_ITEMS = 1000; // Large enough to see the difference between N^3 and N^2 +const TAG_POOL = ["cats", "dogs", "nostr", "bitcoin", "art", "music", "news", "tech", "coding", "python", "javascript", "react", "vue", "angular", "svelte", "deno", "node", "linux", "macos", "windows"]; + +function randomTags() { + const count = Math.floor(Math.random() * 5) + 1; + const tags = new Set(); + while (tags.size < count) { + tags.add(TAG_POOL[Math.floor(Math.random() * TAG_POOL.length)]); + } + return Array.from(tags).map(t => ["t", t]); +} + +function buildItem(i) { + return { + video: { + id: `vid-${i}`, + tags: randomTags(), + rootCreatedAt: 1000 + i, + }, + metadata: { + exploreScore: Math.random(), + }, + }; +} + +async function runBenchmark() { + const items = []; + for (let i = 0; i < NUM_ITEMS; i++) { + items.push(buildItem(i)); + } + + const sorter = createExploreDiversitySorter({ lambda: 0.7 }); + + console.log(`Sorting ${NUM_ITEMS} items...`); + const start = performance.now(); + await sorter(items, { addWhy: () => {} }); + const end = performance.now(); + + console.log(`Time: ${(end - start).toFixed(2)}ms`); +} + +runBenchmark(); diff --git a/benchmarks/watch_history_publish_bench.js b/benchmarks/watch_history_publish_bench.js new file mode 100644 index 00000000..ab562bb2 --- /dev/null +++ b/benchmarks/watch_history_publish_bench.js @@ -0,0 +1,69 @@ + +import { createWatchHistoryManager } from "../js/nostr/watchHistory.js"; + +async function main() { + console.log("Starting WatchHistoryManager benchmark..."); + + // Mock dependencies + const mockSigner = { + signEvent: async (event) => { + // Simulate signing latency + await new Promise(resolve => setTimeout(resolve, 10)); + return { ...event, sig: "mock-sig" }; + }, + nip04Encrypt: async (pubkey, plaintext) => { + await new Promise(resolve => setTimeout(resolve, 5)); + return "mock-ciphertext"; + } + }; + + const mockPool = { + publish: (relays, event) => { + return { + on: (status, cb) => { + if (status === 'ok') { + // Simulate network latency + setTimeout(cb, 50); + } + } + } + } + }; + + const deps = { + ensureNostrTools: async () => ({ + nip04: { encrypt: async () => "encrypted" }, + utils: { hexToBytes: () => new Uint8Array(32) } + }), + getCachedNostrTools: () => ({ + nip04: { encrypt: async () => "encrypted" }, + utils: { hexToBytes: () => new Uint8Array(32) } + }), + getActivePubkey: () => "00".repeat(32), + getSessionActor: () => null, + resolveActiveSigner: () => mockSigner, + getPool: () => mockPool, + signEventWithPrivateKey: async () => { + await new Promise(resolve => setTimeout(resolve, 10)); + return { sig: "mock-sig" }; + }, + shouldRequestExtensionPermissions: () => false, + }; + + const manager = createWatchHistoryManager(deps); + + // Create records for 12 months + const records = {}; + for (let i = 1; i <= 12; i++) { + const month = `2023-${String(i).padStart(2, '0')}`; + records[month] = [{ type: 'e', value: `item-${i}`, watchedAt: 1672531200 + i * 100 }]; + } + + const start = Date.now(); + await manager.publishRecords(records, { actorPubkey: "00".repeat(32) }); + const duration = Date.now() - start; + + console.log(`Published 12 months in ${duration}ms`); +} + +main().catch(console.error); diff --git a/bitvid-working-webtorrent/.devcontainer/devcontainer.json b/bitvid-working-webtorrent/.devcontainer/devcontainer.json deleted file mode 100644 index 007b07f4..00000000 --- a/bitvid-working-webtorrent/.devcontainer/devcontainer.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "bitvid", - "image": "mcr.microsoft.com/devcontainers/javascript-node:22", - "features": { - "ghcr.io/devcontainers/features/github-cli:1": {} - }, - "postCreateCommand": "npm ci && npm run build && npx playwright install", - "customizations": { - "vscode": { - "extensions": [ - "esbenp.prettier-vscode", - "bradlc.vscode-tailwindcss", - "stylelint.vscode-stylelint" - ] - } - } -} diff --git a/bitvid-working-webtorrent/.dockerignore b/bitvid-working-webtorrent/.dockerignore deleted file mode 100644 index b5cc6ace..00000000 --- a/bitvid-working-webtorrent/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules -.git -.gitignore -artifacts -Dockerfile -scripts -test-results diff --git a/bitvid-working-webtorrent/.gitattributes b/bitvid-working-webtorrent/.gitattributes deleted file mode 100644 index dfe07704..00000000 --- a/bitvid-working-webtorrent/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/bitvid-working-webtorrent/.github/workflows/agent-pr-review.yml b/bitvid-working-webtorrent/.github/workflows/agent-pr-review.yml deleted file mode 100644 index b010a79b..00000000 --- a/bitvid-working-webtorrent/.github/workflows/agent-pr-review.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Automated Agent PR Review - -on: - pull_request: - types: [opened, synchronize, reopened] - -permissions: - contents: write - pull-requests: write - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - agent-review: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Add upstream remote - run: | - git remote add upstream https://github.com/PR0M3TH3AN/bitvid.git || true - git fetch upstream - - - name: Verify upstream branch and diff - run: | - if git ls-remote --heads upstream "${{ github.base_ref }}" | grep "${{ github.base_ref }}" > /dev/null; then - git fetch upstream "${{ github.base_ref }}" - git diff --name-only upstream/${{ github.base_ref }}...HEAD - else - echo "::warning::Branch upstream/${{ github.base_ref }} not found. Skipping diff." - fi - - - name: Run Agent Review - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - GITHUB_BASE_REF: ${{ github.base_ref }} - GITHUB_BASE_REPO_URL: ${{ github.event.pull_request.base.repo.clone_url }} - run: node scripts/automated-pr-review.mjs diff --git a/bitvid-working-webtorrent/.github/workflows/ci.yml b/bitvid-working-webtorrent/.github/workflows/ci.yml deleted file mode 100644 index 61f888f9..00000000 --- a/bitvid-working-webtorrent/.github/workflows/ci.yml +++ /dev/null @@ -1,247 +0,0 @@ -name: CI - -on: - push: - branches: - - main - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - timeout-minutes: 45 - strategy: - matrix: - node-version: [20] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Cache npm directory - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - - - name: Cache node_modules - id: node-modules-cache - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - - - name: Install dependencies - if: steps.node-modules-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Install ripgrep - run: sudo apt-get update && sudo apt-get install -y ripgrep - - - name: Run lint suite - run: npm run lint - - - name: Build assets - run: npm run build - - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - - - name: Run visual regression tests - id: visual - run: npm run test:visual - continue-on-error: true - - - name: Upload Playwright artifacts - if: steps.visual.outcome == 'failure' - uses: actions/upload-artifact@v4 - with: - name: playwright-visual-diffs - path: | - playwright-report - test-results - if-no-files-found: ignore - - - name: Fail if visual regressions detected - if: steps.visual.outcome == 'failure' - run: exit 1 - - unit-tests: - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - node-version: [20] - shard: [1, 2, 3] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Cache npm directory - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - - - name: Cache node_modules - id: node-modules-cache - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - - - name: Install dependencies - if: steps.node-modules-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Run unit tests (shard ${{ matrix.shard }}/3) - env: - UNIT_TEST_TIMEOUT_MS: 300000 - run: npm run test:unit:shard${{ matrix.shard }} - - dm-unit-tests: - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - matrix: - node-version: [20] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Cache npm directory - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - - - name: Cache node_modules - id: node-modules-cache - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - - - name: Install dependencies - if: steps.node-modules-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Run DM unit tests - run: npm run test:dm:unit - - dm-integration-tests: - runs-on: ubuntu-latest - timeout-minutes: 30 - strategy: - matrix: - node-version: [20] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Cache npm directory - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - - - name: Cache node_modules - id: node-modules-cache - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - - - name: Install dependencies - if: steps.node-modules-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Run DM integration tests - run: npm run test:dm:integration - - e2e-headless: - runs-on: ubuntu-latest - timeout-minutes: 45 - strategy: - matrix: - node-version: [20] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - - name: Cache npm directory - uses: actions/cache@v4 - with: - path: ~/.npm - key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-npm- - - - name: Cache node_modules - id: node-modules-cache - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node-version }}-modules- - - - name: Install dependencies - if: steps.node-modules-cache.outputs.cache-hit != 'true' - run: npm ci - - - name: Install Playwright browsers - run: npx playwright install --with-deps chromium - - - name: Run headless E2E tests - run: npm run test:e2e diff --git a/bitvid-working-webtorrent/.github/workflows/cleanup-stuck-runs.yml b/bitvid-working-webtorrent/.github/workflows/cleanup-stuck-runs.yml deleted file mode 100644 index 3da93cd9..00000000 --- a/bitvid-working-webtorrent/.github/workflows/cleanup-stuck-runs.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Cleanup stuck workflow runs - -on: - schedule: - - cron: '*/30 * * * *' - workflow_dispatch: - -permissions: - actions: write - -jobs: - cleanup: - runs-on: ubuntu-latest - steps: - - name: Cancel stuck workflow runs - uses: actions/github-script@v7 - env: - STALE_THRESHOLD_HOURS: '2' - with: - script: | - const thresholdHours = Number(process.env.STALE_THRESHOLD_HOURS || 2); - const thresholdMs = thresholdHours * 60 * 60 * 1000; - const now = Date.now(); - - const statuses = ['queued', 'in_progress']; - const owner = context.repo.owner; - const repo = context.repo.repo; - const staleRuns = []; - - for (const status of statuses) { - const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { - owner, - repo, - status, - per_page: 100, - }); - - for (const run of runs) { - const createdAt = Date.parse(run.created_at); - if (Number.isNaN(createdAt)) { - core.warning(`Skipping run ${run.id} (${run.name}) due to invalid created_at.`); - continue; - } - - const ageMs = now - createdAt; - if (ageMs > thresholdMs) { - staleRuns.push({ - id: run.id, - name: run.name, - status: run.status, - created_at: run.created_at, - html_url: run.html_url, - }); - } - } - } - - if (staleRuns.length === 0) { - core.info(`No runs older than ${thresholdHours} hours found in queued/in_progress.`); - return; - } - - core.info(`Found ${staleRuns.length} stale runs older than ${thresholdHours} hours.`); - - for (const run of staleRuns) { - core.info(`Canceling run ${run.id} (${run.name}) status=${run.status} created_at=${run.created_at} url=${run.html_url}`); - await github.request('POST /repos/{owner}/{repo}/actions/runs/{run_id}/force-cancel', { - owner, - repo, - run_id: run.id, - }); - } diff --git a/bitvid-working-webtorrent/.github/workflows/daily-design-system-audit.yml b/bitvid-working-webtorrent/.github/workflows/daily-design-system-audit.yml deleted file mode 100644 index c16cffa3..00000000 --- a/bitvid-working-webtorrent/.github/workflows/daily-design-system-audit.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Daily Design System Audit - -on: - schedule: - - cron: '0 9 * * *' # Run daily at 09:00 UTC - workflow_dispatch: - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - audit: - name: Audit Design System - runs-on: ubuntu-latest - steps: - - name: Checkout unstable branch - uses: actions/checkout@v4 - with: - ref: unstable - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Verify gh CLI availability - run: gh --version - - - name: Install dependencies - run: npm ci - - - name: Run Design System Audit (with fix) - id: audit - run: node scripts/daily-design-system-audit.mjs --fix - continue-on-error: true - - - name: Check for changes - id: git-check - run: | - if [ -n "$(git status --porcelain)" ]; then - echo "changes=true" >> $GITHUB_OUTPUT - else - echo "changes=false" >> $GITHUB_OUTPUT - fi - - - name: Create Pull Request - if: steps.git-check.outputs.changes == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "ds-audit/$(date +%Y-%m-%d)" - git add . - git commit -m "chore: Apply daily design system remediation" - git push origin HEAD - gh pr create \ - --title "Daily Design System Remediation" \ - --body-file REMEDIATION_REPORT.md \ - --base unstable \ - --head "ds-audit/$(date +%Y-%m-%d)" \ - --label "design-system" - - - name: Create Issue on Failure (No Fixes) - if: steps.audit.outcome == 'failure' && steps.git-check.outputs.changes == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh issue create \ - --title "⚠� Design System Violations Detected" \ - --body-file REMEDIATION_REPORT.md \ - --label "design-system" diff --git a/bitvid-working-webtorrent/.gitignore b/bitvid-working-webtorrent/.gitignore deleted file mode 100644 index f0d7d671..00000000 --- a/bitvid-working-webtorrent/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -saved_config.yaml -repo-context.txt -node_modules/ -src/webtorrent-docs/ -playwright-report/ -test-results/ -tests/visual/__snapshots__/ -artifacts/ -css/tailwind.generated.css -ai/reports/ -*.log -coverage/ diff --git a/bitvid-working-webtorrent/.vscode/settings.json b/bitvid-working-webtorrent/.vscode/settings.json deleted file mode 100644 index d2ac1bee..00000000 --- a/bitvid-working-webtorrent/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "liveServer.settings.root": "./src" -} diff --git a/bitvid-working-webtorrent/AGENTS.md b/bitvid-working-webtorrent/AGENTS.md deleted file mode 100644 index c55a1d28..00000000 --- a/bitvid-working-webtorrent/AGENTS.md +++ /dev/null @@ -1,202 +0,0 @@ -# bitvid — AI Agent Guide - -This guide tells AI agents how to keep bitvid aligned with the current product direction. Follow it whenever you touch code, content, or documentation inside this repository. - -**Important:** Before starting any development or debugging, please review `KNOWN_ISSUES.md`. This file documents pre-existing test failures, environmental quirks, and architectural limitations that you should be aware of to avoid unnecessary investigation. - -For any nostr related work, please review the nip documentation located in /docs/nips before begining work. - -**Comment publishing guardrail:** Session actors are limited to passive telemetry (e.g., view counters). Always require the logged-in Nostr signer when sending video comments through [`js/nostr/commentEvents.js`](js/nostr/commentEvents.js). See the reminder in [`docs/nostr-event-schemas.md`](docs/nostr-event-schemas.md#event-catalogue) for context. - ---- - -## 1. Release Channels: Main vs. Unstable - -* **Main** is the production track. Anything merged here must preserve today’s UX and magnet safety guarantees. Rollbacks should be painless: keep commits atomic, avoid destructive migrations, and leave feature flags in a known-good default (`false` unless product explicitly flips them). -* **Unstable** is our experimentation lane. Gate risky behavior behind feature flags defined in `js/constants.js` and document the toggle/rollback plan in PR descriptions. -* **Emergency response:** If a change regresses URL-first playback or breaks magnet parsing, revert immediately, call this out in the PR, and annotate the AGENTS.md changelog with remediation tips. **Note:** These regressions usually surface in dev logs and tests around playback/magnet helpers—reference them when triaging so future agents don’t repeat the mistake. - -### Browser logging policy - -* Route every browser log through `js/utils/logger.js`. Do **not** call `console.*` directly. -* Use `logger.dev.*` for diagnostics that should disappear in production (flagged by `IS_DEV_MODE`/`isDevMode`). Use `logger.user.*` for operator-facing warnings and errors that must reach production consoles. -* Before promoting a build, flip `IS_DEV_MODE` in `config/instance-config.js` to match the target environment so `window.__BITVID_DEV_MODE__` and the logger channels behave correctly. -* Review and follow [`docs/logging.md`](docs/logging.md) whenever you add new logging or touch inline scripts. - -## 2. Mission: URL‑First Playback with WebTorrent Fallback - -* **Goal:** Always deliver smooth playback while keeping hosting costs low. -* **Primary transport:** A hosted video URL (MP4/WebM/HLS/DASH) that the `