13 Commits

Author SHA1 Message Date
2a21ac3075 v1.12.6
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:18:35 +00:00
bd3823741b fix(build): remove skiplibcheck from the build script 2026-03-24 19:18:35 +00:00
2c23b8862b v1.12.5
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:18:00 +00:00
5a25f22fc5 fix(package): bump package version to 1.12.5 2026-03-24 19:18:00 +00:00
219a34e6a5 v1.12.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 19:02:45 +00:00
69ab8f3436 fix(config): migrate project config to .smartconfig.json and refresh build settings 2026-03-24 19:02:45 +00:00
1b5b023556 fix(build): update bundled_ui.ts with latest build output 2026-03-24 15:09:16 +00:00
61a83f0c03 v1.12.3
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 15:08:40 +00:00
a9448ec0df fix(package): bump package version to 1.12.3 2026-03-24 15:08:40 +00:00
a29f13c75a v1.12.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-24 15:08:13 +00:00
02b3a79d99 fix(config): migrate runtime configuration loading from npmextra to smartconfig 2026-03-24 15:08:13 +00:00
7f4528bdab v1.12.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-14 23:27:25 +00:00
a829f76d4b fix(storage): rename S3 configuration and change stream interfaces to storage-oriented types 2026-03-14 23:27:25 +00:00
26 changed files with 2955 additions and 3038 deletions

View File

@@ -34,7 +34,7 @@
"@git.zone/cli": { "@git.zone/cli": {
"services": ["mongodb", "minio"], "services": ["mongodb", "minio"],
"release": { "release": {
"registries": ["https://verdaccio.lossless.digital"], "registries": ["https://verdaccio.lossless.digital", "https://registry.npmjs.org"],
"accessLevel": "public" "accessLevel": "public"
}, },
"projectType": "npm", "projectType": "npm",

View File

@@ -1,7 +1,7 @@
{ {
"json.schemas": [ "json.schemas": [
{ {
"fileMatch": ["/npmextra.json"], "fileMatch": ["/.smartconfig.json"],
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -1,5 +1,44 @@
# Changelog # Changelog
## 2026-03-24 - 1.12.6 - fix(build)
remove skiplibcheck from the build script
- Updates the build command to run tsbuild without the --skiplibcheck flag.
- Tightens TypeScript build validation by including library type checks during builds.
## 2026-03-24 - 1.12.5 - fix(package)
bump package version to 1.12.5
- Updates the package metadata version from 1.12.4 to 1.12.5.
## 2026-03-24 - 1.12.4 - fix(config)
migrate project config to .smartconfig.json and refresh build settings
- replaces npmextra.json with .smartconfig.json and updates packaged files accordingly
- adds --skiplibcheck to the build script and simplifies tsconfig settings
- bumps build and runtime dependencies and updates README references to the new config file
## 2026-03-24 - 1.12.3 - fix(package)
bump package version to 1.12.3
- Updates the package version from 1.12.2 to 1.12.3.
## 2026-03-24 - 1.12.2 - fix(config)
migrate runtime configuration loading from npmextra to smartconfig
- replace the @push.rocks/npmextra dependency with @push.rocks/smartconfig
- update tsview startup configuration types and loading logic to read port, killIfBusy, and openBrowser from smartconfig
- adjust busy-port error messaging to reference smartconfig.json
- add npmjs.org to the release registries configuration
## 2026-03-14 - 1.12.1 - fix(storage)
rename S3 configuration and change stream interfaces to storage-oriented types
- Renames public config APIs from setS3Config/getS3Config/hasS3 to setStorageConfig/getStorageConfig/hasStorage.
- Replaces shared S3 interfaces with storage equivalents such as IStorageConfig, IStorageObject, IStorageChangeEvent, and storage subscription request types.
- Updates frontend integration to use dees-storage-browser and the new storage data provider and change stream types.
- Refreshes dependency versions and documentation to match the new storage-oriented naming.
## 2026-03-12 - 1.12.0 - feat(web) ## 2026-03-12 - 1.12.0 - feat(web)
replace custom S3 browser components with dees-s3-browser integration replace custom S3 browser components with dees-s3-browser integration

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsview", "name": "@git.zone/tsview",
"version": "1.12.0", "version": "1.12.6",
"private": false, "private": false,
"description": "A CLI tool for viewing S3 and MongoDB data with a web UI", "description": "A CLI tool for viewing S3 and MongoDB data with a web UI",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -20,35 +20,35 @@
"tsview": "cli.js" "tsview": "cli.js"
}, },
"devDependencies": { "devDependencies": {
"@api.global/typedsocket": "^4.1.0", "@api.global/typedsocket": "^4.1.2",
"@git.zone/tsbuild": "^4.1.2", "@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.8.3", "@git.zone/tsbundle": "^2.10.0",
"@git.zone/tsrun": "^2.0.1", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.1.8", "@git.zone/tstest": "^3.5.1",
"@git.zone/tswatch": "3.0.1", "@git.zone/tswatch": "^3.3.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
"@types/node": "^25.0.10" "@types/node": "^25.5.0"
}, },
"dependencies": { "dependencies": {
"@api.global/typedrequest": "^3.2.5", "@api.global/typedrequest": "^3.3.0",
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.3.0", "@api.global/typedserver": "^8.4.6",
"@aws-sdk/client-s3": "^3.975.0", "@aws-sdk/client-s3": "^3.1015.0",
"@design.estate/dees-catalog": "^3.41.2", "@design.estate/dees-catalog": "^3.49.0",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.2.3",
"@push.rocks/early": "^4.0.4", "@push.rocks/early": "^4.0.4",
"@push.rocks/npmextra": "^5.3.3", "@push.rocks/smartbucket": "^4.5.1",
"@push.rocks/smartbucket": "^4.4.1",
"@push.rocks/smartcli": "^4.0.20", "@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartdata": "^7.0.15", "@push.rocks/smartconfig": "^6.0.1",
"@push.rocks/smartdata": "^7.1.0",
"@push.rocks/smartfile": "^13.1.2", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartlog": "^3.1.10", "@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartlog-destination-local": "^9.0.2", "@push.rocks/smartlog-destination-local": "^9.0.2",
"@push.rocks/smartnetwork": "^4.4.0", "@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartopen": "^2.0.0", "@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"mongodb": "^7.0.0" "mongodb": "^7.1.1"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",
@@ -59,7 +59,7 @@
"dist_ts_web/**/*", "dist_ts_web/**/*",
"assets/**/*", "assets/**/*",
"cli.js", "cli.js",
"npmextra.json", ".smartconfig.json",
"readme.md" "readme.md"
], ],
"browserslist": [ "browserslist": [

5399
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

130
readme.md
View File

@@ -1,6 +1,6 @@
# @git.zone/tsview # @git.zone/tsview
A powerful developer tool for browsing and managing S3-compatible storage and MongoDB databases through a sleek web UI — with real-time change streaming baked in. Built with TypeScript, designed for developers who need quick, visual access to their data stores. 🚀 A powerful developer tool for browsing and managing S3-compatible storage and MongoDB databases through a sleek web UI — with real-time change streaming baked in. Built with TypeScript, designed for developers who need quick, visual access to their data stores during development.
## Issue Reporting and Security ## Issue Reporting and Security
@@ -16,51 +16,11 @@ pnpm add -g @git.zone/tsview
pnpm add @git.zone/tsview pnpm add @git.zone/tsview
``` ```
## Features ✨ ## 🚀 Quick Start
### 🗄️ S3 Storage Browser
- **Column View Navigation** — Mac Finder-style interface with resizable columns
- **List View** — Traditional key-based view with hierarchical navigation
- **Real-time Preview** — View images, JSON, text files, code, and more directly in the browser
- **Bucket Management** — Create, delete, and switch between buckets
- **File Operations** — Upload, download, delete objects
- **In-place Text Editing** — Edit text files directly in the browser with change tracking
- **Smart Content Type Detection** — Automatic recognition for 20+ file types
- **Breadcrumb Navigation** — Clickable path traversal
### 🍃 MongoDB Browser
- **Database Explorer** — Hierarchical navigation through databases and collections
- **Database Overview** — Collection counts, data sizes, index stats at a glance
- **Document Viewer** — Paginated table view with JSON filter support
- **Document Editor** — Full CRUD with syntax-highlighted code editor and change tracking
- **Index Management** — View, create, and drop indexes
- **Collection Stats** — Document counts, sizes, storage metrics
- **Server Status** — Connection info, version, uptime
- **Show/Hide System Databases** — Toggle visibility of `admin`, `local`, `config`
### ⚡ Real-Time Change Streaming
- **MongoDB Change Streams** — Live updates via native MongoDB change streams
- **S3 Change Detection** — Polling-based bucket monitoring with ETag comparison (5s intervals)
- **Activity Stream** — Combined timeline of all changes from both sources, filterable by type
- **Live Indicators** — Green dot + change count badges on active views
- **WebSocket Subscriptions** — Per-collection, per-bucket, or global activity feed
- **Auto-Reconnect** — Subscriptions automatically restored after connection loss
### 🎨 Modern Web UI
- 🌙 Dark theme designed for developer comfort
- 📱 Responsive layout with resizable panels
- ⌨️ Context menus for quick actions
- 🔌 Everything bundled — zero external runtime dependencies in the browser
## Quick Start 🚀
### 1. Configure Your Connection ### 1. Configure Your Connection
Create a `.nogit/env.json` file in your project root: Create a `.nogit/env.json` file in your project root (auto-generated by `gitzone service`):
```json ```json
{ {
@@ -80,7 +40,50 @@ Create a `.nogit/env.json` file in your project root:
tsview tsview
``` ```
That's it! 🎉 Your browser will automatically open to the viewer interface. That's it! Your browser opens automatically to the viewer interface.
## ✨ Features
### 🗄️ S3 Storage Browser
Powered by `dees-storage-browser` from `@design.estate/dees-catalog`:
- **Column View Navigation** — Mac Finder-style interface with resizable columns
- **List View** — Traditional key-based view with hierarchical navigation
- **Real-time Preview** — View images, JSON, text, code, and more directly in the browser
- **Bucket Management** — Create, delete, and switch between buckets
- **File Operations** — Upload, download, delete, move, and copy objects
- **In-place Text Editing** — Edit text files directly with change tracking
- **Smart Content Type Detection** — Automatic recognition for 20+ file types
- **Breadcrumb Navigation** — Clickable path traversal
### 🍃 MongoDB Browser
- **Database Explorer** — Hierarchical navigation through databases and collections
- **Database Overview** — Collection counts, data sizes, index stats at a glance
- **Document Viewer** — Paginated table view with JSON filter support
- **Document Editor** — Full CRUD with syntax-highlighted code editor and change tracking
- **Index Management** — View, create, and drop indexes
- **Aggregation Pipeline** — Run aggregation queries directly
- **Collection Stats** — Document counts, sizes, storage metrics
- **Server Status** — Connection info, version, uptime
- **Show/Hide System Databases** — Toggle visibility of `admin`, `local`, `config`
### ⚡ Real-Time Change Streaming
- **MongoDB Change Streams** — Live updates via native MongoDB change streams
- **S3 Change Detection** — Polling-based bucket monitoring with ETag comparison (5s intervals)
- **Activity Stream** — Combined timeline of all changes from both sources, filterable by type
- **Live Indicators** — Green dot + change count badges on active views
- **WebSocket Subscriptions** — Per-collection, per-bucket, or global activity feed
- **Auto-Reconnect** — Subscriptions automatically restored after connection loss
### 🎨 Modern Web UI
- Dark theme designed for developer comfort
- Responsive layout with resizable panels
- Context menus for quick actions
- Everything bundled — zero external runtime dependencies in the browser
## CLI Usage ## CLI Usage
@@ -113,7 +116,7 @@ const viewer = new TsView();
await viewer.loadConfigFromEnv(); await viewer.loadConfigFromEnv();
// Option 2: Configure programmatically // Option 2: Configure programmatically
viewer.setS3Config({ viewer.setStorageConfig({
endpoint: 'localhost', endpoint: 'localhost',
port: 9000, port: 9000,
accessKey: 'minioadmin', accessKey: 'minioadmin',
@@ -127,7 +130,7 @@ viewer.setMongoConfig({
}); });
// Option 3: Cloud services // Option 3: Cloud services
viewer.setS3Config({ viewer.setStorageConfig({
endpoint: 's3.amazonaws.com', endpoint: 's3.amazonaws.com',
accessKey: 'AKIAXXXXXXX', accessKey: 'AKIAXXXXXXX',
accessSecret: 'your-secret-key', accessSecret: 'your-secret-key',
@@ -153,7 +156,7 @@ await viewer.stop();
## Configuration ## Configuration
### Project-level via `npmextra.json` ### Project-level via `.smartconfig.json`
```json ```json
{ {
@@ -171,7 +174,7 @@ await viewer.stop();
| `killIfBusy` | `boolean` | `false` | Kill existing process if port is busy | | `killIfBusy` | `boolean` | `false` | Kill existing process if port is busy |
| `openBrowser` | `boolean` | `true` | Automatically open browser on start | | `openBrowser` | `boolean` | `true` | Automatically open browser on start |
**Port priority:** CLI `--port` flag → `npmextra.json` → auto-detect **Port priority:** CLI `--port` flag → `.smartconfig.json` → auto-detect
### Environment Variables (`.nogit/env.json`) ### Environment Variables (`.nogit/env.json`)
@@ -207,14 +210,14 @@ Or use individual variables:
tsview works with any S3-compatible storage: tsview works with any S3-compatible storage:
| Provider | Status | | Provider | Status |
| ----------------------- | --------------------------- | | ----------------------- | -------------------------- |
| **MinIO** | Perfect for local dev | | **MinIO** | Perfect for local dev |
| **AWS S3** | Amazon's object storage | | **AWS S3** | Amazon's object storage |
| **DigitalOcean Spaces** | Simple object storage | | **DigitalOcean Spaces** | Simple object storage |
| **Backblaze B2** | S3-compatible API | | **Backblaze B2** | S3-compatible API |
| **Cloudflare R2** | Zero egress fees | | **Cloudflare R2** | Zero egress fees |
| **Wasabi** | Hot cloud storage | | **Wasabi** | Hot cloud storage |
| **Self-hosted** | Any S3-compatible server | | **Self-hosted** | Any S3-compatible server |
## Supported File Types for Preview ## Supported File Types for Preview
@@ -226,11 +229,11 @@ tsview works with any S3-compatible storage:
| **Data** | `.csv`, `.xml`, `.yaml`, `.yml` | | **Data** | `.csv`, `.xml`, `.yaml`, `.yml` |
| **Documents** | `.pdf` | | **Documents** | `.pdf` |
## Architecture ## 🏗️ Architecture
``` ```
tsview/ tsview/
├── ts/ # Backend ├── ts/ # Backend (Node.js)
│ ├── api/ # TypedRequest API handlers │ ├── api/ # TypedRequest API handlers
│ │ ├── handlers.s3.ts # S3 bucket & object operations │ │ ├── handlers.s3.ts # S3 bucket & object operations
│ │ └── handlers.mongodb.ts # MongoDB CRUD & admin operations │ │ └── handlers.mongodb.ts # MongoDB CRUD & admin operations
@@ -241,23 +244,24 @@ tsview/
│ │ └── interfaces.streaming.ts # Subscription interfaces │ │ └── interfaces.streaming.ts # Subscription interfaces
│ ├── interfaces/ # Shared TypeScript interfaces │ ├── interfaces/ # Shared TypeScript interfaces
│ └── tsview.classes.tsview.ts # Main class │ └── tsview.classes.tsview.ts # Main class
├── ts_web/ # Frontend ├── ts_web/ # Frontend (bundled via esbuild → base64ts)
│ ├── elements/ # Web components (LitElement) │ ├── elements/ # Web components (LitElement)
│ │ ├── tsview-app.ts # App shell + navigation │ │ ├── tsview-app.ts # App shell + navigation
│ │ ├── tsview-s3-*.ts # S3 browser components
│ │ ├── tsview-mongo-*.ts # MongoDB browser components │ │ ├── tsview-mongo-*.ts # MongoDB browser components
│ │ └── tsview-activity-stream.ts # Real-time activity feed │ │ └── tsview-activity-stream.ts # Real-time activity feed
│ ├── adapters/ # Data provider adapters
│ │ └── s3-data-provider.ts # IStorageDataProvider for dees-storage-browser
│ ├── services/ # API + WebSocket clients │ ├── services/ # API + WebSocket clients
│ ├── styles/ # Dark theme │ ├── styles/ # Dark theme
│ └── utilities/ # Formatting helpers │ └── utilities/ # Formatting helpers
└── cli.ts.js # CLI entry point └── .smartconfig.json # Build & runtime config
``` ```
### How It Works ### How It Works
1. **Backend** — A `TypedServer` serves the bundled web UI and exposes a typed API via `TypedRequest` over HTTP. A `TypedSocket` WebSocket layer handles real-time streaming subscriptions. 1. **Backend** — A `TypedServer` serves the bundled web UI and exposes a typed API via `TypedRequest` over HTTP. A `TypedSocket` WebSocket layer handles real-time streaming subscriptions.
2. **Frontend** — LitElement-based web components communicate with the backend via `TypedRequest`. The `ChangeStreamService` connects over WebSocket and distributes real-time events to active views via RxJS Subjects. 2. **Frontend** — LitElement-based web components communicate with the backend via `TypedRequest`. The S3 browser uses `dees-storage-browser` from `@design.estate/dees-catalog` with a custom `IStorageDataProvider` adapter. The `ChangeStreamService` connects over WebSocket and distributes real-time events to active views via RxJS Subjects.
3. **Streaming** — The `ChangeStreamManager` creates MongoDB Change Streams and S3 BucketWatchers on demand (one per subscribed resource). Changes are pushed to subscribed clients and accumulated in a 1000-event ring buffer for the Activity Stream view. 3. **Streaming** — The `ChangeStreamManager` creates MongoDB Change Streams and S3 BucketWatchers on demand (one per subscribed resource). Changes are pushed to subscribed clients and accumulated in a 1000-event ring buffer for the Activity Stream view.
@@ -283,7 +287,7 @@ pnpm test
## License and Legal Information ## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file. This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -10,7 +10,7 @@ tsview/
├── cli.ts.js # Dev CLI (uses tsrun to run cli.child.ts) ├── cli.ts.js # Dev CLI (uses tsrun to run cli.child.ts)
├── cli.child.ts # Dev CLI entry (imports ts/index.js) ├── cli.child.ts # Dev CLI entry (imports ts/index.js)
├── package.json # bin: { "tsview": "cli.js" } ├── package.json # bin: { "tsview": "cli.js" }
├── npmextra.json # tsbundle config for UI bundling ├── .smartconfig.json # tsbundle/tswatch config
├── tsconfig.json ├── tsconfig.json
├── readme.md ├── readme.md
├── readme.hints.md ├── readme.hints.md

View File

@@ -14,8 +14,8 @@ tap.test('should create TsView instance', async () => {
tap.test('should have config methods', async () => { tap.test('should have config methods', async () => {
const viewer = new tsview.TsView(); const viewer = new tsview.TsView();
// Set S3 config // Set storage config
viewer.setS3Config({ viewer.setStorageConfig({
endpoint: 'localhost', endpoint: 'localhost',
port: 9000, port: 9000,
accessKey: 'test', accessKey: 'test',
@@ -23,7 +23,7 @@ tap.test('should have config methods', async () => {
useSsl: false, useSsl: false,
}); });
expect(viewer.config.hasS3()).toBeTrue(); expect(viewer.config.hasStorage()).toBeTrue();
expect(viewer.config.hasMongo()).toBeFalse(); expect(viewer.config.hasMongo()).toBeFalse();
// Set MongoDB config // Set MongoDB config

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsview', name: '@git.zone/tsview',
version: '1.12.0', version: '1.12.6',
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI' description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
} }

View File

@@ -119,7 +119,7 @@ export async function registerS3Handlers(
} }
} }
const objects: interfaces.IS3Object[] = []; const objects: interfaces.IStorageObject[] = [];
const prefixSet = new Set<string>(); const prefixSet = new Set<string>();
// List files in current directory // List files in current directory

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@ import type * as interfaces from '../interfaces/index.js';
* or accepts programmatic configuration. * or accepts programmatic configuration.
*/ */
export class TsViewConfig { export class TsViewConfig {
private s3Config: interfaces.IS3Config | null = null; private storageConfig: interfaces.IStorageConfig | null = null;
private mongoConfig: interfaces.IMongoConfig | null = null; private mongoConfig: interfaces.IMongoConfig | null = null;
/** /**
@@ -29,7 +29,7 @@ export class TsViewConfig {
// Parse S3 config // Parse S3 config
if (envConfig.S3_HOST || envConfig.S3_ENDPOINT) { if (envConfig.S3_HOST || envConfig.S3_ENDPOINT) {
this.s3Config = { this.storageConfig = {
endpoint: envConfig.S3_ENDPOINT || envConfig.S3_HOST || '', endpoint: envConfig.S3_ENDPOINT || envConfig.S3_HOST || '',
port: envConfig.S3_PORT ? parseInt(envConfig.S3_PORT, 10) : undefined, port: envConfig.S3_PORT ? parseInt(envConfig.S3_PORT, 10) : undefined,
accessKey: envConfig.S3_ACCESSKEY || '', accessKey: envConfig.S3_ACCESSKEY || '',
@@ -69,8 +69,8 @@ export class TsViewConfig {
/** /**
* Set S3 configuration programmatically * Set S3 configuration programmatically
*/ */
public setS3Config(config: interfaces.IS3Config): void { public setStorageConfig(config: interfaces.IStorageConfig): void {
this.s3Config = config; this.storageConfig = config;
} }
/** /**
@@ -83,8 +83,8 @@ export class TsViewConfig {
/** /**
* Get S3 configuration * Get S3 configuration
*/ */
public getS3Config(): interfaces.IS3Config | null { public getStorageConfig(): interfaces.IStorageConfig | null {
return this.s3Config; return this.storageConfig;
} }
/** /**
@@ -97,8 +97,8 @@ export class TsViewConfig {
/** /**
* Check if S3 is configured * Check if S3 is configured
*/ */
public hasS3(): boolean { public hasStorage(): boolean {
return this.s3Config !== null && !!this.s3Config.endpoint && !!this.s3Config.accessKey; return this.storageConfig !== null && !!this.storageConfig.endpoint && !!this.storageConfig.accessKey;
} }
/** /**

View File

@@ -1,9 +1,9 @@
import type * as plugins from '../plugins.js'; import type * as plugins from '../plugins.js';
/** /**
* Configuration for S3 connection * Configuration for storage (S3-compatible) connection
*/ */
export interface IS3Config { export interface IStorageConfig {
endpoint: string; endpoint: string;
port?: number; port?: number;
accessKey: string; accessKey: string;
@@ -24,14 +24,14 @@ export interface IMongoConfig {
* Combined configuration for tsview * Combined configuration for tsview
*/ */
export interface ITsViewConfig { export interface ITsViewConfig {
s3?: IS3Config; s3?: IStorageConfig;
mongo?: IMongoConfig; mongo?: IMongoConfig;
} }
/** /**
* Configuration from npmextra.json for @git.zone/tsview * Configuration from smartconfig.json for @git.zone/tsview
*/ */
export interface INpmextraConfig { export interface ISmartconfigConfig {
port?: number; // Fixed port to use (optional) port?: number; // Fixed port to use (optional)
killIfBusy?: boolean; // Kill process on port if busy (default: false) killIfBusy?: boolean; // Kill process on port if busy (default: false)
openBrowser?: boolean; // Open browser on start (default: true) openBrowser?: boolean; // Open browser on start (default: true)
@@ -97,7 +97,7 @@ export interface IReq_DeleteBucket extends plugins.typedrequestInterfaces.implem
}; };
} }
export interface IS3Object { export interface IStorageObject {
key: string; key: string;
size?: number; size?: number;
lastModified?: string; lastModified?: string;
@@ -115,7 +115,7 @@ export interface IReq_ListObjects extends plugins.typedrequestInterfaces.impleme
delimiter?: string; delimiter?: string;
}; };
response: { response: {
objects: IS3Object[]; objects: IStorageObject[];
prefixes: string[]; prefixes: string[];
}; };
} }

View File

@@ -6,7 +6,7 @@ export { path };
import * as early from '@push.rocks/early'; import * as early from '@push.rocks/early';
early.start('tsview'); early.start('tsview');
import * as npmextra from '@push.rocks/npmextra'; import * as smartconfig from '@push.rocks/smartconfig';
import * as smartbucket from '@push.rocks/smartbucket'; import * as smartbucket from '@push.rocks/smartbucket';
import * as smartcli from '@push.rocks/smartcli'; import * as smartcli from '@push.rocks/smartcli';
import * as smartdata from '@push.rocks/smartdata'; import * as smartdata from '@push.rocks/smartdata';
@@ -21,7 +21,7 @@ import * as smartrx from '@push.rocks/smartrx';
export { export {
early, early,
npmextra, smartconfig,
smartbucket, smartbucket,
smartcli, smartcli,
smartdata, smartdata,

View File

@@ -39,7 +39,7 @@ export class ViewServer {
this.changeStreamManager = new ChangeStreamManager(this.tsview); this.changeStreamManager = new ChangeStreamManager(this.tsview);
// Register API handlers directly to server's router // Register API handlers directly to server's router
if (this.tsview.config.hasS3()) { if (this.tsview.config.hasStorage()) {
await registerS3Handlers(this.typedServer.typedrouter, this.tsview); await registerS3Handlers(this.typedServer.typedrouter, this.tsview);
} }
@@ -107,9 +107,9 @@ export class ViewServer {
) )
); );
// Subscribe to S3 bucket changes // Subscribe to storage bucket changes
typedrouter.addTypedHandler( typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_SubscribeS3>( new plugins.typedrequest.TypedHandler<interfaces.IReq_SubscribeStorage>(
'subscribeS3', 'subscribeS3',
async (reqData, context) => { async (reqData, context) => {
const connectionId = this.getConnectionId(context); const connectionId = this.getConnectionId(context);
@@ -127,9 +127,9 @@ export class ViewServer {
) )
); );
// Unsubscribe from S3 bucket changes // Unsubscribe from storage bucket changes
typedrouter.addTypedHandler( typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.IReq_UnsubscribeS3>( new plugins.typedrequest.TypedHandler<interfaces.IReq_UnsubscribeStorage>(
'unsubscribeS3', 'unsubscribeS3',
async (reqData, context) => { async (reqData, context) => {
const connectionId = this.getConnectionId(context); const connectionId = this.getConnectionId(context);

View File

@@ -1,7 +1,7 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import type { TsView } from '../tsview.classes.tsview.js'; import type { TsView } from '../tsview.classes.tsview.js';
import type * as interfaces from './interfaces.streaming.js'; import type * as interfaces from './interfaces.streaming.js';
import type { IS3ChangeEvent } from '@push.rocks/smartbucket'; import type { IStorageChangeEvent } from './interfaces.streaming.js';
/** /**
* Subscription entry tracking a client's subscription to a resource * Subscription entry tracking a client's subscription to a resource
@@ -21,19 +21,19 @@ interface IMongoWatcherEntry {
} }
/** /**
* S3 watcher entry * Storage watcher entry
*/ */
interface IS3WatcherEntry { interface IStorageWatcherEntry {
watcher: plugins.smartbucket.BucketWatcher; watcher: plugins.smartbucket.BucketWatcher;
subscriptions: Map<string, ISubscriptionEntry>; // connectionId -> subscription subscriptions: Map<string, ISubscriptionEntry>; // connectionId -> subscription
} }
/** /**
* ChangeStreamManager manages real-time change streaming for both MongoDB and S3. * ChangeStreamManager manages real-time change streaming for both MongoDB and storage.
* *
* Features: * Features:
* - MongoDB Change Streams for real-time database updates * - MongoDB Change Streams for real-time database updates
* - S3 BucketWatcher for polling-based S3 change detection * - S3 BucketWatcher for polling-based storage change detection
* - Subscription management per WebSocket client * - Subscription management per WebSocket client
* - Activity stream with ring buffer for recent events * - Activity stream with ring buffer for recent events
* - Automatic cleanup on client disconnect * - Automatic cleanup on client disconnect
@@ -45,8 +45,8 @@ export class ChangeStreamManager {
// MongoDB watchers: "db/collection" -> watcher entry // MongoDB watchers: "db/collection" -> watcher entry
private mongoWatchers: Map<string, IMongoWatcherEntry> = new Map(); private mongoWatchers: Map<string, IMongoWatcherEntry> = new Map();
// S3 watchers: "bucket/prefix" -> watcher entry // Storage watchers: "bucket/prefix" -> watcher entry
private s3Watchers: Map<string, IS3WatcherEntry> = new Map(); private storageWatchers: Map<string, IStorageWatcherEntry> = new Map();
// Activity subscribers: connectionId -> subscription entry // Activity subscribers: connectionId -> subscription entry
private activitySubscribers: Map<string, ISubscriptionEntry> = new Map(); private activitySubscribers: Map<string, ISubscriptionEntry> = new Map();
@@ -57,7 +57,7 @@ export class ChangeStreamManager {
// Global watchers for the activity stream (started lazily on first subscriber) // Global watchers for the activity stream (started lazily on first subscriber)
private globalMongoWatcher: plugins.mongodb.ChangeStream | null = null; private globalMongoWatcher: plugins.mongodb.ChangeStream | null = null;
private globalS3Watchers: Map<string, plugins.smartbucket.BucketWatcher> = new Map(); private globalStorageWatchers: Map<string, plugins.smartbucket.BucketWatcher> = new Map();
private globalWatchersActive: boolean = false; private globalWatchersActive: boolean = false;
// Counter for generating unique subscription IDs // Counter for generating unique subscription IDs
@@ -89,9 +89,9 @@ export class ChangeStreamManager {
} }
/** /**
* Get the S3 key for a bucket/prefix pair * Get the storage key for a bucket/prefix pair
*/ */
private getS3Key(bucket: string, prefix?: string): string { private getStorageKey(bucket: string, prefix?: string): string {
return prefix ? `${bucket}/${prefix}` : bucket; return prefix ? `${bucket}/${prefix}` : bucket;
} }
@@ -280,24 +280,24 @@ export class ChangeStreamManager {
} }
// =========================================== // ===========================================
// S3 Change Watching // Storage Change Watching
// =========================================== // ===========================================
/** /**
* Subscribe a client to S3 bucket/prefix changes * Subscribe a client to storage bucket/prefix changes
*/ */
public async subscribeToS3( public async subscribeToS3(
connectionId: string, connectionId: string,
bucket: string, bucket: string,
prefix?: string prefix?: string
): Promise<{ success: boolean; subscriptionId: string }> { ): Promise<{ success: boolean; subscriptionId: string }> {
const key = this.getS3Key(bucket, prefix); const key = this.getStorageKey(bucket, prefix);
let entry = this.s3Watchers.get(key); let entry = this.storageWatchers.get(key);
// Create watcher if it doesn't exist // Create watcher if it doesn't exist
if (!entry) { if (!entry) {
const watcher = await this.createS3Watcher(bucket, prefix); const watcher = await this.createStorageWatcher(bucket, prefix);
if (!watcher) { if (!watcher) {
return { success: false, subscriptionId: '' }; return { success: false, subscriptionId: '' };
} }
@@ -306,7 +306,7 @@ export class ChangeStreamManager {
watcher, watcher,
subscriptions: new Map(), subscriptions: new Map(),
}; };
this.s3Watchers.set(key, entry); this.storageWatchers.set(key, entry);
} }
// Add subscription // Add subscription
@@ -317,47 +317,47 @@ export class ChangeStreamManager {
createdAt: new Date(), createdAt: new Date(),
}); });
console.log(`[ChangeStream] S3 subscription added: ${key} for connection ${connectionId}`); console.log(`[ChangeStream] Storage subscription added: ${key} for connection ${connectionId}`);
return { success: true, subscriptionId }; return { success: true, subscriptionId };
} }
/** /**
* Unsubscribe a client from S3 bucket/prefix changes * Unsubscribe a client from storage bucket/prefix changes
*/ */
public async unsubscribeFromS3( public async unsubscribeFromS3(
connectionId: string, connectionId: string,
bucket: string, bucket: string,
prefix?: string prefix?: string
): Promise<boolean> { ): Promise<boolean> {
const key = this.getS3Key(bucket, prefix); const key = this.getStorageKey(bucket, prefix);
const entry = this.s3Watchers.get(key); const entry = this.storageWatchers.get(key);
if (!entry) { if (!entry) {
return false; return false;
} }
entry.subscriptions.delete(connectionId); entry.subscriptions.delete(connectionId);
console.log(`[ChangeStream] S3 subscription removed: ${key} for connection ${connectionId}`); console.log(`[ChangeStream] Storage subscription removed: ${key} for connection ${connectionId}`);
// Close watcher if no more subscribers // Close watcher if no more subscribers
if (entry.subscriptions.size === 0) { if (entry.subscriptions.size === 0) {
await this.closeS3Watcher(key); await this.closeStorageWatcher(key);
} }
return true; return true;
} }
/** /**
* Create an S3 bucket watcher * Create a storage bucket watcher
*/ */
private async createS3Watcher( private async createStorageWatcher(
bucket: string, bucket: string,
prefix?: string prefix?: string
): Promise<plugins.smartbucket.BucketWatcher | null> { ): Promise<plugins.smartbucket.BucketWatcher | null> {
try { try {
const smartbucket = await this.tsview.getSmartBucket(); const smartbucket = await this.tsview.getSmartBucket();
if (!smartbucket) { if (!smartbucket) {
console.error('[ChangeStream] S3 not configured'); console.error('[ChangeStream] Storage not configured');
return null; return null;
} }
@@ -371,10 +371,10 @@ export class ChangeStreamManager {
}); });
// Subscribe to change events // Subscribe to change events
watcher.changeSubject.subscribe((eventOrEvents: IS3ChangeEvent | IS3ChangeEvent[]) => { watcher.changeSubject.subscribe((eventOrEvents: IStorageChangeEvent | IStorageChangeEvent[]) => {
const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents]; const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents];
for (const event of events) { for (const event of events) {
this.handleS3Change(bucket, prefix, event); this.handleStorageChange(bucket, prefix, event);
} }
}); });
@@ -382,41 +382,41 @@ export class ChangeStreamManager {
await watcher.start(); await watcher.start();
await watcher.readyDeferred.promise; await watcher.readyDeferred.promise;
console.log(`[ChangeStream] S3 watcher created for ${bucket}${prefix ? '/' + prefix : ''}`); console.log(`[ChangeStream] Storage watcher created for ${bucket}${prefix ? '/' + prefix : ''}`);
return watcher; return watcher;
} catch (error) { } catch (error) {
console.error(`[ChangeStream] Failed to create S3 watcher for ${bucket}:`, error); console.error(`[ChangeStream] Failed to create storage watcher for ${bucket}:`, error);
return null; return null;
} }
} }
/** /**
* Handle an S3 change event * Handle a storage change event
*/ */
private handleS3Change(bucket: string, prefix: string | undefined, event: IS3ChangeEvent): void { private handleStorageChange(bucket: string, prefix: string | undefined, event: IStorageChangeEvent): void {
const key = this.getS3Key(bucket, prefix); const key = this.getStorageKey(bucket, prefix);
const entry = this.s3Watchers.get(key); const entry = this.storageWatchers.get(key);
if (!entry) return; if (!entry) return;
// Only add to activity buffer if global watchers are NOT active. // Only add to activity buffer if global watchers are NOT active.
// When active, the global S3 watchers already feed the activity stream. // When active, the global storage watchers already feed the activity stream.
if (!this.globalWatchersActive) { if (!this.globalWatchersActive) {
this.addToActivityBuffer('s3', event); this.addToActivityBuffer('storage', event);
} }
// Push to all subscribed clients // Push to all subscribed clients
this.pushS3ChangeToClients(key, event); this.pushStorageChangeToClients(key, event);
} }
/** /**
* Push S3 change to subscribed clients * Push storage change to subscribed clients
*/ */
private async pushS3ChangeToClients( private async pushStorageChangeToClients(
key: string, key: string,
event: IS3ChangeEvent event: IStorageChangeEvent
): Promise<void> { ): Promise<void> {
const entry = this.s3Watchers.get(key); const entry = this.storageWatchers.get(key);
if (!entry || !this.typedSocket) return; if (!entry || !this.typedSocket) return;
for (const [connectionId, _sub] of entry.subscriptions) { for (const [connectionId, _sub] of entry.subscriptions) {
@@ -426,31 +426,31 @@ export class ChangeStreamManager {
}); });
if (connection) { if (connection) {
const request = this.typedSocket.createTypedRequest<interfaces.IReq_PushS3Change>( const request = this.typedSocket.createTypedRequest<interfaces.IReq_PushStorageChange>(
'pushS3Change', 'pushS3Change',
connection connection
); );
await request.fire({ event }); await request.fire({ event });
} }
} catch (error) { } catch (error) {
console.error(`[ChangeStream] Failed to push S3 change to ${connectionId}:`, error); console.error(`[ChangeStream] Failed to push storage change to ${connectionId}:`, error);
} }
} }
} }
/** /**
* Close an S3 bucket watcher * Close a storage bucket watcher
*/ */
private async closeS3Watcher(key: string): Promise<void> { private async closeStorageWatcher(key: string): Promise<void> {
const entry = this.s3Watchers.get(key); const entry = this.storageWatchers.get(key);
if (!entry) return; if (!entry) return;
try { try {
await entry.watcher.stop(); await entry.watcher.stop();
this.s3Watchers.delete(key); this.storageWatchers.delete(key);
console.log(`[ChangeStream] S3 watcher closed for ${key}`); console.log(`[ChangeStream] Storage watcher closed for ${key}`);
} catch (error) { } catch (error) {
console.error(`[ChangeStream] Error closing S3 watcher for ${key}:`, error); console.error(`[ChangeStream] Error closing storage watcher for ${key}:`, error);
} }
} }
@@ -515,11 +515,11 @@ export class ChangeStreamManager {
* Add an event to the activity buffer * Add an event to the activity buffer
*/ */
private addToActivityBuffer( private addToActivityBuffer(
source: 'mongodb' | 's3', source: 'mongodb' | 'storage',
event: interfaces.IMongoChangeEvent | IS3ChangeEvent event: interfaces.IMongoChangeEvent | IStorageChangeEvent
): void { ): void {
const activityEvent: interfaces.IActivityEvent = { const activityEvent: interfaces.IActivityEvent = {
id: `evt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, id: `evt_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
source, source,
event, event,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
@@ -567,7 +567,7 @@ export class ChangeStreamManager {
/** /**
* Start global watchers when the first activity subscriber connects. * Start global watchers when the first activity subscriber connects.
* These watch all MongoDB and S3 activity and feed into the activity buffer. * These watch all MongoDB and storage activity and feed into the activity buffer.
*/ */
private async startGlobalWatchers(): Promise<void> { private async startGlobalWatchers(): Promise<void> {
if (this.globalWatchersActive) return; if (this.globalWatchersActive) return;
@@ -577,7 +577,7 @@ export class ChangeStreamManager {
await Promise.all([ await Promise.all([
this.startGlobalMongoWatcher(), this.startGlobalMongoWatcher(),
this.startGlobalS3Watchers(), this.startGlobalStorageWatchers(),
]); ]);
} }
@@ -628,13 +628,13 @@ export class ChangeStreamManager {
} }
/** /**
* Start S3 bucket watchers — one BucketWatcher per bucket. * Start storage bucket watchers — one BucketWatcher per bucket.
*/ */
private async startGlobalS3Watchers(): Promise<void> { private async startGlobalStorageWatchers(): Promise<void> {
try { try {
const smartbucket = await this.tsview.getSmartBucket(); const smartbucket = await this.tsview.getSmartBucket();
if (!smartbucket) { if (!smartbucket) {
console.log('[ChangeStream] S3 not configured, skipping global S3 watchers'); console.log('[ChangeStream] Storage not configured, skipping global storage watchers');
return; return;
} }
@@ -652,26 +652,26 @@ export class ChangeStreamManager {
bufferTimeMs: 500, bufferTimeMs: 500,
}); });
watcher.changeSubject.subscribe((eventOrEvents: IS3ChangeEvent | IS3ChangeEvent[]) => { watcher.changeSubject.subscribe((eventOrEvents: IStorageChangeEvent | IStorageChangeEvent[]) => {
const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents]; const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents];
for (const event of events) { for (const event of events) {
this.addToActivityBuffer('s3', event); this.addToActivityBuffer('storage', event);
} }
}); });
await watcher.start(); await watcher.start();
await watcher.readyDeferred.promise; await watcher.readyDeferred.promise;
this.globalS3Watchers.set(bucketName, watcher); this.globalStorageWatchers.set(bucketName, watcher);
console.log(`[ChangeStream] Global S3 watcher started for bucket: ${bucketName}`); console.log(`[ChangeStream] Global storage watcher started for bucket: ${bucketName}`);
} catch (bucketError) { } catch (bucketError) {
console.error(`[ChangeStream] Failed to start global S3 watcher for bucket ${bucketName}:`, bucketError); console.error(`[ChangeStream] Failed to start global storage watcher for bucket ${bucketName}:`, bucketError);
} }
} }
console.log(`[ChangeStream] Global S3 watchers started (${this.globalS3Watchers.size}/${bucketNames.length} buckets)`); console.log(`[ChangeStream] Global storage watchers started (${this.globalStorageWatchers.size}/${bucketNames.length} buckets)`);
} catch (error) { } catch (error) {
console.error('[ChangeStream] Failed to start global S3 watchers:', error); console.error('[ChangeStream] Failed to start global storage watchers:', error);
} }
} }
@@ -694,16 +694,16 @@ export class ChangeStreamManager {
this.globalMongoWatcher = null; this.globalMongoWatcher = null;
} }
// Close all global S3 watchers // Close all global storage watchers
for (const [bucketName, watcher] of this.globalS3Watchers) { for (const [bucketName, watcher] of this.globalStorageWatchers) {
try { try {
await watcher.stop(); await watcher.stop();
console.log(`[ChangeStream] Global S3 watcher stopped for bucket: ${bucketName}`); console.log(`[ChangeStream] Global storage watcher stopped for bucket: ${bucketName}`);
} catch (error) { } catch (error) {
console.error(`[ChangeStream] Error closing global S3 watcher for ${bucketName}:`, error); console.error(`[ChangeStream] Error closing global storage watcher for ${bucketName}:`, error);
} }
} }
this.globalS3Watchers.clear(); this.globalStorageWatchers.clear();
this.globalWatchersActive = false; this.globalWatchersActive = false;
console.log('[ChangeStream] Global watchers stopped'); console.log('[ChangeStream] Global watchers stopped');
@@ -729,12 +729,12 @@ export class ChangeStreamManager {
} }
} }
// Clean up S3 subscriptions // Clean up storage subscriptions
for (const [key, entry] of this.s3Watchers) { for (const [key, entry] of this.storageWatchers) {
if (entry.subscriptions.has(connectionId)) { if (entry.subscriptions.has(connectionId)) {
entry.subscriptions.delete(connectionId); entry.subscriptions.delete(connectionId);
if (entry.subscriptions.size === 0) { if (entry.subscriptions.size === 0) {
await this.closeS3Watcher(key); await this.closeStorageWatcher(key);
} }
} }
} }
@@ -762,9 +762,9 @@ export class ChangeStreamManager {
await this.closeMongoWatcher(key); await this.closeMongoWatcher(key);
} }
// Close all S3 watchers // Close all storage watchers
for (const key of this.s3Watchers.keys()) { for (const key of this.storageWatchers.keys()) {
await this.closeS3Watcher(key); await this.closeStorageWatcher(key);
} }
// Clear activity buffer and subscribers // Clear activity buffer and subscribers

View File

@@ -1,7 +1,8 @@
import type * as plugins from '../plugins.js'; import type * as plugins from '../plugins.js';
// Re-export S3 change event from smartbucket // Re-export storage change event from smartbucket
export type { IS3ChangeEvent } from '@push.rocks/smartbucket'; import type { IStorageChangeEvent } from '@push.rocks/smartbucket';
export type { IStorageChangeEvent };
/** /**
* MongoDB change event - wraps smartdata watcher output * MongoDB change event - wraps smartdata watcher output
@@ -24,8 +25,8 @@ export interface IMongoChangeEvent {
*/ */
export interface IActivityEvent { export interface IActivityEvent {
id: string; id: string;
source: 'mongodb' | 's3'; source: 'mongodb' | 'storage';
event: IMongoChangeEvent | import('@push.rocks/smartbucket').IS3ChangeEvent; event: IMongoChangeEvent | IStorageChangeEvent;
timestamp: string; timestamp: string;
} }
@@ -69,11 +70,11 @@ export interface IReq_UnsubscribeMongo extends plugins.typedrequestInterfaces.im
} }
/** /**
* Subscribe to S3 bucket/prefix changes * Subscribe to storage bucket/prefix changes
*/ */
export interface IReq_SubscribeS3 extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_SubscribeStorage extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest, plugins.typedrequestInterfaces.ITypedRequest,
IReq_SubscribeS3 IReq_SubscribeStorage
> { > {
method: 'subscribeS3'; method: 'subscribeS3';
request: { request: {
@@ -87,11 +88,11 @@ export interface IReq_SubscribeS3 extends plugins.typedrequestInterfaces.impleme
} }
/** /**
* Unsubscribe from S3 bucket/prefix changes * Unsubscribe from storage bucket/prefix changes
*/ */
export interface IReq_UnsubscribeS3 extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_UnsubscribeStorage extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest, plugins.typedrequestInterfaces.ITypedRequest,
IReq_UnsubscribeS3 IReq_UnsubscribeStorage
> { > {
method: 'unsubscribeS3'; method: 'unsubscribeS3';
request: { request: {
@@ -104,7 +105,7 @@ export interface IReq_UnsubscribeS3 extends plugins.typedrequestInterfaces.imple
} }
/** /**
* Subscribe to activity stream (all changes from MongoDB and S3) * Subscribe to activity stream (all changes from MongoDB and storage)
*/ */
export interface IReq_SubscribeActivity extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_SubscribeActivity extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest, plugins.typedrequestInterfaces.ITypedRequest,
@@ -169,15 +170,15 @@ export interface IReq_PushMongoChange extends plugins.typedrequestInterfaces.imp
} }
/** /**
* Server pushes S3 change to client * Server pushes storage change to client
*/ */
export interface IReq_PushS3Change extends plugins.typedrequestInterfaces.implementsTR< export interface IReq_PushStorageChange extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest, plugins.typedrequestInterfaces.ITypedRequest,
IReq_PushS3Change IReq_PushStorageChange
> { > {
method: 'pushS3Change'; method: 'pushS3Change';
request: { request: {
event: import('@push.rocks/smartbucket').IS3ChangeEvent; event: IStorageChangeEvent;
}; };
response: { response: {
received: boolean; received: boolean;
@@ -206,7 +207,7 @@ export interface IReq_PushActivityEvent extends plugins.typedrequestInterfaces.i
export interface ISubscriptionTag extends plugins.typedrequestInterfaces.ITag { export interface ISubscriptionTag extends plugins.typedrequestInterfaces.ITag {
name: 'subscription'; name: 'subscription';
payload: { payload: {
type: 'mongo' | 's3' | 'activity'; type: 'mongo' | 'storage' | 'activity';
key: string; // e.g., "db/collection" or "bucket/prefix" or "activity" key: string; // e.g., "db/collection" or "bucket/prefix" or "activity"
}; };
} }

View File

@@ -33,8 +33,8 @@ export class TsView {
/** /**
* Set S3 configuration programmatically * Set S3 configuration programmatically
*/ */
public setS3Config(config: interfaces.IS3Config): void { public setStorageConfig(config: interfaces.IStorageConfig): void {
this.config.setS3Config(config); this.config.setStorageConfig(config);
} }
/** /**
@@ -52,17 +52,17 @@ export class TsView {
return this.smartbucketInstance; return this.smartbucketInstance;
} }
const s3Config = this.config.getS3Config(); const storageConfig = this.config.getStorageConfig();
if (!s3Config) { if (!storageConfig) {
return null; return null;
} }
this.smartbucketInstance = new plugins.smartbucket.SmartBucket({ this.smartbucketInstance = new plugins.smartbucket.SmartBucket({
endpoint: s3Config.endpoint, endpoint: storageConfig.endpoint,
port: s3Config.port, port: storageConfig.port,
accessKey: s3Config.accessKey, accessKey: storageConfig.accessKey,
accessSecret: s3Config.accessSecret, accessSecret: storageConfig.accessSecret,
useSsl: s3Config.useSsl ?? true, useSsl: storageConfig.useSsl ?? true,
}); });
return this.smartbucketInstance; return this.smartbucketInstance;
@@ -103,11 +103,11 @@ export class TsView {
} }
/** /**
* Load configuration from npmextra.json * Load configuration from smartconfig.json
*/ */
private loadNpmextraConfig(cwd?: string): interfaces.INpmextraConfig { private loadSmartconfigConfig(cwd?: string): interfaces.ISmartconfigConfig {
const npmextra = new plugins.npmextra.Npmextra(cwd || process.cwd()); const smartconfigInstance = new plugins.smartconfig.Smartconfig(cwd || process.cwd());
const config = npmextra.dataFor<interfaces.INpmextraConfig>('@git.zone/tsview', {}); const config = smartconfigInstance.dataFor<interfaces.ISmartconfigConfig>('@git.zone/tsview', {});
return config || {}; return config || {};
} }
@@ -135,7 +135,7 @@ export class TsView {
* @param cliPort - Optional port number from CLI (highest priority) * @param cliPort - Optional port number from CLI (highest priority)
*/ */
public async start(cliPort?: number): Promise<number> { public async start(cliPort?: number): Promise<number> {
const npmextraConfig = await this.loadNpmextraConfig(); const smartconfigConfig = await this.loadSmartconfigConfig();
let port: number; let port: number;
let portWasExplicitlySet = false; let portWasExplicitlySet = false;
@@ -144,9 +144,9 @@ export class TsView {
// CLI has highest priority // CLI has highest priority
port = cliPort; port = cliPort;
portWasExplicitlySet = true; portWasExplicitlySet = true;
} else if (npmextraConfig.port) { } else if (smartconfigConfig.port) {
// Config port specified // Config port specified
port = npmextraConfig.port; port = smartconfigConfig.port;
portWasExplicitlySet = true; portWasExplicitlySet = true;
} else { } else {
// Auto-find free port // Auto-find free port
@@ -158,11 +158,11 @@ export class TsView {
const isFree = await network.isLocalPortUnused(port); const isFree = await network.isLocalPortUnused(port);
if (!isFree) { if (!isFree) {
if (npmextraConfig.killIfBusy) { if (smartconfigConfig.killIfBusy) {
console.log(`Port ${port} is busy. Killing existing process...`); console.log(`Port ${port} is busy. Killing existing process...`);
await this.killProcessOnPort(port); await this.killProcessOnPort(port);
} else if (portWasExplicitlySet) { } else if (portWasExplicitlySet) {
throw new Error(`Port ${port} is busy. Set "killIfBusy": true in npmextra.json to auto-kill, or use a different port.`); throw new Error(`Port ${port} is busy. Set "killIfBusy": true in smartconfig.json to auto-kill, or use a different port.`);
} else { } else {
// Auto port was already free, shouldn't happen, but fallback // Auto port was already free, shouldn't happen, but fallback
port = await this.findFreePort(port + 1); port = await this.findFreePort(port + 1);
@@ -175,7 +175,7 @@ export class TsView {
console.log(`TsView server started on http://localhost:${port}`); console.log(`TsView server started on http://localhost:${port}`);
// Open browser (default: true, can be disabled via config) // Open browser (default: true, can be disabled via config)
const shouldOpenBrowser = npmextraConfig.openBrowser !== false; const shouldOpenBrowser = smartconfigConfig.openBrowser !== false;
if (shouldOpenBrowser) { if (shouldOpenBrowser) {
try { try {
await plugins.smartopen.openUrl(`http://localhost:${port}`); await plugins.smartopen.openUrl(`http://localhost:${port}`);

View File

@@ -19,7 +19,7 @@ export class TsViewCli {
this.smartcli.standardCommand().subscribe(async (argvArg) => { this.smartcli.standardCommand().subscribe(async (argvArg) => {
await this.startViewer({ await this.startViewer({
port: argvArg.port as number | undefined, port: argvArg.port as number | undefined,
s3Only: false, storageOnly: false,
mongoOnly: false, mongoOnly: false,
}); });
}); });
@@ -29,7 +29,7 @@ export class TsViewCli {
s3Command.subscribe(async (argvArg) => { s3Command.subscribe(async (argvArg) => {
await this.startViewer({ await this.startViewer({
port: argvArg.port as number | undefined, port: argvArg.port as number | undefined,
s3Only: true, storageOnly: true,
mongoOnly: false, mongoOnly: false,
}); });
}); });
@@ -39,7 +39,7 @@ export class TsViewCli {
mongoCommand.subscribe(async (argvArg) => { mongoCommand.subscribe(async (argvArg) => {
await this.startViewer({ await this.startViewer({
port: argvArg.port as number | undefined, port: argvArg.port as number | undefined,
s3Only: false, storageOnly: false,
mongoOnly: true, mongoOnly: true,
}); });
}); });
@@ -56,7 +56,7 @@ export class TsViewCli {
*/ */
private async startViewer(options: { private async startViewer(options: {
port?: number; port?: number;
s3Only: boolean; storageOnly: boolean;
mongoOnly: boolean; mongoOnly: boolean;
}): Promise<void> { }): Promise<void> {
console.log('Starting TsView...'); console.log('Starting TsView...');
@@ -67,10 +67,10 @@ export class TsViewCli {
await viewer.loadConfigFromEnv(); await viewer.loadConfigFromEnv();
// Check what's configured // Check what's configured
const hasS3 = viewer.config.hasS3(); const hasStorage = viewer.config.hasStorage();
const hasMongo = viewer.config.hasMongo(); const hasMongo = viewer.config.hasMongo();
if (!hasS3 && !hasMongo) { if (!hasStorage && !hasMongo) {
console.error('Error: No S3 or MongoDB configuration found.'); console.error('Error: No S3 or MongoDB configuration found.');
console.error('Please create .nogit/env.json with your configuration.'); console.error('Please create .nogit/env.json with your configuration.');
console.error(''); console.error('');
@@ -87,7 +87,7 @@ export class TsViewCli {
process.exit(1); process.exit(1);
} }
if (options.s3Only && !hasS3) { if (options.storageOnly && !hasStorage) {
console.error('Error: S3 configuration not found in .nogit/env.json'); console.error('Error: S3 configuration not found in .nogit/env.json');
process.exit(1); process.exit(1);
} }
@@ -98,7 +98,7 @@ export class TsViewCli {
} }
// Log what's available // Log what's available
if (hasS3) { if (hasStorage) {
console.log('S3 storage configured'); console.log('S3 storage configured');
} }
if (hasMongo) { if (hasMongo) {

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tsview', name: '@git.zone/tsview',
version: '1.12.0', version: '1.12.6',
description: 'A CLI tool for viewing S3 and MongoDB data with a web UI' description: 'A CLI tool for viewing S3 and MongoDB data with a web UI'
} }

View File

@@ -1,10 +1,10 @@
import type { IS3DataProvider } from '@design.estate/dees-catalog'; import type { IStorageDataProvider } from '@design.estate/dees-catalog';
import { apiService } from '../services/index.js'; import { apiService } from '../services/index.js';
/** /**
* Adapter that implements IS3DataProvider by delegating to tsview's ApiService * Adapter that implements IStorageDataProvider by delegating to tsview's ApiService
*/ */
export class TsviewS3DataProvider implements IS3DataProvider { export class TsviewS3DataProvider implements IStorageDataProvider {
async listObjects(bucket: string, prefix?: string, delimiter?: string) { async listObjects(bucket: string, prefix?: string, delimiter?: string) {
return apiService.listObjects(bucket, prefix, delimiter); return apiService.listObjects(bucket, prefix, delimiter);
} }

View File

@@ -1,10 +1,11 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import { changeStreamService, type IActivityEvent, type IMongoChangeEvent, type IS3ChangeEvent } from '../services/index.js'; import { changeStreamService, type IActivityEvent, type IMongoChangeEvent } from '../services/index.js';
import type { IStorageChangeEvent } from '@design.estate/dees-catalog';
import { themeStyles } from '../styles/index.js'; import { themeStyles } from '../styles/index.js';
const { html, css, cssManager, customElement, property, state, DeesElement } = plugins; const { html, css, cssManager, customElement, property, state, DeesElement } = plugins;
type TFilterMode = 'all' | 'mongodb' | 's3'; type TFilterMode = 'all' | 'mongodb' | 'storage';
@customElement('tsview-activity-stream') @customElement('tsview-activity-stream')
export class TsviewActivityStream extends DeesElement { export class TsviewActivityStream extends DeesElement {
@@ -426,8 +427,8 @@ export class TsviewActivityStream extends DeesElement {
const mongoEvent = event.event as IMongoChangeEvent; const mongoEvent = event.event as IMongoChangeEvent;
return `${mongoEvent.database}.${mongoEvent.collection}`; return `${mongoEvent.database}.${mongoEvent.collection}`;
} else { } else {
const s3Event = event.event as IS3ChangeEvent; const storageEvent = event.event as IStorageChangeEvent;
return s3Event.bucket; return storageEvent.bucket;
} }
} }
@@ -439,8 +440,8 @@ export class TsviewActivityStream extends DeesElement {
} }
return ''; return '';
} else { } else {
const s3Event = event.event as IS3ChangeEvent; const storageEvent = event.event as IStorageChangeEvent;
return s3Event.key; return storageEvent.key;
} }
} }
@@ -464,12 +465,12 @@ export class TsviewActivityStream extends DeesElement {
}) })
); );
} else { } else {
const s3Event = event.event as IS3ChangeEvent; const storageEvent = event.event as IStorageChangeEvent;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('navigate-to-s3', { new CustomEvent('navigate-to-s3', {
detail: { detail: {
bucket: s3Event.bucket, bucket: storageEvent.bucket,
key: s3Event.key, key: storageEvent.key,
}, },
bubbles: true, bubbles: true,
composed: true, composed: true,
@@ -529,8 +530,8 @@ export class TsviewActivityStream extends DeesElement {
MongoDB MongoDB
</button> </button>
<button <button
class="filter-tab ${this.filterMode === 's3' ? 'active' : ''}" class="filter-tab ${this.filterMode === 'storage' ? 'active' : ''}"
@click=${() => this.setFilterMode('s3')} @click=${() => this.setFilterMode('storage')}
> >
S3 S3
</button> </button>

View File

@@ -2,7 +2,7 @@ import * as plugins from '../plugins.js';
import { apiService, changeStreamService } from '../services/index.js'; import { apiService, changeStreamService } from '../services/index.js';
import { themeStyles } from '../styles/index.js'; import { themeStyles } from '../styles/index.js';
import { s3DataProvider } from '../adapters/s3-data-provider.js'; import { s3DataProvider } from '../adapters/s3-data-provider.js';
import type { IS3ChangeEvent } from '@design.estate/dees-catalog'; import type { IStorageChangeEvent } from '@design.estate/dees-catalog';
const { html, css, cssManager, customElement, state, DeesElement } = plugins; const { html, css, cssManager, customElement, state, DeesElement } = plugins;
const { DeesContextmenu } = plugins.deesCatalog; const { DeesContextmenu } = plugins.deesCatalog;
@@ -1041,16 +1041,16 @@ export class TsviewApp extends DeesElement {
return html` return html`
<div class="content-area"> <div class="content-area">
<dees-s3-browser <dees-storage-browser
.dataProvider=${s3DataProvider} .dataProvider=${s3DataProvider}
.bucketName=${this.selectedBucket} .bucketName=${this.selectedBucket}
.onChangeEvent=${(callback: (event: IS3ChangeEvent) => void) => { .onChangeEvent=${(callback: (event: IStorageChangeEvent) => void) => {
const sub = changeStreamService const sub = changeStreamService
.getBucketChanges(this.selectedBucket) .getBucketChanges(this.selectedBucket)
.subscribe(callback); .subscribe(callback);
return () => sub.unsubscribe(); return () => sub.unsubscribe();
}} }}
></dees-s3-browser> ></dees-storage-browser>
</div> </div>
`; `;
} }

View File

@@ -1,13 +1,6 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import type { IStorageObject } from '@design.estate/dees-catalog';
// Import interfaces from shared types export type { IStorageObject };
// Note: In bundled form these are inlined
export interface IS3Object {
key: string;
size?: number;
lastModified?: string;
isPrefix?: boolean;
}
export interface IMongoDatabase { export interface IMongoDatabase {
name: string; name: string;
@@ -100,7 +93,7 @@ export class ApiService {
bucketName: string, bucketName: string,
prefix?: string, prefix?: string,
delimiter?: string delimiter?: string
): Promise<{ objects: IS3Object[]; prefixes: string[] }> { ): Promise<{ objects: IStorageObject[]; prefixes: string[] }> {
return this.request('listObjects', { bucketName, prefix, delimiter }); return this.request('listObjects', { bucketName, prefix, delimiter });
} }

View File

@@ -1,4 +1,6 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import type { IStorageChangeEvent } from '@design.estate/dees-catalog';
export type { IStorageChangeEvent };
/** /**
* MongoDB change event * MongoDB change event
@@ -16,25 +18,13 @@ export interface IMongoChangeEvent {
timestamp: string; timestamp: string;
} }
/**
* S3 change event
*/
export interface IS3ChangeEvent {
type: 'add' | 'modify' | 'delete';
key: string;
size?: number;
etag?: string;
lastModified?: Date;
bucket: string;
}
/** /**
* Combined activity event * Combined activity event
*/ */
export interface IActivityEvent { export interface IActivityEvent {
id: string; id: string;
source: 'mongodb' | 's3'; source: 'mongodb' | 'storage';
event: IMongoChangeEvent | IS3ChangeEvent; event: IMongoChangeEvent | IStorageChangeEvent;
timestamp: string; timestamp: string;
} }
@@ -42,7 +32,7 @@ export interface IActivityEvent {
* Subscription info tracked by the service * Subscription info tracked by the service
*/ */
interface ISubscription { interface ISubscription {
type: 'mongo' | 's3' | 'activity'; type: 'mongo' | 'storage' | 'activity';
key: string; // "db/collection" or "bucket/prefix" or "activity" key: string; // "db/collection" or "bucket/prefix" or "activity"
subscriptionId: string; subscriptionId: string;
} }
@@ -69,7 +59,7 @@ export class ChangeStreamService {
// RxJS Subjects for UI components // RxJS Subjects for UI components
public readonly mongoChanges$ = new plugins.smartrx.rxjs.Subject<IMongoChangeEvent>(); public readonly mongoChanges$ = new plugins.smartrx.rxjs.Subject<IMongoChangeEvent>();
public readonly s3Changes$ = new plugins.smartrx.rxjs.Subject<IS3ChangeEvent>(); public readonly storageChanges$ = new plugins.smartrx.rxjs.Subject<IStorageChangeEvent>();
public readonly activityEvents$ = new plugins.smartrx.rxjs.Subject<IActivityEvent>(); public readonly activityEvents$ = new plugins.smartrx.rxjs.Subject<IActivityEvent>();
public readonly connectionStatus$ = new plugins.smartrx.rxjs.ReplaySubject<'connected' | 'disconnected' | 'connecting'>(1); public readonly connectionStatus$ = new plugins.smartrx.rxjs.ReplaySubject<'connected' | 'disconnected' | 'connecting'>(1);
@@ -193,8 +183,8 @@ export class ChangeStreamService {
router.addTypedHandler( router.addTypedHandler(
new plugins.typedrequest.TypedHandler<any>( new plugins.typedrequest.TypedHandler<any>(
'pushS3Change', 'pushS3Change',
async (data: { event: IS3ChangeEvent }) => { async (data: { event: IStorageChangeEvent }) => {
this.s3Changes$.next(data.event); this.storageChanges$.next(data.event);
return { received: true }; return { received: true };
} }
) )
@@ -540,8 +530,8 @@ export class ChangeStreamService {
/** /**
* Get S3 changes as an Observable * Get S3 changes as an Observable
*/ */
public getS3Changes(): plugins.smartrx.rxjs.Observable<IS3ChangeEvent> { public getStorageChanges(): plugins.smartrx.rxjs.Observable<IStorageChangeEvent> {
return this.s3Changes$.asObservable(); return this.storageChanges$.asObservable();
} }
/** /**
@@ -565,8 +555,8 @@ export class ChangeStreamService {
/** /**
* Get filtered S3 changes for a specific bucket/prefix * Get filtered S3 changes for a specific bucket/prefix
*/ */
public getBucketChanges(bucket: string, prefix?: string): plugins.smartrx.rxjs.Observable<IS3ChangeEvent> { public getBucketChanges(bucket: string, prefix?: string): plugins.smartrx.rxjs.Observable<IStorageChangeEvent> {
return this.s3Changes$.pipe( return this.storageChanges$.pipe(
plugins.smartrx.rxjs.ops.filter((event) => { plugins.smartrx.rxjs.ops.filter((event) => {
if (event.bucket !== bucket) return false; if (event.bucket !== bucket) return false;
if (prefix && !event.key.startsWith(prefix)) return false; if (prefix && !event.key.startsWith(prefix)) return false;

View File

@@ -4,9 +4,7 @@
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"esModuleInterop": true, "esModuleInterop": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true
"baseUrl": ".",
"paths": {}
}, },
"exclude": ["dist_*/**/*.d.ts"] "exclude": ["dist_*/**/*.d.ts"]
} }