mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-10-28 08:01:15 +00:00
refactor: unify docker deployment and restructure frontend architecture
This commit implements a unified Docker deployment strategy, moving from separate frontend and backend images to a single, multi-stage build image containing both services. It also introduces a major reorganization of the frontend directory structure and simplifies the environment configuration. Key changes: - **Deployment**: Added a multi-stage `Dockerfile` and `docker-entrypoint.sh` to package the Go backend and Nginx-served frontend into a single container. - **CI/CD**: Updated GitHub Actions workflows (`ci-cd.yml`, `release.yml`) to build and push the new unified image instead of separate ones. - **Frontend Refactor**: Reorganized `frontend/src/pages` into a domain-driven directory structure (e.g., `auth/`, `admin/`, `content/`, `communication/`, `productivity/`, `settings/`, `misc/`). - **Configuration**: Simplified `.env.example` and updated `docker-compose.yml` to reflect the unified service model and single host port. - **Cleanup**: Removed deprecated `docker-compose.demo.yml`, `docker-compose.prod.yml`, and various unused frontend components and services. - **Backend**: Refactored configuration loading to use exported `GetDurationEnv` for better consistency.
This commit is contained in:
1 parent
c6a99c7e21
commit
6c448b336a
71 files changed
+135367
-4481
No files matched your search
+6
-44
@@ -1,54 +1,16 @@
|
|||||||
# Server Configuration
|
# Trackeep Configuration for Casa OS
|
||||||
FRONTEND_PORT=3000
|
# Only required variables - everything else is auto-configured
|
||||||
FRONTEND_HOST_PORT=3900
|
|
||||||
BACKEND_PORT=8080
|
|
||||||
BACKEND_HOST_PORT=9000
|
|
||||||
GIN_MODE=debug
|
|
||||||
|
|
||||||
# Demo Mode Configuration
|
# Host port for the application (default: 8080)
|
||||||
# Set to true for demo mode (read-only with demo data)
|
HOST_PORT=8080
|
||||||
# Set to false for normal mode (full functionality)
|
|
||||||
VITE_DEMO_MODE=true
|
|
||||||
VITE_API_URL=http://localhost:9000
|
|
||||||
FRONTEND_URL=http://localhost:3900
|
|
||||||
PUBLIC_API_URL=http://localhost:9000
|
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
DB_TYPE=postgres
|
DB_PASSWORD=your_secure_password_here
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_HOST_PORT=5433
|
|
||||||
DB_USER=trackeep
|
DB_USER=trackeep
|
||||||
DB_PASSWORD=your_password_here
|
|
||||||
DB_NAME=trackeep
|
DB_NAME=trackeep
|
||||||
DB_SSL_MODE=disable
|
|
||||||
|
|
||||||
# DragonflyDB Configuration
|
# DragonflyDB Configuration
|
||||||
DRAGONFLY_ADDR=dragonfly:6379
|
|
||||||
DRAGONFLY_PORT=6379
|
|
||||||
DRAGONFLY_HOST_PORT=6380
|
|
||||||
DRAGONFLY_PASSWORD=your_dragonfly_password_here
|
DRAGONFLY_PASSWORD=your_dragonfly_password_here
|
||||||
|
|
||||||
# JWT Configuration (also used for encryption)
|
# JWT Secret (generate with: openssl rand -hex 32)
|
||||||
# Generate a secure 64-character hex string using: openssl rand -hex 32
|
|
||||||
JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
|
JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
|
||||||
# Token expiration time (e.g., 24h, 1h, 30m, 7d)
|
|
||||||
JWT_EXPIRES_IN=24h
|
|
||||||
|
|
||||||
# GitHub backup storage
|
|
||||||
# Self-hosted Trackeep instances use the unified control service for GitHub sign-in
|
|
||||||
# and GitHub App installation. No per-instance GitHub App credentials are required.
|
|
||||||
GITHUB_BACKUP_ROOT=./data/github-backups
|
|
||||||
GITHUB_BACKUP_TIMEOUT=10m
|
|
||||||
|
|
||||||
# File Upload Configuration
|
|
||||||
UPLOAD_DIR=./uploads
|
|
||||||
MAX_FILE_SIZE=10485760
|
|
||||||
|
|
||||||
# CORS Configuration
|
|
||||||
CORS_ALLOWED_ORIGINS=*
|
|
||||||
|
|
||||||
# Auto Update Configuration
|
|
||||||
AUTO_UPDATE_CHECK=false
|
|
||||||
UPDATE_CHECK_INTERVAL=24h
|
|
||||||
PRERELEASE_UPDATES=false
|
|
||||||
@@ -128,43 +128,23 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta-backend
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/backend
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=sha,prefix={{branch}}-
|
type=sha,prefix={{branch}}-
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Build and push unified image
|
||||||
id: meta-frontend
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/frontend
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=sha,prefix={{branch}}-
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
|
|
||||||
- name: Build and push backend image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: ./backend
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta-backend.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Build and push frontend image
|
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./frontend/Dockerfile
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta-frontend.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
# deploy:
|
# deploy:
|
||||||
# name: Deploy to Production
|
# name: Deploy to Production
|
||||||
|
|||||||
@@ -37,27 +37,24 @@ jobs:
|
|||||||
build-and-push:
|
build-and-push:
|
||||||
needs: extract-version
|
needs: extract-version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
service: [backend, frontend]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ matrix.service }}
|
images: ${{ env.REGISTRY }}
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
@@ -66,36 +63,30 @@ jobs:
|
|||||||
version=${{ needs.extract-version.outputs.version }}
|
version=${{ needs.extract-version.outputs.version }}
|
||||||
build-date=${{ github.event.head_commit.timestamp }}
|
build-date=${{ github.event.head_commit.timestamp }}
|
||||||
commit=${{ github.sha }}
|
commit=${{ github.sha }}
|
||||||
service=${{ matrix.service }}
|
|
||||||
prerelease=${{ needs.extract-version.outputs.is-prerelease }}
|
prerelease=${{ needs.extract-version.outputs.is-prerelease }}
|
||||||
|
|
||||||
- name: Build and push ${{ matrix.service }}
|
- name: Build and push unified image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: |
|
context: .
|
||||||
backend=./backend
|
|
||||||
frontend=.
|
|
||||||
file: |
|
|
||||||
backend=./backend/Dockerfile
|
|
||||||
frontend=./frontend/Dockerfile
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
- name: Generate SBOM
|
- name: Generate SBOM
|
||||||
uses: anchore/sbom-action@v0
|
uses: anchore/sbom-action@v0
|
||||||
with:
|
with:
|
||||||
image: ${{ env.REGISTRY }}/${{ matrix.service }}:${{ needs.extract-version.outputs.version }}
|
image: ${{ env.REGISTRY }}:${{ needs.extract-version.outputs.version }}
|
||||||
format: spdx-json
|
format: spdx-json
|
||||||
output-file: ./sbom-${{ matrix.service }}.spdx.json
|
output-file: ./sbom.spdx.json
|
||||||
|
|
||||||
- name: Upload SBOM
|
- name: Upload SBOM
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: sbom-${{ matrix.service }}
|
name: sbom
|
||||||
path: ./sbom-${{ matrix.service }}.spdx.json
|
path: ./sbom.spdx.json
|
||||||
|
|
||||||
create-github-release:
|
create-github-release:
|
||||||
needs: [extract-version, build-and-push]
|
needs: [extract-version, build-and-push]
|
||||||
@@ -107,80 +98,62 @@ jobs:
|
|||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
tag: v${{ needs.extract-version.outputs.version }}
|
|
||||||
name: Trackeep v${{ needs.extract-version.outputs.version }}
|
name: Trackeep v${{ needs.extract-version.outputs.version }}
|
||||||
body: |
|
body: |
|
||||||
## 🚀 Trackeep v${{ needs.extract-version.outputs.version }}
|
## 🚀 Trackeep v${{ needs.extract-version.outputs.version }}
|
||||||
|
|
||||||
### 🐳 Docker Images
|
### 🐳 Docker Image
|
||||||
- **Backend**: `ghcr.io/dvorinka/trackeep/backend:${{ needs.extract-version.outputs.version }}`
|
- **Unified**: `ghcr.io/dvorinka/trackeep:${{ needs.extract-version.outputs.version }}`
|
||||||
- **Frontend**: `ghcr.io/dvorinka/trackeep/frontend:${{ needs.extract-version.outputs.version }}`
|
- **Latest**: `ghcr.io/dvorinka/trackeep:latest`
|
||||||
- **Latest**: `ghcr.io/dvorinka/trackeep/backend:latest` and `ghcr.io/dvorinka/trackeep/frontend:latest`
|
|
||||||
|
|
||||||
### 📋 Changes
|
### 📋 Changes
|
||||||
${{ github.event.head_commit.message }}
|
${{ github.event.head_commit.message }}
|
||||||
|
|
||||||
### 🔧 Installation
|
### 🔧 Installation
|
||||||
```bash
|
```bash
|
||||||
# Set version
|
# Deploy with docker compose
|
||||||
export APP_VERSION=${{ needs.extract-version.outputs.version }}
|
docker compose up -d
|
||||||
|
|
||||||
# Deploy with production compose
|
|
||||||
docker compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### ⚡ Auto-Updates
|
### ⚡ Auto-Updates
|
||||||
The application includes a built-in update system that:
|
The application includes a built-in update system that:
|
||||||
- ✅ Automatically checks for updates every 24 hours
|
- ✅ Automatically checks for updates every 24 hours
|
||||||
- ✅ Shows update notifications in the left navigation
|
- ✅ Shows update notifications in the left navigation
|
||||||
- ✅ One-click installation from the UI
|
- ✅ One-click installation from the UI
|
||||||
- ✅ No authentication or setup required
|
- ✅ No authentication or setup required
|
||||||
|
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: ${{ needs.extract-version.outputs.is-prerelease }}
|
prerelease: ${{ needs.extract-version.outputs.is-prerelease }}
|
||||||
files: |
|
files: sbom.spdx.json
|
||||||
sbom-backend.spdx.json
|
|
||||||
sbom-frontend.spdx.json
|
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|
||||||
update-docker-compose-prod:
|
update-version-files:
|
||||||
needs: extract-version
|
needs: extract-version
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Update version in all files
|
- name: Update version in all files
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ needs.extract-version.outputs.version }}"
|
VERSION="${{ needs.extract-version.outputs.version }}"
|
||||||
echo "🏷️ Updating all version files to $VERSION"
|
echo "🏷️ Updating all version files to $VERSION"
|
||||||
|
|
||||||
# Update frontend package.json
|
# Update frontend package.json
|
||||||
if [ -f "frontend/package.json" ]; then
|
if [ -f "frontend/package.json" ]; then
|
||||||
echo "📝 Updating frontend/package.json..."
|
echo "📝 Updating frontend/package.json..."
|
||||||
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" frontend/package.json
|
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" frontend/package.json
|
||||||
echo "✅ Frontend updated to $VERSION"
|
echo "✅ Frontend updated to $VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update backend go.mod
|
# Update backend go.mod
|
||||||
if [ -f "backend/go.mod" ]; then
|
if [ -f "backend/go.mod" ]; then
|
||||||
echo "📝 Updating backend/go.mod..."
|
echo "📝 Updating backend/go.mod..."
|
||||||
sed -i "s/go [^\"]*\"/go $VERSION/" backend/go.mod
|
sed -i "s/go [^\"]*\"/go $VERSION/" backend/go.mod
|
||||||
echo "✅ Backend updated to $VERSION"
|
echo "✅ Backend updated to $VERSION"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update docker-compose files
|
|
||||||
if [ -f "docker-compose.yml" ]; then
|
|
||||||
sed -i "s/APP_VERSION=.*/APP_VERSION=$VERSION/" docker-compose.yml
|
|
||||||
echo "✅ docker-compose.yml updated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "docker-compose.prod.yml" ]; then
|
|
||||||
sed -i "s/APP_VERSION=.*/APP_VERSION=$VERSION/" docker-compose.prod.yml
|
|
||||||
echo "✅ docker-compose.prod.yml updated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🎉 All version files updated to $VERSION"
|
echo "🎉 All version files updated to $VERSION"
|
||||||
|
|
||||||
- name: Commit updated version files
|
- name: Commit updated version files
|
||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- img "Trackeep Logo" [ref=e7]
|
|
||||||
- heading "Trackeep" [level=1] [ref=e8]
|
|
||||||
- paragraph [ref=e9]: Create your account
|
|
||||||
- generic [ref=e10]:
|
|
||||||
- generic [ref=e13]: Create Admin Account
|
|
||||||
- paragraph [ref=e14]: No accounts exist yet. Create the first administrator account to get started.
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- generic [ref=e16]:
|
|
||||||
- generic [ref=e17]: Email
|
|
||||||
- textbox "Email" [ref=e18]:
|
|
||||||
- /placeholder: [email protected]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- generic [ref=e20]: Username
|
|
||||||
- textbox "Username" [ref=e21]:
|
|
||||||
- /placeholder: username
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- generic [ref=e23]: Full Name
|
|
||||||
- textbox "Full Name" [ref=e24]:
|
|
||||||
- /placeholder: Your Name
|
|
||||||
- generic [ref=e25]:
|
|
||||||
- generic [ref=e26]: Password
|
|
||||||
- textbox "Password" [ref=e27]:
|
|
||||||
- /placeholder: ••••••••
|
|
||||||
- button "Sign Up" [ref=e28] [cursor=pointer]
|
|
||||||
- generic [ref=e31]: or
|
|
||||||
- button "Continue with GitHub App" [ref=e33] [cursor=pointer]:
|
|
||||||
- img [ref=e34]
|
|
||||||
- text: Continue with GitHub App
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e37]:
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e42] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e43]
|
|
||||||
- generic [ref=e44]: Trackeep
|
|
||||||
- group [ref=e46]:
|
|
||||||
- button "Trackeep Workspace" [ref=e47] [cursor=pointer]:
|
|
||||||
- generic [ref=e48]:
|
|
||||||
- img [ref=e50]
|
|
||||||
- generic [ref=e53]: Trackeep Workspace
|
|
||||||
- img [ref=e55]
|
|
||||||
- navigation [ref=e57]:
|
|
||||||
- link "Home" [ref=e58] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e59]:
|
|
||||||
- img [ref=e60]
|
|
||||||
- generic [ref=e64]: Home
|
|
||||||
- link "Bookmarks" [ref=e66] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e67]:
|
|
||||||
- img [ref=e68]
|
|
||||||
- generic [ref=e70]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e72] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e73]:
|
|
||||||
- img [ref=e74]
|
|
||||||
- generic [ref=e77]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e79] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e84]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e86] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e87]:
|
|
||||||
- img [ref=e88]
|
|
||||||
- generic [ref=e90]: Calendar
|
|
||||||
- link "Files" [ref=e92] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e93]:
|
|
||||||
- img [ref=e94]
|
|
||||||
- generic [ref=e96]: Files
|
|
||||||
- link "Notes" [ref=e98] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e99]:
|
|
||||||
- img [ref=e100]
|
|
||||||
- generic [ref=e102]: Notes
|
|
||||||
- link "Messages" [ref=e104] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e105]:
|
|
||||||
- img [ref=e106]
|
|
||||||
- generic [ref=e108]: Messages
|
|
||||||
- link "YouTube" [ref=e110] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e111]:
|
|
||||||
- img [ref=e112]
|
|
||||||
- generic [ref=e115]: YouTube
|
|
||||||
- link "Members" [ref=e117] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e118]:
|
|
||||||
- img [ref=e119]
|
|
||||||
- generic [ref=e124]: Members
|
|
||||||
- link "Learning" [ref=e126] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e131]: Learning
|
|
||||||
- link "Stats" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e137]: Stats
|
|
||||||
- link "GitHub" [ref=e139] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e140]:
|
|
||||||
- img [ref=e141]
|
|
||||||
- generic [ref=e143]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e145] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e146]:
|
|
||||||
- img [ref=e147]
|
|
||||||
- generic [ref=e154]: AI Assistant
|
|
||||||
- navigation [ref=e156]:
|
|
||||||
- link "Removed stuff" [ref=e157] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e158]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- generic [ref=e162]: Removed stuff
|
|
||||||
- link "Settings" [ref=e164] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e165]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- generic [ref=e169]: Settings
|
|
||||||
- button "Logout" [ref=e171] [cursor=pointer]:
|
|
||||||
- generic [ref=e172]:
|
|
||||||
- img [ref=e173]
|
|
||||||
- generic [ref=e177]: Logout
|
|
||||||
- generic [ref=e179]:
|
|
||||||
- generic [ref=e180]:
|
|
||||||
- generic [ref=e181]:
|
|
||||||
- button [ref=e182] [cursor=pointer]:
|
|
||||||
- img [ref=e183]
|
|
||||||
- button "Quick search" [ref=e184] [cursor=pointer]:
|
|
||||||
- img [ref=e185]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e188]:
|
|
||||||
- button "Import a document" [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e194] [cursor=pointer]:
|
|
||||||
- img [ref=e195]
|
|
||||||
- img [ref=e200]
|
|
||||||
- button "AU" [ref=e204] [cursor=pointer]:
|
|
||||||
- generic [ref=e205]: AU
|
|
||||||
- img [ref=e206]
|
|
||||||
- main [ref=e208]:
|
|
||||||
- generic [ref=e210]:
|
|
||||||
- generic [ref=e211]:
|
|
||||||
- generic [ref=e213]:
|
|
||||||
- img [ref=e215]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- paragraph [ref=e219]: "0"
|
|
||||||
- paragraph [ref=e220]: Documents
|
|
||||||
- generic [ref=e222]:
|
|
||||||
- img [ref=e224]
|
|
||||||
- generic [ref=e226]:
|
|
||||||
- paragraph [ref=e227]: "0"
|
|
||||||
- paragraph [ref=e228]: Bookmarks
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- img [ref=e232]
|
|
||||||
- generic [ref=e235]:
|
|
||||||
- paragraph [ref=e236]: "0"
|
|
||||||
- paragraph [ref=e237]: Tasks
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- paragraph [ref=e244]: "0"
|
|
||||||
- paragraph [ref=e245]: Notes
|
|
||||||
- generic [ref=e246]:
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- img [ref=e250]
|
|
||||||
- generic [ref=e253]:
|
|
||||||
- paragraph [ref=e254]: "0"
|
|
||||||
- paragraph [ref=e255]: Videos
|
|
||||||
- generic [ref=e257]:
|
|
||||||
- img [ref=e259]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- paragraph [ref=e263]: "0"
|
|
||||||
- paragraph [ref=e264]: Learning
|
|
||||||
- generic [ref=e266]:
|
|
||||||
- img [ref=e268]
|
|
||||||
- generic [ref=e271]:
|
|
||||||
- paragraph [ref=e272]: 0m
|
|
||||||
- paragraph [ref=e273]: Time
|
|
||||||
- generic [ref=e275]:
|
|
||||||
- img [ref=e277]
|
|
||||||
- generic [ref=e280]:
|
|
||||||
- paragraph [ref=e281]: 0%
|
|
||||||
- paragraph [ref=e282]: Productivity
|
|
||||||
- generic [ref=e284]:
|
|
||||||
- img [ref=e286]
|
|
||||||
- generic [ref=e288]:
|
|
||||||
- paragraph [ref=e289]: "0"
|
|
||||||
- paragraph [ref=e290]: Documents
|
|
||||||
- generic [ref=e292]:
|
|
||||||
- img [ref=e294]
|
|
||||||
- generic [ref=e296]:
|
|
||||||
- paragraph [ref=e297]: "0"
|
|
||||||
- paragraph [ref=e298]: Notes
|
|
||||||
- generic [ref=e299]:
|
|
||||||
- generic [ref=e300]:
|
|
||||||
- generic [ref=e301]:
|
|
||||||
- img [ref=e302]
|
|
||||||
- heading "Recent Achievements" [level=3] [ref=e305]
|
|
||||||
- paragraph [ref=e306]: No achievements yet.
|
|
||||||
- generic [ref=e307]:
|
|
||||||
- generic [ref=e308]:
|
|
||||||
- img [ref=e309]
|
|
||||||
- heading "Upcoming Deadlines" [level=3] [ref=e312]
|
|
||||||
- paragraph [ref=e313]: No upcoming deadlines.
|
|
||||||
- generic [ref=e314]:
|
|
||||||
- generic [ref=e315]:
|
|
||||||
- generic [ref=e316]:
|
|
||||||
- img [ref=e317]
|
|
||||||
- heading "Task Completion" [level=3] [ref=e319]
|
|
||||||
- generic [ref=e320]:
|
|
||||||
- generic [ref=e321]:
|
|
||||||
- generic [ref=e322]: Progress
|
|
||||||
- generic [ref=e323]: 0/0
|
|
||||||
- paragraph [ref=e325]: 0% completion rate
|
|
||||||
- generic [ref=e326]:
|
|
||||||
- generic [ref=e327]:
|
|
||||||
- paragraph [ref=e328]: "0"
|
|
||||||
- paragraph [ref=e329]: Completed
|
|
||||||
- generic [ref=e330]:
|
|
||||||
- paragraph [ref=e331]: "0"
|
|
||||||
- paragraph [ref=e332]: Active
|
|
||||||
- generic [ref=e333]:
|
|
||||||
- generic [ref=e334]:
|
|
||||||
- img [ref=e335]
|
|
||||||
- heading "Storage Usage" [level=3] [ref=e337]
|
|
||||||
- generic [ref=e338]:
|
|
||||||
- generic [ref=e339]:
|
|
||||||
- generic [ref=e340]: Used Space
|
|
||||||
- generic [ref=e341]: 0 MB
|
|
||||||
- paragraph [ref=e343]: 0% of 50 GB used
|
|
||||||
- generic [ref=e344]:
|
|
||||||
- generic [ref=e345]:
|
|
||||||
- img [ref=e346]
|
|
||||||
- heading "Weekly Activity" [level=3] [ref=e348]
|
|
||||||
- generic [ref=e349]:
|
|
||||||
- paragraph [ref=e351]: No activity data yet.
|
|
||||||
- generic [ref=e352]:
|
|
||||||
- generic [ref=e353]: "Total: 0 activities"
|
|
||||||
- generic [ref=e354]: "Avg: 0 per day"
|
|
||||||
- generic [ref=e355]:
|
|
||||||
- generic [ref=e356]:
|
|
||||||
- button "Upload documents Drag and drop or click to browse" [ref=e357] [cursor=pointer]:
|
|
||||||
- img [ref=e358]
|
|
||||||
- generic [ref=e361]:
|
|
||||||
- generic [ref=e362]: Upload documents
|
|
||||||
- generic [ref=e363]: Drag and drop or click to browse
|
|
||||||
- generic [ref=e364]:
|
|
||||||
- button "Save YouTube Video Save a YouTube video link" [ref=e365] [cursor=pointer]:
|
|
||||||
- img [ref=e366]
|
|
||||||
- generic [ref=e370]:
|
|
||||||
- generic [ref=e371]: Save YouTube Video
|
|
||||||
- generic [ref=e372]: Save a YouTube video link
|
|
||||||
- button "Add Bookmark Save web links" [ref=e373] [cursor=pointer]:
|
|
||||||
- img [ref=e374]
|
|
||||||
- generic [ref=e376]:
|
|
||||||
- generic [ref=e377]: Add Bookmark
|
|
||||||
- generic [ref=e378]: Save web links
|
|
||||||
- generic [ref=e379]:
|
|
||||||
- generic [ref=e381]:
|
|
||||||
- img [ref=e382]
|
|
||||||
- heading "GitHub Activity" [level=3] [ref=e384]
|
|
||||||
- paragraph [ref=e385]: No GitHub activity yet.
|
|
||||||
- generic [ref=e386]:
|
|
||||||
- generic [ref=e387]:
|
|
||||||
- generic [ref=e388]:
|
|
||||||
- img [ref=e389]
|
|
||||||
- heading "Activity Feed" [level=3] [ref=e392]
|
|
||||||
- button [ref=e393] [cursor=pointer]:
|
|
||||||
- img [ref=e394]
|
|
||||||
- generic [ref=e397]:
|
|
||||||
- generic [ref=e400]: (0 items)
|
|
||||||
- generic [ref=e401]:
|
|
||||||
- img [ref=e402]
|
|
||||||
- paragraph [ref=e405]: No activity yet
|
|
||||||
- paragraph [ref=e406]: Start using Trackeep to see your activity here
|
|
||||||
- generic [ref=e408]:
|
|
||||||
- generic [ref=e409]:
|
|
||||||
- img [ref=e410]
|
|
||||||
- heading "Popular Tags" [level=3] [ref=e413]
|
|
||||||
- paragraph [ref=e414]: No tags yet.
|
|
||||||
- heading "Latest imported documents" [level=2] [ref=e415]
|
|
||||||
- table [ref=e418]:
|
|
||||||
- rowgroup [ref=e419]:
|
|
||||||
- row "File name Actions" [ref=e420]:
|
|
||||||
- columnheader "File name" [ref=e421]
|
|
||||||
- columnheader [ref=e422]
|
|
||||||
- columnheader [ref=e423]
|
|
||||||
- columnheader "Actions" [ref=e424]:
|
|
||||||
- generic [ref=e425]: Actions
|
|
||||||
- rowgroup [ref=e426]:
|
|
||||||
- row "No documents yet." [ref=e427]:
|
|
||||||
- cell "No documents yet." [ref=e428]
|
|
||||||
- button "AI Assistant" [ref=e429] [cursor=pointer]:
|
|
||||||
- img [ref=e430]
|
|
||||||
- generic [ref=e437]:
|
|
||||||
- generic [ref=e438]:
|
|
||||||
- generic [ref=e439]:
|
|
||||||
- img [ref=e441]
|
|
||||||
- generic [ref=e448]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e449]
|
|
||||||
- paragraph [ref=e450]: Always here to help
|
|
||||||
- button [ref=e452] [cursor=pointer]:
|
|
||||||
- img [ref=e453]
|
|
||||||
- generic [ref=e457]:
|
|
||||||
- img [ref=e459]
|
|
||||||
- generic [ref=e466]:
|
|
||||||
- paragraph [ref=e467]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e469]: 01:18 PM
|
|
||||||
- generic [ref=e470]:
|
|
||||||
- generic [ref=e471]:
|
|
||||||
- textbox "Type your message..." [ref=e472]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e474]:
|
|
||||||
- button "longcat icon LongCat" [ref=e476] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e477]
|
|
||||||
- generic [ref=e478]: LongCat
|
|
||||||
- img [ref=e479]
|
|
||||||
- generic [ref=e481]:
|
|
||||||
- generic [ref=e482]: longcat
|
|
||||||
- link "AI settings" [ref=e483] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Bookmark" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- textbox "URL *"
|
|
||||||
- textbox "Title (optional)"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- text: Tags
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- textbox "Add tags..."
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Bookmark" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add YouTube Video" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- text: YouTube URL
|
|
||||||
- textbox "https://www.youtube.com/watch?v=..."
|
|
||||||
- generic:
|
|
||||||
- text: Title (optional)
|
|
||||||
- textbox "Video title"
|
|
||||||
- generic:
|
|
||||||
- text: Description (optional)
|
|
||||||
- textbox "Video description"
|
|
||||||
- generic:
|
|
||||||
- text: Tags (comma-separated)
|
|
||||||
- textbox "tutorial, learning, tech"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Video" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e2]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- navigation [ref=e123]:
|
|
||||||
- link "Removed stuff" [ref=e124] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e125]:
|
|
||||||
- img [ref=e126]
|
|
||||||
- generic [ref=e129]: Removed stuff
|
|
||||||
- link "Settings" [ref=e131] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e132]:
|
|
||||||
- img [ref=e133]
|
|
||||||
- generic [ref=e136]: Settings
|
|
||||||
- button "Logout" [ref=e138] [cursor=pointer]:
|
|
||||||
- generic [ref=e139]:
|
|
||||||
- img [ref=e140]
|
|
||||||
- generic [ref=e144]: Logout
|
|
||||||
- generic [ref=e146]:
|
|
||||||
- generic [ref=e147]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- button [ref=e149] [cursor=pointer]:
|
|
||||||
- img [ref=e150]
|
|
||||||
- button "Quick search" [ref=e151] [cursor=pointer]:
|
|
||||||
- img [ref=e152]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- button "Import a document" [ref=e156] [cursor=pointer]:
|
|
||||||
- img [ref=e157]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e161] [cursor=pointer]:
|
|
||||||
- img [ref=e162]
|
|
||||||
- img [ref=e167]
|
|
||||||
- button "AU" [ref=e171] [cursor=pointer]:
|
|
||||||
- generic [ref=e172]: AU
|
|
||||||
- img [ref=e173]
|
|
||||||
- main [ref=e175]:
|
|
||||||
- generic [ref=e177]:
|
|
||||||
- generic [ref=e178]:
|
|
||||||
- heading "Browser Extension Settings" [level=1] [ref=e179]
|
|
||||||
- paragraph [ref=e180]: Manage API keys and browser extensions for secure access to your Trackeep account.
|
|
||||||
- navigation [ref=e182]:
|
|
||||||
- button "Overview" [ref=e183] [cursor=pointer]:
|
|
||||||
- img [ref=e184]
|
|
||||||
- text: Overview
|
|
||||||
- button "API Keys" [ref=e187] [cursor=pointer]:
|
|
||||||
- img [ref=e188]
|
|
||||||
- text: API Keys
|
|
||||||
- button "Extensions" [ref=e192] [cursor=pointer]:
|
|
||||||
- img [ref=e193]
|
|
||||||
- text: Extensions
|
|
||||||
- button "Examples" [ref=e198] [cursor=pointer]:
|
|
||||||
- img [ref=e199]
|
|
||||||
- text: Examples
|
|
||||||
- generic [ref=e202]:
|
|
||||||
- heading "API Keys" [level=2] [ref=e203]
|
|
||||||
- button "Generate New Key" [ref=e204] [cursor=pointer]
|
|
||||||
- generic [ref=e206]:
|
|
||||||
- heading "Registered Extensions" [level=2] [ref=e207]
|
|
||||||
- paragraph [ref=e208]: Manage browser extensions that have access to your account.
|
|
||||||
- button "AI Assistant" [ref=e209] [cursor=pointer]:
|
|
||||||
- img [ref=e210]
|
|
||||||
- generic [ref=e217]:
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- img [ref=e221]
|
|
||||||
- generic [ref=e228]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e229]
|
|
||||||
- paragraph [ref=e230]: Always here to help
|
|
||||||
- button [ref=e232] [cursor=pointer]:
|
|
||||||
- img [ref=e233]
|
|
||||||
- generic [ref=e237]:
|
|
||||||
- img [ref=e239]
|
|
||||||
- generic [ref=e246]:
|
|
||||||
- paragraph [ref=e247]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e249]: 01:23 PM
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- textbox "Type your message..." [ref=e252]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e254]:
|
|
||||||
- button "longcat icon LongCat" [ref=e256] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e257]
|
|
||||||
- generic [ref=e258]: LongCat
|
|
||||||
- img [ref=e259]
|
|
||||||
- generic [ref=e261]:
|
|
||||||
- generic [ref=e262]: longcat
|
|
||||||
- link "AI settings" [ref=e263] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- alert [ref=e266]:
|
|
||||||
- generic [ref=e267]:
|
|
||||||
- img [ref=e269]
|
|
||||||
- paragraph [ref=e272]: Failed to load API keys
|
|
||||||
- button "Close" [ref=e274] [cursor=pointer]:
|
|
||||||
- generic [ref=e275]: Close
|
|
||||||
- img
|
|
||||||
- alert [ref=e280]:
|
|
||||||
- generic [ref=e281]:
|
|
||||||
- img [ref=e283]
|
|
||||||
- paragraph [ref=e286]: Failed to load extensions
|
|
||||||
- button "Close" [ref=e288] [cursor=pointer]:
|
|
||||||
- generic [ref=e289]: Close
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- navigation [ref=e123]:
|
|
||||||
- link "Removed stuff" [ref=e124] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e125]:
|
|
||||||
- img [ref=e126]
|
|
||||||
- generic [ref=e129]: Removed stuff
|
|
||||||
- link "Settings" [ref=e131] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e132]:
|
|
||||||
- img [ref=e133]
|
|
||||||
- generic [ref=e136]: Settings
|
|
||||||
- button "Logout" [ref=e138] [cursor=pointer]:
|
|
||||||
- generic [ref=e139]:
|
|
||||||
- img [ref=e140]
|
|
||||||
- generic [ref=e144]: Logout
|
|
||||||
- generic [ref=e146]:
|
|
||||||
- generic [ref=e147]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- button [ref=e149] [cursor=pointer]:
|
|
||||||
- img [ref=e150]
|
|
||||||
- button "Quick search" [ref=e151] [cursor=pointer]:
|
|
||||||
- img [ref=e152]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- button "Import a document" [ref=e156] [cursor=pointer]:
|
|
||||||
- img [ref=e157]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e161] [cursor=pointer]:
|
|
||||||
- img [ref=e162]
|
|
||||||
- img [ref=e167]
|
|
||||||
- button "AU" [ref=e171] [cursor=pointer]:
|
|
||||||
- generic [ref=e172]: AU
|
|
||||||
- img [ref=e173]
|
|
||||||
- main [ref=e175]:
|
|
||||||
- generic [ref=e177]:
|
|
||||||
- generic [ref=e178]:
|
|
||||||
- heading "Browser Extension Settings" [level=1] [ref=e179]
|
|
||||||
- paragraph [ref=e180]: Manage API keys and browser extensions for secure access to your Trackeep account.
|
|
||||||
- navigation [ref=e182]:
|
|
||||||
- button "Overview" [ref=e183] [cursor=pointer]:
|
|
||||||
- img [ref=e184]
|
|
||||||
- text: Overview
|
|
||||||
- button "API Keys" [ref=e187] [cursor=pointer]:
|
|
||||||
- img [ref=e188]
|
|
||||||
- text: API Keys
|
|
||||||
- button "Extensions" [ref=e192] [cursor=pointer]:
|
|
||||||
- img [ref=e193]
|
|
||||||
- text: Extensions
|
|
||||||
- button "Examples" [ref=e198] [cursor=pointer]:
|
|
||||||
- img [ref=e199]
|
|
||||||
- text: Examples
|
|
||||||
- generic [ref=e202]:
|
|
||||||
- heading "API Keys" [level=2] [ref=e203]
|
|
||||||
- button "Generate New Key" [ref=e204] [cursor=pointer]
|
|
||||||
- generic [ref=e206]:
|
|
||||||
- heading "Registered Extensions" [level=2] [ref=e207]
|
|
||||||
- paragraph [ref=e208]: Manage browser extensions that have access to your account.
|
|
||||||
- button "AI Assistant" [ref=e209] [cursor=pointer]:
|
|
||||||
- img [ref=e210]
|
|
||||||
- generic [ref=e217]:
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- img [ref=e221]
|
|
||||||
- generic [ref=e228]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e229]
|
|
||||||
- paragraph [ref=e230]: Always here to help
|
|
||||||
- button [ref=e232] [cursor=pointer]:
|
|
||||||
- img [ref=e233]
|
|
||||||
- generic [ref=e237]:
|
|
||||||
- img [ref=e239]
|
|
||||||
- generic [ref=e246]:
|
|
||||||
- paragraph [ref=e247]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e249]: 01:26 PM
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- textbox "Type your message..." [ref=e252]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e254]:
|
|
||||||
- button "longcat icon LongCat" [ref=e256] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e257]
|
|
||||||
- generic [ref=e258]: LongCat
|
|
||||||
- img [ref=e259]
|
|
||||||
- generic [ref=e261]:
|
|
||||||
- generic [ref=e262]: longcat
|
|
||||||
- link "AI settings" [ref=e263] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- navigation [ref=e123]:
|
|
||||||
- link "Removed stuff" [ref=e124] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e125]:
|
|
||||||
- img [ref=e126]
|
|
||||||
- generic [ref=e129]: Removed stuff
|
|
||||||
- link "Settings" [ref=e131] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e132]:
|
|
||||||
- img [ref=e133]
|
|
||||||
- generic [ref=e136]: Settings
|
|
||||||
- button "Logout" [ref=e138] [cursor=pointer]:
|
|
||||||
- generic [ref=e139]:
|
|
||||||
- img [ref=e140]
|
|
||||||
- generic [ref=e144]: Logout
|
|
||||||
- generic [ref=e146]:
|
|
||||||
- generic [ref=e147]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- button [ref=e149] [cursor=pointer]:
|
|
||||||
- img [ref=e150]
|
|
||||||
- button "Quick search" [ref=e151] [cursor=pointer]:
|
|
||||||
- img [ref=e152]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- button "Import a document" [ref=e156] [cursor=pointer]:
|
|
||||||
- img [ref=e157]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e161] [cursor=pointer]:
|
|
||||||
- img [ref=e162]
|
|
||||||
- img [ref=e167]
|
|
||||||
- button "AU" [ref=e171] [cursor=pointer]:
|
|
||||||
- generic [ref=e172]: AU
|
|
||||||
- img [ref=e173]
|
|
||||||
- main [ref=e175]:
|
|
||||||
- generic [ref=e177]:
|
|
||||||
- generic [ref=e178]:
|
|
||||||
- heading "Bookmarks" [level=1] [ref=e180]
|
|
||||||
- button "Add Bookmark" [ref=e181] [cursor=pointer]:
|
|
||||||
- img [ref=e182]
|
|
||||||
- text: Add Bookmark
|
|
||||||
- navigation [ref=e185]:
|
|
||||||
- button "Web Bookmarks" [ref=e186] [cursor=pointer]:
|
|
||||||
- img [ref=e187]
|
|
||||||
- text: Web Bookmarks
|
|
||||||
- button "Video Bookmarks" [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Video Bookmarks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search bookmarks..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- generic [ref=e199]:
|
|
||||||
- generic [ref=e200]:
|
|
||||||
- generic [ref=e203]:
|
|
||||||
- heading "Trackeep Updated Bookmark" [level=3] [ref=e204]:
|
|
||||||
- link "Trackeep Updated Bookmark" [ref=e205] [cursor=pointer]:
|
|
||||||
- /url: https://example.com
|
|
||||||
- text: Trackeep Updated Bookmark
|
|
||||||
- img [ref=e206]
|
|
||||||
- paragraph [ref=e210]: https://example.com
|
|
||||||
- paragraph [ref=e211]: updated
|
|
||||||
- generic [ref=e212]:
|
|
||||||
- generic [ref=e213]: 4/9/2026
|
|
||||||
- generic [ref=e214]:
|
|
||||||
- button "Mark as favorite" [ref=e215] [cursor=pointer]:
|
|
||||||
- img [ref=e216]
|
|
||||||
- button [ref=e220] [cursor=pointer]:
|
|
||||||
- img [ref=e221]
|
|
||||||
- button "AI Assistant" [ref=e225] [cursor=pointer]:
|
|
||||||
- img [ref=e226]
|
|
||||||
- generic [ref=e233]:
|
|
||||||
- generic [ref=e234]:
|
|
||||||
- generic [ref=e235]:
|
|
||||||
- img [ref=e237]
|
|
||||||
- generic [ref=e244]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e245]
|
|
||||||
- paragraph [ref=e246]: Always here to help
|
|
||||||
- button [ref=e248] [cursor=pointer]:
|
|
||||||
- img [ref=e249]
|
|
||||||
- generic [ref=e253]:
|
|
||||||
- img [ref=e255]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- paragraph [ref=e263]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e265]: 01:30 PM
|
|
||||||
- generic [ref=e266]:
|
|
||||||
- generic [ref=e267]:
|
|
||||||
- textbox "Type your message..." [ref=e268]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e270]:
|
|
||||||
- button "longcat icon LongCat" [ref=e272] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e273]
|
|
||||||
- generic [ref=e274]: LongCat
|
|
||||||
- img [ref=e275]
|
|
||||||
- generic [ref=e277]:
|
|
||||||
- generic [ref=e278]: longcat
|
|
||||||
- link "AI settings" [ref=e279] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Bookmark" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- textbox "URL *"
|
|
||||||
- textbox "Title (optional)"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- text: Tags
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- textbox "Add tags..."
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Bookmark" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Bookmark" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "URL *"
|
|
||||||
- textbox "Title"
|
|
||||||
- textbox "Description"
|
|
||||||
- generic:
|
|
||||||
- text: Tags
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- textbox "Add tags..."
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add YouTube Video" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- text: YouTube URL
|
|
||||||
- textbox "https://www.youtube.com/watch?v=..."
|
|
||||||
- generic:
|
|
||||||
- text: Title (optional)
|
|
||||||
- textbox "Video title"
|
|
||||||
- generic:
|
|
||||||
- text: Description (optional)
|
|
||||||
- textbox "Video description"
|
|
||||||
- generic:
|
|
||||||
- text: Tags (comma-separated)
|
|
||||||
- textbox "tutorial, learning, tech"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Video" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- navigation [ref=e123]:
|
|
||||||
- link "Removed stuff" [ref=e124] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e125]:
|
|
||||||
- img [ref=e126]
|
|
||||||
- generic [ref=e129]: Removed stuff
|
|
||||||
- link "Settings" [ref=e131] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e132]:
|
|
||||||
- img [ref=e133]
|
|
||||||
- generic [ref=e136]: Settings
|
|
||||||
- button "Logout" [ref=e138] [cursor=pointer]:
|
|
||||||
- generic [ref=e139]:
|
|
||||||
- img [ref=e140]
|
|
||||||
- generic [ref=e144]: Logout
|
|
||||||
- generic [ref=e146]:
|
|
||||||
- generic [ref=e147]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- button [ref=e149] [cursor=pointer]:
|
|
||||||
- img [ref=e150]
|
|
||||||
- button "Quick search" [ref=e151] [cursor=pointer]:
|
|
||||||
- img [ref=e152]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- button "Import a document" [ref=e156] [cursor=pointer]:
|
|
||||||
- img [ref=e157]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e161] [cursor=pointer]:
|
|
||||||
- img [ref=e162]
|
|
||||||
- img [ref=e167]
|
|
||||||
- button "AU" [ref=e171] [cursor=pointer]:
|
|
||||||
- generic [ref=e172]: AU
|
|
||||||
- img [ref=e173]
|
|
||||||
- main [ref=e175]:
|
|
||||||
- generic [ref=e177]:
|
|
||||||
- generic [ref=e178]:
|
|
||||||
- heading "Browser Extension Settings" [level=1] [ref=e179]
|
|
||||||
- paragraph [ref=e180]: Manage API keys and browser extensions for secure access to your Trackeep account.
|
|
||||||
- navigation [ref=e182]:
|
|
||||||
- button "Overview" [ref=e183] [cursor=pointer]:
|
|
||||||
- img [ref=e184]
|
|
||||||
- text: Overview
|
|
||||||
- button "API Keys" [ref=e187] [cursor=pointer]:
|
|
||||||
- img [ref=e188]
|
|
||||||
- text: API Keys
|
|
||||||
- button "Extensions" [ref=e192] [cursor=pointer]:
|
|
||||||
- img [ref=e193]
|
|
||||||
- text: Extensions
|
|
||||||
- button "Examples" [ref=e198] [cursor=pointer]:
|
|
||||||
- img [ref=e199]
|
|
||||||
- text: Examples
|
|
||||||
- generic [ref=e202]:
|
|
||||||
- heading "API Keys" [level=2] [ref=e203]
|
|
||||||
- button "Generate New Key" [ref=e204] [cursor=pointer]
|
|
||||||
- generic [ref=e206]:
|
|
||||||
- heading "Registered Extensions" [level=2] [ref=e207]
|
|
||||||
- paragraph [ref=e208]: Manage browser extensions that have access to your account.
|
|
||||||
- button "AI Assistant" [ref=e209] [cursor=pointer]:
|
|
||||||
- img [ref=e210]
|
|
||||||
- generic [ref=e217]:
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- img [ref=e221]
|
|
||||||
- generic [ref=e228]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e229]
|
|
||||||
- paragraph [ref=e230]: Always here to help
|
|
||||||
- button [ref=e232] [cursor=pointer]:
|
|
||||||
- img [ref=e233]
|
|
||||||
- generic [ref=e237]:
|
|
||||||
- img [ref=e239]
|
|
||||||
- generic [ref=e246]:
|
|
||||||
- paragraph [ref=e247]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e249]: 01:30 PM
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- textbox "Type your message..." [ref=e252]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e254]:
|
|
||||||
- button "longcat icon LongCat" [ref=e256] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e257]
|
|
||||||
- generic [ref=e258]: LongCat
|
|
||||||
- img [ref=e259]
|
|
||||||
- generic [ref=e261]:
|
|
||||||
- generic [ref=e262]: longcat
|
|
||||||
- link "AI settings" [ref=e263] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
# Multi-stage build for unified Trackeep image
|
||||||
|
# Builds both frontend and backend in one package
|
||||||
|
|
||||||
|
# Stage 1: Build Frontend
|
||||||
|
FROM node:22-alpine AS frontend-builder
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package*.json ./
|
||||||
|
RUN npm ci --only=production
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Build Backend
|
||||||
|
FROM golang:1.25-alpine AS backend-builder
|
||||||
|
WORKDIR /app/backend
|
||||||
|
COPY backend/go.mod backend/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY backend/ ./
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
|
||||||
|
|
||||||
|
# Stage 3: Final unified image
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apk --no-cache add ca-certificates tzdata nginx
|
||||||
|
|
||||||
|
# Copy backend binary and migrations
|
||||||
|
COPY --from=backend-builder /app/backend/main /app/main
|
||||||
|
COPY --from=backend-builder /app/backend/migrations /app/migrations
|
||||||
|
|
||||||
|
# Copy frontend build
|
||||||
|
COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy branding assets
|
||||||
|
COPY trackeep.svg /usr/share/nginx/html/
|
||||||
|
COPY trackeepfavi.png /usr/share/nginx/html/
|
||||||
|
COPY trackeepfavi_bg.png /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Copy nginx configuration
|
||||||
|
COPY frontend/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
RUN mkdir -p /app/uploads /data /var/log/nginx
|
||||||
|
|
||||||
|
# Expose single port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
|
# Start script to run both backend and nginx
|
||||||
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
@@ -33,6 +33,22 @@
|
|||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### One-Command Deployment (GHCR Image - Recommended for Casa OS)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name trackeep \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e DB_PASSWORD=your_password \
|
||||||
|
-e DB_USER=trackeep \
|
||||||
|
-e DB_NAME=trackeep \
|
||||||
|
-e DRAGONFLY_PASSWORD=your_dragonfly_password \
|
||||||
|
-e JWT_SECRET=your_jwt_secret \
|
||||||
|
ghcr.io/dvorinka/trackeep:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: This requires external PostgreSQL and DragonflyDB services. For full deployment with included databases, use Docker Compose below.
|
||||||
|
|
||||||
### Production Deployment with Docker Compose
|
### Production Deployment with Docker Compose
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -40,96 +56,50 @@ git clone https://github.com/dvorinka/trackeep.git
|
|||||||
cd trackeep
|
cd trackeep
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env file with your configuration
|
# Edit .env file with your configuration
|
||||||
docker-compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The `docker-compose.prod.yml` file uses environment variables with sensible defaults:
|
The setup uses a unified Docker image with frontend and backend in a single container.
|
||||||
|
|
||||||
|
**Complete docker-compose.yml**:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
icon: https://github.com/Dvorinka/Trackeep/raw/main/trackeepfavi_bg.png
|
||||||
trackeep-frontend:
|
|
||||||
image: 'ghcr.io/dvorinka/trackeep/frontend:latest'
|
|
||||||
ports:
|
|
||||||
- "${FRONTEND_PORT:-80}:${FRONTEND_PORT:-80}"
|
|
||||||
- "${HTTPS_PORT:-443}:443"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:8080}
|
|
||||||
- FRONTEND_PORT=${FRONTEND_PORT:-80}
|
|
||||||
- BACKEND_PORT=${BACKEND_PORT:-8080}
|
|
||||||
depends_on:
|
|
||||||
- trackeep-backend
|
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
|
||||||
- trackeep-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pgrep nginx > /dev/null || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 20s
|
|
||||||
|
|
||||||
trackeep-backend:
|
services:
|
||||||
image: 'ghcr.io/dvorinka/trackeep/backend:latest'
|
trackeep:
|
||||||
|
image: ghcr.io/dvorinka/trackeep:latest
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
|
- "${HOST_PORT:-8080}:8080"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- BACKEND_PORT=${BACKEND_PORT:-8080}
|
- BACKEND_PORT=8080
|
||||||
- FRONTEND_PORT=${FRONTEND_PORT:-80}
|
- DB_HOST=postgres
|
||||||
- GIN_MODE=${GIN_MODE:-release}
|
- DB_PORT=5432
|
||||||
- DB_TYPE=${DB_TYPE:-postgres}
|
- DRAGONFLY_ADDR=dragonfly:6379
|
||||||
- DB_HOST=${DB_HOST:-postgres}
|
- GIN_MODE=release
|
||||||
- DB_PORT=${DB_PORT:-5432}
|
|
||||||
- DB_USER=${DB_USER:-trackeep}
|
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
|
||||||
- DB_NAME=${DB_NAME:-trackeep}
|
|
||||||
- DB_SSL_MODE=${DB_SSL_MODE:-disable}
|
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
|
||||||
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-24h}
|
|
||||||
- UPLOAD_DIR=${UPLOAD_DIR:-./uploads}
|
|
||||||
- MAX_FILE_SIZE=${MAX_FILE_SIZE:-10485760}
|
|
||||||
- 'CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-*}'
|
|
||||||
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
|
||||||
- AUTO_UPDATE_CHECK=${AUTO_UPDATE_CHECK:-false}
|
|
||||||
- UPDATE_CHECK_INTERVAL=${UPDATE_CHECK_INTERVAL:-24h}
|
|
||||||
- PRERELEASE_UPDATES=${PRERELEASE_UPDATES:-false}
|
|
||||||
- DRAGONFLY_ADDR=${DRAGONFLY_ADDR:-dragonfly:6379}
|
|
||||||
- DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
|
|
||||||
volumes:
|
volumes:
|
||||||
- './data:/data'
|
- ./uploads:/app/uploads
|
||||||
- './uploads:/app/uploads'
|
- ./data:/data
|
||||||
- './logs:/app/logs'
|
|
||||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
depends_on:
|
||||||
- trackeep-network
|
postgres:
|
||||||
healthcheck:
|
condition: service_healthy
|
||||||
test:
|
dragonfly:
|
||||||
- CMD
|
condition: service_healthy
|
||||||
- wget
|
|
||||||
- '--no-verbose'
|
|
||||||
- '--tries=1'
|
|
||||||
- '--spider'
|
|
||||||
- "http://localhost:${BACKEND_PORT:-8080}/health"
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: 'postgres:15-alpine'
|
image: postgres:15-alpine
|
||||||
ports:
|
|
||||||
- "${DB_PORT:-5432}:5432"
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${DB_NAME:-trackeep}
|
POSTGRES_DB: ${DB_NAME:-trackeep}
|
||||||
POSTGRES_USER: ${DB_USER:-trackeep}
|
POSTGRES_USER: ${DB_USER:-trackeep}
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "${DB_HOST_PORT:-5433}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- 'postgres_data:/var/lib/postgres/data'
|
- postgres_data:/var/lib/postgresql/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- trackeep-network
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-trackeep} -d ${DB_NAME:-trackeep}"]
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-trackeep} -d ${DB_NAME:-trackeep}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -139,17 +109,14 @@ services:
|
|||||||
|
|
||||||
dragonfly:
|
dragonfly:
|
||||||
image: ghcr.io/dragonflydb/dragonfly:latest
|
image: ghcr.io/dragonflydb/dragonfly:latest
|
||||||
container_name: dragonfly
|
|
||||||
ports:
|
ports:
|
||||||
- "${DRAGONFLY_PORT:-6379}:6379"
|
- "${DRAGONFLY_HOST_PORT:-6380}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- dragonfly_data:/data
|
- dragonfly_data:/data
|
||||||
command: dragonfly --requirepass=${DRAGONFLY_PASSWORD} --proactor_threads=2
|
command: dragonfly --requirepass=${DRAGONFLY_PASSWORD} --proactor_threads=2
|
||||||
environment:
|
environment:
|
||||||
- DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
|
- DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- trackeep-network
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "redis-cli -a ${DRAGONFLY_PASSWORD} ping"]
|
test: ["CMD-SHELL", "redis-cli -a ${DRAGONFLY_PASSWORD} ping"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -158,40 +125,31 @@ services:
|
|||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data: null
|
postgres_data:
|
||||||
dragonfly_data: null
|
dragonfly_data:
|
||||||
|
|
||||||
networks:
|
|
||||||
trackeep-network:
|
|
||||||
driver: bridge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Service Architecture
|
### Service Architecture
|
||||||
|
|
||||||
Trackeep production deployment consists of **4 essential services**:
|
Trackeep deployment consists of **3 services**:
|
||||||
|
|
||||||
#### **🎯 Frontend Service**
|
#### **🎯 Trackeep Service (Unified)**
|
||||||
- **Image**: `ghcr.io/dvorinka/trackeep/frontend:latest`
|
- **Image**: Built from unified Dockerfile (frontend + backend in one)
|
||||||
- **Ports**: `${FRONTEND_PORT:-80}:${FRONTEND_PORT:-80}`, `${HTTPS_PORT:-443}:443`
|
- **Ports**: `${HOST_PORT:-8080}:8080`
|
||||||
- **Purpose**: Web interface and user experience
|
- **Purpose**: Web interface, API server, and business logic combined
|
||||||
- **Health**: nginx process check
|
|
||||||
|
|
||||||
#### **🔧 Backend Service**
|
|
||||||
- **Image**: `ghcr.io/dvorinka/trackeep/backend:latest`
|
|
||||||
- **Ports**: `${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}`
|
|
||||||
- **Purpose**: API server and business logic
|
|
||||||
- **Health**: HTTP health check endpoint
|
- **Health**: HTTP health check endpoint
|
||||||
|
- **Auto-configuration**: Frontend automatically connects to backend via nginx proxy
|
||||||
|
|
||||||
#### **🗄️ Database Service**
|
#### **🗄️ Database Service**
|
||||||
- **Image**: `postgres:15-alpine`
|
- **Image**: `postgres:15-alpine`
|
||||||
- **Ports**: `${DB_PORT:-5432}:5432`
|
- **Ports**: `${DB_HOST_PORT:-5433}:5432`
|
||||||
- **Purpose**: Data persistence and storage
|
- **Purpose**: Data persistence and storage
|
||||||
- **Health**: PostgreSQL readiness check
|
- **Health**: PostgreSQL readiness check
|
||||||
- **Storage**: Persistent volume for data
|
- **Storage**: Persistent volume for data
|
||||||
|
|
||||||
#### **🐉 DragonflyDB Service**
|
#### **🐉 DragonflyDB Service**
|
||||||
- **Image**: `ghcr.io/dragonflydb/dragonfly:latest`
|
- **Image**: `ghcr.io/dragonflydb/dragonfly:latest`
|
||||||
- **Ports**: `${DRAGONFLY_PORT:-6379}:6379`
|
- **Ports**: `${DRAGONFLY_HOST_PORT:-6380}:6379`
|
||||||
- **Purpose**: In-memory caching and session storage
|
- **Purpose**: In-memory caching and session storage
|
||||||
- **Health**: Redis-cli ping check
|
- **Health**: Redis-cli ping check
|
||||||
- **Storage**: Persistent volume for cache data
|
- **Storage**: Persistent volume for cache data
|
||||||
@@ -201,16 +159,23 @@ Trackeep production deployment consists of **4 essential services**:
|
|||||||
Create a `.env` file from the provided `.env.example` and configure these required variables:
|
Create a `.env` file from the provided `.env.example` and configure these required variables:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# Database Configuration
|
# Host port for the application (default: 8080)
|
||||||
DB_PASSWORD=your_secure_password
|
HOST_PORT=8080
|
||||||
|
|
||||||
# Security Configuration
|
# Database Configuration
|
||||||
JWT_SECRET=your_jwt_secret_key
|
DB_PASSWORD=your_secure_password_here
|
||||||
|
DB_USER=trackeep
|
||||||
|
DB_NAME=trackeep
|
||||||
|
|
||||||
# DragonflyDB Configuration
|
# DragonflyDB Configuration
|
||||||
DRAGONFLY_PASSWORD=your_dragonfly_password
|
DRAGONFLY_PASSWORD=your_dragonfly_password_here
|
||||||
|
|
||||||
|
# JWT Secret (generate with: openssl rand -hex 32)
|
||||||
|
JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: The frontend automatically connects to the backend via nginx proxy - no VITE_API_URL or additional configuration needed.
|
||||||
|
|
||||||
### AI Services Configuration
|
### AI Services Configuration
|
||||||
|
|
||||||
AI services are now configured **only within the Trackeep application**. No environment variables are needed for AI configuration. Simply:
|
AI services are now configured **only within the Trackeep application**. No environment variables are needed for AI configuration. Simply:
|
||||||
@@ -490,9 +455,9 @@ DISABLE_CHINESE_AI=true
|
|||||||
```
|
```
|
||||||
|
|
||||||
4. **Access the application**
|
4. **Access the application**
|
||||||
- Frontend: http://localhost:${FRONTEND_PORT:-80}
|
- Application: http://localhost:${HOST_PORT:-8080}
|
||||||
- Backend API: http://localhost:${BACKEND_PORT:-8080}
|
- Health Check: http://localhost:${HOST_PORT:-8080}/health
|
||||||
- Health Check: http://localhost:${BACKEND_PORT:-8080}/health
|
- API: http://localhost:${HOST_PORT:-8080}/api/
|
||||||
|
|
||||||
### Demo Login
|
### Demo Login
|
||||||
- Email: `demo@trackeep.com`
|
- Email: `demo@trackeep.com`
|
||||||
@@ -516,8 +481,8 @@ trackeep/
|
|||||||
├── scripts/ # Utility scripts
|
├── scripts/ # Utility scripts
|
||||||
├── data/ # Data storage directory
|
├── data/ # Data storage directory
|
||||||
├── uploads/ # File upload directory
|
├── uploads/ # File upload directory
|
||||||
├── docker-compose.yml # Multi-service orchestration
|
├── docker-compose.yml # Unified service orchestration
|
||||||
├── docker-compose.prod.yml # Production configuration
|
├── Dockerfile # Unified frontend + backend build
|
||||||
├── start.sh # Startup script
|
├── start.sh # Startup script
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -562,47 +527,23 @@ Additional documentation files:
|
|||||||
Key environment variables to configure:
|
Key environment variables to configure:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Server Configuration
|
# Host port for the application
|
||||||
FRONTEND_PORT=80
|
HOST_PORT=8080
|
||||||
BACKEND_PORT=8080
|
|
||||||
VITE_API_URL=http://localhost:8080
|
|
||||||
GIN_MODE=release
|
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
DB_TYPE=postgres
|
DB_PASSWORD=your_secure_password_here
|
||||||
DB_HOST=postgres
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=trackeep
|
DB_USER=trackeep
|
||||||
DB_PASSWORD=your_password_here
|
|
||||||
DB_NAME=trackeep
|
DB_NAME=trackeep
|
||||||
DB_SSL_MODE=disable
|
|
||||||
|
|
||||||
# DragonflyDB Configuration
|
# DragonflyDB Configuration
|
||||||
DRAGONFLY_ADDR=dragonfly:6379
|
DRAGONFLY_PASSWORD=your_dragonfly_password_here
|
||||||
DRAGONFLY_PORT=6379
|
|
||||||
DRAGONFLY_PASSWORD=your_dragonfly_password
|
|
||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration (generate with: openssl rand -hex 32)
|
||||||
# Generate a secure 64-character hex string using: openssl rand -hex 32
|
|
||||||
JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
|
JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
|
||||||
JWT_EXPIRES_IN=24h
|
|
||||||
|
|
||||||
# File Upload Configuration
|
|
||||||
UPLOAD_DIR=./uploads
|
|
||||||
MAX_FILE_SIZE=10485760
|
|
||||||
|
|
||||||
# CORS Configuration
|
|
||||||
CORS_ALLOWED_ORIGINS=*
|
|
||||||
|
|
||||||
# Demo Mode Configuration
|
|
||||||
VITE_DEMO_MODE=false
|
|
||||||
|
|
||||||
# Auto Update Configuration
|
|
||||||
AUTO_UPDATE_CHECK=false
|
|
||||||
UPDATE_CHECK_INTERVAL=24h
|
|
||||||
PRERELEASE_UPDATES=false
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: All other configuration has sensible defaults. The frontend automatically connects to the backend via nginx proxy - no additional API URL configuration needed.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Building Trackeep as a solo developer has been an incredible journey, but it's always better when we build together! Whether you're fixing a typo, adding a feature, or just sharing ideas – your contribution matters.
|
Building Trackeep as a solo developer has been an incredible journey, but it's always better when we build together! Whether you're fixing a typo, adding a feature, or just sharing ideas – your contribution matters.
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ func Load() *Config {
|
|||||||
return &Config{
|
return &Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: getEnvWithDefault("PORT", getEnvWithDefault("BACKEND_PORT", "8080")),
|
Port: getEnvWithDefault("PORT", getEnvWithDefault("BACKEND_PORT", "8080")),
|
||||||
ReadTimeout: getDurationEnv("READ_TIMEOUT", 15*time.Second),
|
ReadTimeout: GetDurationEnv("READ_TIMEOUT", 15*time.Second),
|
||||||
WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 15*time.Second),
|
WriteTimeout: GetDurationEnv("WRITE_TIMEOUT", 15*time.Second),
|
||||||
IdleTimeout: getDurationEnv("IDLE_TIMEOUT", 60*time.Second),
|
IdleTimeout: GetDurationEnv("IDLE_TIMEOUT", 60*time.Second),
|
||||||
ShutdownTimeout: getDurationEnv("SHUTDOWN_TIMEOUT", 30*time.Second),
|
ShutdownTimeout: GetDurationEnv("SHUTDOWN_TIMEOUT", 30*time.Second),
|
||||||
},
|
},
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
Host: getEnvWithDefault("DB_HOST", "localhost"),
|
Host: getEnvWithDefault("DB_HOST", "localhost"),
|
||||||
@@ -99,7 +99,7 @@ func getEnvWithDefault(key, defaultValue string) string {
|
|||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDurationEnv(key string, defaultValue time.Duration) time.Duration {
|
func GetDurationEnv(key string, defaultValue time.Duration) time.Duration {
|
||||||
value := os.Getenv(key)
|
value := os.Getenv(key)
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -68,25 +67,6 @@ type Claims struct {
|
|||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDurationEnv parses duration from environment variable with fallback
|
|
||||||
func getDurationEnv(key string, defaultValue time.Duration) time.Duration {
|
|
||||||
value := os.Getenv(key)
|
|
||||||
if value == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
seconds, err := strconv.Atoi(value)
|
|
||||||
if err != nil {
|
|
||||||
duration, err := time.ParseDuration(value)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return duration
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Duration(seconds) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateJWT creates a new JWT token for a user
|
// GenerateJWT creates a new JWT token for a user
|
||||||
func GenerateJWT(user models.User) (string, error) {
|
func GenerateJWT(user models.User) (string, error) {
|
||||||
return generateJWT(user)
|
return generateJWT(user)
|
||||||
@@ -103,7 +83,7 @@ func generateJWT(user models.User) (string, error) {
|
|||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
GitHubID: user.GitHubID,
|
GitHubID: user.GitHubID,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(getDurationEnv("JWT_EXPIRES_IN", 24*time.Hour))),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.GetDurationEnv("JWT_EXPIRES_IN", 24*time.Hour))),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
Issuer: "trackeep",
|
Issuer: "trackeep",
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
@@ -1,52 +0,0 @@
|
|||||||
services:
|
|
||||||
trackeep-backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
|
||||||
- "${BACKEND_HOST_PORT:-9000}:${BACKEND_PORT:-8080}"
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- APP_VERSION=${APP_VERSION:-1.0.0}
|
|
||||||
- BACKEND_PORT=${BACKEND_PORT:-8080}
|
|
||||||
- FRONTEND_PORT=${FRONTEND_PORT:-3000}
|
|
||||||
- VITE_DEMO_MODE=true
|
|
||||||
volumes:
|
|
||||||
- ./data:/data
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # Docker socket for updates
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${BACKEND_PORT:-8080}/health || wget --no-verbose --tries=1 --spider http://localhost:${BACKEND_PORT:-8080}/live"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
trackeep-frontend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./frontend/Dockerfile
|
|
||||||
args:
|
|
||||||
- VITE_DEMO_MODE=true
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:${BACKEND_HOST_PORT:-9000}}
|
|
||||||
ports:
|
|
||||||
- "${FRONTEND_HOST_PORT:-3900}:${FRONTEND_PORT:-3000}"
|
|
||||||
environment:
|
|
||||||
- VITE_APP_VERSION=${APP_VERSION:-1.0.0}
|
|
||||||
- VITE_DEMO_MODE=true
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:${BACKEND_HOST_PORT:-9000}}
|
|
||||||
- FRONTEND_PORT=${FRONTEND_PORT:-3000}
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # Docker socket for updates
|
|
||||||
depends_on:
|
|
||||||
trackeep-backend:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pgrep nginx > /dev/null || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 20s
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: trackeep-postgres
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: ${DB_NAME:-trackeep}
|
|
||||||
POSTGRES_USER: ${DB_USER:-trackeep}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
|
||||||
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=en_US.UTF-8"
|
|
||||||
ports:
|
|
||||||
- "${DB_HOST_PORT:-5433}:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
- ./backups:/backups
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-trackeep} -d ${DB_NAME:-trackeep}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 30s
|
|
||||||
networks:
|
|
||||||
- trackeep-network
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2'
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: '0.5'
|
|
||||||
memory: 512M
|
|
||||||
|
|
||||||
dragonfly:
|
|
||||||
image: ghcr.io/dragonflydb/dragonfly:latest
|
|
||||||
container_name: trackeep-dragonfly
|
|
||||||
ports:
|
|
||||||
- "${DRAGONFLY_HOST_PORT:-6380}:6379"
|
|
||||||
volumes:
|
|
||||||
- dragonfly_data:/data
|
|
||||||
command: >
|
|
||||||
dragonfly
|
|
||||||
--requirepass=${DRAGONFLY_PASSWORD:?DRAGONFLY_PASSWORD is required}
|
|
||||||
--proactor_threads=4
|
|
||||||
--maxmemory=2gb
|
|
||||||
--maxmemory-policy=allkeys-lru
|
|
||||||
--save_schedule=*:30
|
|
||||||
--dir=/data
|
|
||||||
environment:
|
|
||||||
- DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "redis-cli -a ${DRAGONFLY_PASSWORD} ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 30s
|
|
||||||
networks:
|
|
||||||
- trackeep-network
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1'
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: '0.25'
|
|
||||||
memory: 512M
|
|
||||||
|
|
||||||
trackeep-backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- GO_VERSION=1.22
|
|
||||||
container_name: trackeep-backend
|
|
||||||
ports:
|
|
||||||
- "${BACKEND_HOST_PORT:-8080}:${BACKEND_PORT:-8080}"
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- APP_VERSION=${APP_VERSION:-1.2.5}
|
|
||||||
- BACKEND_PORT=${BACKEND_PORT:-8080}
|
|
||||||
- FRONTEND_PORT=${FRONTEND_PORT:-80}
|
|
||||||
- GIN_MODE=release
|
|
||||||
- DB_HOST=postgres
|
|
||||||
- DRAGONFLY_ADDR=dragonfly:6379
|
|
||||||
volumes:
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
- ./logs:/app/logs
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
dragonfly:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:${BACKEND_PORT:-8080}/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
networks:
|
|
||||||
- trackeep-network
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '2'
|
|
||||||
memory: 1G
|
|
||||||
reservations:
|
|
||||||
cpus: '0.5'
|
|
||||||
memory: 256M
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
trackeep-frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
- NODE_VERSION=20
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:${BACKEND_HOST_PORT:-8080}}
|
|
||||||
container_name: trackeep-frontend
|
|
||||||
ports:
|
|
||||||
- "${FRONTEND_HOST_PORT:-80}:80"
|
|
||||||
environment:
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:${BACKEND_HOST_PORT:-8080}}
|
|
||||||
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
|
||||||
depends_on:
|
|
||||||
- trackeep-backend
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
networks:
|
|
||||||
- trackeep-network
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: '1'
|
|
||||||
memory: 512M
|
|
||||||
reservations:
|
|
||||||
cpus: '0.25'
|
|
||||||
memory: 128M
|
|
||||||
logging:
|
|
||||||
driver: "json-file"
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "3"
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
dragonfly_data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
trackeep-network:
|
|
||||||
driver: bridge
|
|
||||||
+13
-47
@@ -1,14 +1,16 @@
|
|||||||
|
icon: https://github.com/Dvorinka/Trackeep/raw/main/trackeepfavi_bg.png
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${DB_NAME:-trackeep}
|
POSTGRES_DB: ${DB_NAME:-trackeep}
|
||||||
POSTGRES_USER: ${DB_USER:-trackeep}
|
POSTGRES_USER: ${DB_USER:-trackeep}
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- "${DB_HOST_PORT:-5433}:5432"
|
- "${DB_HOST_PORT:-5433}:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgres/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-trackeep} -d ${DB_NAME:-trackeep}"]
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-trackeep} -d ${DB_NAME:-trackeep}"]
|
||||||
@@ -19,7 +21,6 @@ services:
|
|||||||
|
|
||||||
dragonfly:
|
dragonfly:
|
||||||
image: ghcr.io/dragonflydb/dragonfly:latest
|
image: ghcr.io/dragonflydb/dragonfly:latest
|
||||||
container_name: dragonfly
|
|
||||||
ports:
|
ports:
|
||||||
- "${DRAGONFLY_HOST_PORT:-6380}:6379"
|
- "${DRAGONFLY_HOST_PORT:-6380}:6379"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -35,64 +36,29 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 30s
|
start_period: 30s
|
||||||
|
|
||||||
trackeep-backend:
|
trackeep:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_HOST_PORT:-9000}:${BACKEND_PORT:-8080}"
|
- "${HOST_PORT:-8080}:8080"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- APP_VERSION=${APP_VERSION:-1.0.0}
|
- BACKEND_PORT=8080
|
||||||
- BACKEND_PORT=${BACKEND_PORT:-8080}
|
- DB_HOST=postgres
|
||||||
- FRONTEND_PORT=${FRONTEND_PORT:-3000}
|
- DB_PORT=5432
|
||||||
- DRAGONFLY_ADDR=${DRAGONFLY_ADDR:-dragonfly:6379}
|
- DRAGONFLY_ADDR=dragonfly:6379
|
||||||
- DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
|
- GIN_MODE=release
|
||||||
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
|
||||||
- ./uploads:/app/uploads
|
- ./uploads:/app/uploads
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # Docker socket for updates
|
- ./data:/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
dragonfly:
|
dragonfly:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${BACKEND_PORT:-8080}/health || wget --no-verbose --tries=1 --spider http://localhost:${BACKEND_PORT:-8080}/live"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
trackeep-frontend:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./frontend/Dockerfile
|
|
||||||
args:
|
|
||||||
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:${BACKEND_HOST_PORT:-9000}}
|
|
||||||
ports:
|
|
||||||
- "${FRONTEND_HOST_PORT:-3900}:${FRONTEND_PORT:-3000}"
|
|
||||||
environment:
|
|
||||||
- VITE_APP_VERSION=${APP_VERSION:-1.0.0}
|
|
||||||
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:${BACKEND_HOST_PORT:-9000}}
|
|
||||||
- FRONTEND_PORT=${FRONTEND_PORT:-3000}
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock # Docker socket for updates
|
|
||||||
depends_on:
|
|
||||||
trackeep-backend:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pgrep nginx > /dev/null || exit 1"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 20s
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Unified entrypoint for Trackeep
|
||||||
|
# Starts both backend and nginx in one container
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Backend configuration
|
||||||
|
export BACKEND_PORT=${BACKEND_PORT:-8080}
|
||||||
|
export DB_HOST=${DB_HOST:-postgres}
|
||||||
|
export DB_PORT=${DB_PORT:-5432}
|
||||||
|
export DB_NAME=${DB_NAME:-trackeep}
|
||||||
|
export DB_USER=${DB_USER:-trackeep}
|
||||||
|
export DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
export DRAGONFLY_ADDR=${DRAGONFLY_ADDR:-dragonfly:6379}
|
||||||
|
export DRAGONFLY_PASSWORD=${DRAGONFLY_PASSWORD}
|
||||||
|
export JWT_SECRET=${JWT_SECRET}
|
||||||
|
export GIN_MODE=${GIN_MODE:-release}
|
||||||
|
|
||||||
|
# Start backend in background
|
||||||
|
cd /app
|
||||||
|
echo "Starting Trackeep backend on port ${BACKEND_PORT}..."
|
||||||
|
./main &
|
||||||
|
|
||||||
|
# Wait for backend to be ready
|
||||||
|
echo "Waiting for backend to be ready..."
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if wget --no-verbose --tries=1 --spider http://localhost:${BACKEND_PORT}/health 2>/dev/null; then
|
||||||
|
echo "Backend is ready!"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Waiting... ($i/30)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Update nginx config to proxy to localhost backend
|
||||||
|
sed -i "s|http://trackeep-backend:8080/|http://localhost:${BACKEND_PORT}/|g" /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
echo "Starting nginx..."
|
||||||
|
nginx -g "daemon off;"
|
||||||
+4
-4
@@ -55,9 +55,9 @@ http {
|
|||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# API proxy to backend with retry logic
|
# API proxy to backend (internal localhost)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://trackeep-backend:8080/;
|
proxy_pass http://localhost:8080/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
@@ -66,7 +66,7 @@ http {
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
# Retry configuration
|
# Retry configuration
|
||||||
proxy_connect_timeout 5s;
|
proxy_connect_timeout 5s;
|
||||||
proxy_send_timeout 10s;
|
proxy_send_timeout 10s;
|
||||||
@@ -74,7 +74,7 @@ http {
|
|||||||
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
|
||||||
proxy_next_upstream_tries 3;
|
proxy_next_upstream_tries 3;
|
||||||
proxy_next_upstream_timeout 30s;
|
proxy_next_upstream_timeout 30s;
|
||||||
|
|
||||||
# Buffer settings
|
# Buffer settings
|
||||||
proxy_buffering on;
|
proxy_buffering on;
|
||||||
proxy_buffer_size 4k;
|
proxy_buffer_size 4k;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
+26
-26
@@ -3,33 +3,33 @@ import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'
|
|||||||
import { Layout } from '@/components/layout/Layout'
|
import { Layout } from '@/components/layout/Layout'
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { ToastContainer } from '@/components/ui/Toast'
|
import { ToastContainer } from '@/components/ui/Toast'
|
||||||
import { Dashboard } from '@/pages/Dashboard'
|
import { Dashboard } from '@/pages/misc/Dashboard'
|
||||||
import { Bookmarks } from '@/pages/Bookmarks'
|
import { Bookmarks } from '@/pages/content/Bookmarks'
|
||||||
import { Tasks } from '@/pages/Tasks'
|
import { Tasks } from '@/pages/productivity/Tasks'
|
||||||
import { Files } from '@/pages/Files'
|
import { Files } from '@/pages/content/Files'
|
||||||
import { Notes } from '@/pages/Notes'
|
import { Notes } from '@/pages/content/Notes'
|
||||||
import Chat from '@/pages/Chat'
|
import Chat from '@/pages/communication/Chat'
|
||||||
import { Settings } from '@/pages/Settings'
|
import { Settings } from '@/pages/settings/Settings'
|
||||||
import { Login } from '@/pages/Login'
|
import { Login } from '@/pages/auth/Login'
|
||||||
import { Youtube } from '@/pages/Youtube'
|
import { Youtube } from '@/pages/content/Youtube'
|
||||||
import { Members } from '@/pages/Members'
|
import { Members } from '@/pages/admin/Members'
|
||||||
import { RemovedStuff } from '@/pages/RemovedStuff'
|
import { RemovedStuff } from '@/pages/misc/RemovedStuff'
|
||||||
import { AdminSettings } from '@/pages/AdminSettings'
|
import { AdminSettings } from '@/pages/admin/AdminSettings'
|
||||||
import { ColorSwitcher } from '@/pages/ColorSwitcher'
|
import { ColorSwitcher } from '@/pages/settings/ColorSwitcher'
|
||||||
import { AdminDashboard } from '@/pages/AdminDashboard'
|
import { AdminDashboard } from '@/pages/admin/AdminDashboard'
|
||||||
import { Stats } from '@/pages/Stats'
|
import { Stats } from '@/pages/productivity/Stats'
|
||||||
import { Profile } from '@/pages/Profile'
|
import { Profile } from '@/pages/auth/Profile'
|
||||||
import { LearningPaths } from '@/pages/LearningPaths'
|
import { LearningPaths } from '@/pages/content/LearningPaths'
|
||||||
import { GitHub } from '@/pages/GitHub'
|
import { GitHub } from '@/pages/content/GitHub'
|
||||||
import { TimeTracking } from '@/pages/TimeTracking'
|
import { TimeTracking } from '@/pages/productivity/TimeTracking'
|
||||||
import { Calendar } from '@/pages/Calendar'
|
import { Calendar } from '@/pages/productivity/Calendar'
|
||||||
import { AuthCallback } from '@/pages/AuthCallback'
|
import { AuthCallback } from '@/pages/auth/AuthCallback'
|
||||||
import { AuthProvider, useAuth } from '@/lib/auth'
|
import { AuthProvider, useAuth } from '@/lib/auth'
|
||||||
import { Search } from '@/pages/Search'
|
import { Search } from '@/pages/content/Search'
|
||||||
import { Analytics } from '@/pages/Analytics'
|
import { Analytics } from '@/pages/admin/Analytics'
|
||||||
import { Messages } from '@/pages/Messages'
|
import { Messages } from '@/pages/communication/Messages'
|
||||||
import { ShareTarget } from '@/pages/ShareTarget'
|
import { ShareTarget } from '@/pages/misc/ShareTarget'
|
||||||
import BrowserExtensionSettings from '@/pages/BrowserExtensionSettings'
|
import BrowserExtensionSettings from '@/pages/settings/BrowserExtensionSettings'
|
||||||
import { initializeDemoMode, clearDemoMode, isEnvDemoMode } from '@/lib/demo-mode'
|
import { initializeDemoMode, clearDemoMode, isEnvDemoMode } from '@/lib/demo-mode'
|
||||||
import { onMount, createEffect } from 'solid-js'
|
import { onMount, createEffect } from 'solid-js'
|
||||||
import { useNavigate } from '@solidjs/router'
|
import { useNavigate } from '@solidjs/router'
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
import { createSignal, Show } from 'solid-js'
|
|
||||||
import { IconX, IconSend, IconUser, IconChevronDown } from '@tabler/icons-solidjs'
|
|
||||||
import longcatIcon from '@/assets/longcat-color.svg'
|
|
||||||
import { ModalPortal } from '@/components/ui/ModalPortal'
|
|
||||||
|
|
||||||
interface FloatingAIProps {
|
|
||||||
onToggleChat: () => void
|
|
||||||
isChatOpen: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Message {
|
|
||||||
id: string
|
|
||||||
role: 'user' | 'assistant'
|
|
||||||
content: string
|
|
||||||
timestamp: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FloatingAI(props: FloatingAIProps) {
|
|
||||||
const [messages, setMessages] = createSignal<Message[]>([
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Hello! I\'m your AI assistant. How can I help you today?',
|
|
||||||
timestamp: new Date()
|
|
||||||
}
|
|
||||||
])
|
|
||||||
const [inputValue, setInputValue] = createSignal('')
|
|
||||||
const [selectedModel, setSelectedModel] = createSignal('longcat-flash-chat')
|
|
||||||
const [showModelSelector, setShowModelSelector] = createSignal(false)
|
|
||||||
|
|
||||||
const aiModels = [
|
|
||||||
{ id: 'longcat-flash-chat', name: 'LongCat Flash', description: 'Fast and efficient' },
|
|
||||||
{ id: 'gpt-4', name: 'GPT-4', description: 'Most capable' },
|
|
||||||
{ id: 'claude-3', name: 'Claude 3', description: 'Balanced performance' }
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleSendMessage = () => {
|
|
||||||
const value = inputValue().trim()
|
|
||||||
if (!value) return
|
|
||||||
|
|
||||||
const userMessage: Message = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
role: 'user',
|
|
||||||
content: value,
|
|
||||||
timestamp: new Date()
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, userMessage])
|
|
||||||
setInputValue('')
|
|
||||||
|
|
||||||
// Simulate AI response
|
|
||||||
setTimeout(() => {
|
|
||||||
const aiMessage: Message = {
|
|
||||||
id: (Date.now() + 1).toString(),
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'I understand your question. Let me help you with that...',
|
|
||||||
timestamp: new Date()
|
|
||||||
}
|
|
||||||
setMessages(prev => [...prev, aiMessage])
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeyPress = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSendMessage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Floating AI Button */}
|
|
||||||
<button
|
|
||||||
onClick={props.onToggleChat}
|
|
||||||
class="fixed bottom-6 right-8 z-40 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-all duration-200 hover:scale-110 w-14 h-14"
|
|
||||||
title="AI Assistant"
|
|
||||||
>
|
|
||||||
<img src={longcatIcon} alt="AI Assistant" class="size-6" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* AI Chat Modal */}
|
|
||||||
<Show when={props.isChatOpen}>
|
|
||||||
<ModalPortal>
|
|
||||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div class="bg-card border border-border rounded-lg shadow-xl max-w-md w-full max-h-[600px] flex flex-col" style="width: 420px;">
|
|
||||||
{/* Header */}
|
|
||||||
<div class="flex items-center justify-between p-4 border-b border-border bg-gradient-to-r from-primary/10 to-primary/5">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<div class="flex items-center justify-center p-3 rounded-lg bg-primary/20">
|
|
||||||
<img src={longcatIcon} alt="AI Assistant" class="size-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-semibold text-foreground">AI Assistant</h3>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<p class="text-xs text-muted-foreground">Always here to help</p>
|
|
||||||
<div class="relative">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowModelSelector(!showModelSelector())}
|
|
||||||
class="flex items-center gap-1 text-xs text-primary hover:text-primary/80 transition-colors"
|
|
||||||
>
|
|
||||||
{aiModels.find(m => m.id === selectedModel())?.name || 'LongCat Flash'}
|
|
||||||
<IconChevronDown class="size-3" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Model Selector Dropdown */}
|
|
||||||
<Show when={showModelSelector()}>
|
|
||||||
<div class="absolute bottom-full left-0 mb-2 w-48 bg-popover border border-border rounded-md shadow-lg z-10">
|
|
||||||
{aiModels.map((model) => (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedModel(model.id)
|
|
||||||
setShowModelSelector(false)
|
|
||||||
}}
|
|
||||||
class="w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors first:rounded-t-md last:rounded-b-md"
|
|
||||||
>
|
|
||||||
<div class="font-medium">{model.name}</div>
|
|
||||||
<div class="text-xs text-muted-foreground">{model.description}</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={props.onToggleChat}
|
|
||||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-inherit hover:bg-accent/50 hover:text-accent-foreground h-8 w-8"
|
|
||||||
>
|
|
||||||
<IconX class="size-4 text-foreground" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages */}
|
|
||||||
<div class="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-background to-muted/20" style="max-height: 400px;">
|
|
||||||
{messages().map((message) => (
|
|
||||||
<div class={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'} animate-in slide-in-from-bottom-2 duration-200`}>
|
|
||||||
{message.role === 'assistant' && (
|
|
||||||
<div class="flex items-center justify-center p-2 rounded-lg bg-primary/10 flex-shrink-0">
|
|
||||||
<img src={longcatIcon} alt="AI Assistant" class="size-4" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div class={`max-w-[300px] rounded-lg p-3 shadow-sm ${
|
|
||||||
message.role === 'user'
|
|
||||||
? 'bg-primary text-primary-foreground ml-auto'
|
|
||||||
: 'bg-muted border border-border'
|
|
||||||
}`}>
|
|
||||||
<p class="text-sm leading-relaxed">{message.content}</p>
|
|
||||||
<p class="text-xs opacity-70 mt-2">
|
|
||||||
{message.timestamp.toLocaleTimeString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{message.role === 'user' && (
|
|
||||||
<div class="flex items-center justify-center p-2 rounded-lg bg-primary flex-shrink-0">
|
|
||||||
<IconUser class="size-4 text-primary-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input */}
|
|
||||||
<div class="p-4 border-t border-border bg-muted/30">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={inputValue()}
|
|
||||||
onInput={(e) => setInputValue(e.currentTarget.value)}
|
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
placeholder="Type your message..."
|
|
||||||
class="flex-1 h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleSendMessage}
|
|
||||||
disabled={!inputValue().trim()}
|
|
||||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 hover:shadow-md h-10 px-4"
|
|
||||||
>
|
|
||||||
<IconSend class="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalPortal>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -43,8 +43,6 @@ export const BrowserSearch = () => {
|
|||||||
try {
|
try {
|
||||||
const isDemoMode = isDemo();
|
const isDemoMode = isDemo();
|
||||||
|
|
||||||
console.log(`[BrowserSearch] Demo mode: ${isDemoMode}`);
|
|
||||||
|
|
||||||
// Always use backend API for search to avoid CORS issues
|
// Always use backend API for search to avoid CORS issues
|
||||||
const API_BASE_URL = getApiBaseUrl();
|
const API_BASE_URL = getApiBaseUrl();
|
||||||
const token = localStorage.getItem('token') ||
|
const token = localStorage.getItem('token') ||
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
import { createSignal, Show } from 'solid-js'
|
|
||||||
import { Button } from './Button'
|
|
||||||
import { IconDownload, IconUpload, IconFileText, IconAlertTriangle, IconCheck } from '@tabler/icons-solidjs'
|
|
||||||
import { exportData as exportDataUtil, importData as importDataUtil, validateImportData, getImportSummary, type ExportData } from '@/lib/export-import'
|
|
||||||
|
|
||||||
export interface ExportImportProps {
|
|
||||||
data?: {
|
|
||||||
bookmarks?: any[]
|
|
||||||
tasks?: any[]
|
|
||||||
notes?: any[]
|
|
||||||
files?: any[]
|
|
||||||
}
|
|
||||||
onImport?: (data: ExportData) => Promise<void>
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExportImport = (props: ExportImportProps) => {
|
|
||||||
const [isImporting, setIsImporting] = createSignal(false)
|
|
||||||
const [importStatus, setImportStatus] = createSignal<'idle' | 'validating' | 'success' | 'error'>('idle')
|
|
||||||
const [importMessage, setImportMessage] = createSignal('')
|
|
||||||
const [importData, setImportData] = createSignal<ExportData | null>(null)
|
|
||||||
|
|
||||||
const handleExport = async (type?: 'bookmarks' | 'tasks' | 'notes' | 'files' | 'all') => {
|
|
||||||
try {
|
|
||||||
let exportDataPayload = {}
|
|
||||||
let filename = ''
|
|
||||||
|
|
||||||
if (type === 'all' || !type) {
|
|
||||||
exportDataPayload = props.data || {}
|
|
||||||
filename = `trackeep-full-export-${new Date().toISOString().split('T')[0]}.json`
|
|
||||||
} else {
|
|
||||||
exportDataPayload = { [type]: props.data?.[type] || [] }
|
|
||||||
filename = `trackeep-${type}-export-${new Date().toISOString().split('T')[0]}.json`
|
|
||||||
}
|
|
||||||
|
|
||||||
await exportDataUtil(exportDataPayload, filename)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Export failed:', error)
|
|
||||||
alert('Export failed. Please try again.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileSelect = async (event: Event) => {
|
|
||||||
const file = (event.target as HTMLInputElement).files?.[0]
|
|
||||||
if (!file) return
|
|
||||||
|
|
||||||
setIsImporting(true)
|
|
||||||
setImportStatus('validating')
|
|
||||||
setImportMessage('Reading and validating file...')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await importDataUtil(file)
|
|
||||||
const validation = validateImportData(data)
|
|
||||||
|
|
||||||
if (!validation.isValid) {
|
|
||||||
setImportStatus('error')
|
|
||||||
setImportMessage(`Validation failed: ${validation.errors.join(', ')}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setImportData(data)
|
|
||||||
setImportStatus('success')
|
|
||||||
setImportMessage(getImportSummary(data))
|
|
||||||
} catch (error) {
|
|
||||||
setImportStatus('error')
|
|
||||||
setImportMessage((error as Error).message)
|
|
||||||
} finally {
|
|
||||||
setIsImporting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImport = async () => {
|
|
||||||
const data = importData()
|
|
||||||
if (!data || !props.onImport) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await props.onImport(data)
|
|
||||||
setImportStatus('idle')
|
|
||||||
setImportMessage('Import completed successfully!')
|
|
||||||
setImportData(null)
|
|
||||||
|
|
||||||
// Reset file input
|
|
||||||
const fileInput = document.getElementById('import-file-input') as HTMLInputElement
|
|
||||||
if (fileInput) fileInput.value = ''
|
|
||||||
} catch (error) {
|
|
||||||
setImportStatus('error')
|
|
||||||
setImportMessage(`Import failed: ${(error as Error).message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetImport = () => {
|
|
||||||
setImportStatus('idle')
|
|
||||||
setImportMessage('')
|
|
||||||
setImportData(null)
|
|
||||||
|
|
||||||
// Reset file input
|
|
||||||
const fileInput = document.getElementById('import-file-input') as HTMLInputElement
|
|
||||||
if (fileInput) fileInput.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="space-y-6">
|
|
||||||
{/* Export Section */}
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-medium text-white mb-4 flex items-center">
|
|
||||||
<IconDownload class="mr-2 h-5 w-5" />
|
|
||||||
Export Data
|
|
||||||
</h3>
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleExport('all')}
|
|
||||||
disabled={props.disabled}
|
|
||||||
class="text-gray-300 border-gray-600 hover:text-white hover:border-gray-500"
|
|
||||||
>
|
|
||||||
<IconFileText class="mr-2 h-4 w-4" />
|
|
||||||
Export All
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleExport('bookmarks')}
|
|
||||||
disabled={props.disabled}
|
|
||||||
class="text-gray-300 border-gray-600 hover:text-white hover:border-gray-500"
|
|
||||||
>
|
|
||||||
Bookmarks
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleExport('tasks')}
|
|
||||||
disabled={props.disabled}
|
|
||||||
class="text-gray-300 border-gray-600 hover:text-white hover:border-gray-500"
|
|
||||||
>
|
|
||||||
Tasks
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleExport('notes')}
|
|
||||||
disabled={props.disabled}
|
|
||||||
class="text-gray-300 border-gray-600 hover:text-white hover:border-gray-500"
|
|
||||||
>
|
|
||||||
Notes
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Import Section */}
|
|
||||||
<div>
|
|
||||||
<h3 class="text-lg font-medium text-white mb-4 flex items-center">
|
|
||||||
<IconUpload class="mr-2 h-5 w-5" />
|
|
||||||
Import Data
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* File Input */}
|
|
||||||
<div class="mb-4">
|
|
||||||
<input
|
|
||||||
id="import-file-input"
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
disabled={props.disabled || isImporting()}
|
|
||||||
class="block w-full text-sm text-gray-300 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Import Status */}
|
|
||||||
<Show when={importStatus() !== 'idle'}>
|
|
||||||
<div class={`p-4 rounded-lg mb-4 ${
|
|
||||||
importStatus() === 'success'
|
|
||||||
? 'bg-green-900/20 border border-green-700/50 text-green-300'
|
|
||||||
: importStatus() === 'error'
|
|
||||||
? 'bg-red-900/20 border border-red-700/50 text-red-300'
|
|
||||||
: 'bg-blue-900/20 border border-blue-700/50 text-blue-300'
|
|
||||||
}`}>
|
|
||||||
<div class="flex items-start">
|
|
||||||
<Show
|
|
||||||
when={importStatus() === 'success'}
|
|
||||||
fallback={<IconAlertTriangle class="mr-2 h-5 w-5 flex-shrink-0 mt-0.5" />}
|
|
||||||
>
|
|
||||||
<IconCheck class="mr-2 h-5 w-5 flex-shrink-0 mt-0.5" />
|
|
||||||
</Show>
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="font-medium">
|
|
||||||
{importStatus() === 'validating' ? 'Validating...' :
|
|
||||||
importStatus() === 'success' ? 'File Valid' :
|
|
||||||
'Import Error'}
|
|
||||||
</p>
|
|
||||||
<p class="text-sm mt-1">{importMessage()}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Import Actions */}
|
|
||||||
<Show when={importStatus() === 'success' && props.onImport}>
|
|
||||||
<div class="flex space-x-3">
|
|
||||||
<Button
|
|
||||||
onClick={handleImport}
|
|
||||||
disabled={isImporting()}
|
|
||||||
class="bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
{isImporting() ? 'Importing...' : 'Import Data'}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={resetImport}
|
|
||||||
disabled={isImporting()}
|
|
||||||
class="text-gray-300 border-gray-600 hover:text-white hover:border-gray-500"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Import Preview */}
|
|
||||||
<Show when={importData() && importStatus() === 'success'}>
|
|
||||||
<div class="mt-4 p-4 bg-gray-800 border border-gray-700 rounded-lg">
|
|
||||||
<h4 class="text-sm font-medium text-white mb-2">Import Preview</h4>
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-400">Bookmarks:</span>
|
|
||||||
<span class="ml-2 text-white">{importData()!.bookmarks.length}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-400">Tasks:</span>
|
|
||||||
<span class="ml-2 text-white">{importData()!.tasks.length}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-400">Notes:</span>
|
|
||||||
<span class="ml-2 text-white">{importData()!.notes.length}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-gray-400">Files:</span>
|
|
||||||
<span class="ml-2 text-white">{importData()!.files.length}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 text-xs text-gray-400">
|
|
||||||
Export date: {new Date(importData()!.exportDate).toLocaleDateString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
import { createSignal, For, Show, onMount, onCleanup } from 'solid-js';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { Card } from '@/components/ui/Card';
|
|
||||||
import { ModalPortal } from '@/components/ui/ModalPortal';
|
|
||||||
import {
|
|
||||||
IconX,
|
|
||||||
IconUpload,
|
|
||||||
IconLink,
|
|
||||||
IconTag,
|
|
||||||
IconFileText,
|
|
||||||
IconPhoto,
|
|
||||||
IconVideo,
|
|
||||||
IconMusic,
|
|
||||||
IconFolder
|
|
||||||
} from '@tabler/icons-solidjs';
|
|
||||||
|
|
||||||
interface FileUploadModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onUpload: (fileData: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Association {
|
|
||||||
id: string;
|
|
||||||
type: 'task' | 'bookmark' | 'note' | 'project';
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileUploadModal = (props: FileUploadModalProps) => {
|
|
||||||
const [selectedFile, setSelectedFile] = createSignal<File | null>(null);
|
|
||||||
const [description, setDescription] = createSignal('');
|
|
||||||
const [tags, setTags] = createSignal<string[]>([]);
|
|
||||||
const [tagInput, setTagInput] = createSignal('');
|
|
||||||
const [associations, setAssociations] = createSignal<Association[]>([]);
|
|
||||||
const [linkUrl, setLinkUrl] = createSignal('');
|
|
||||||
const [isLinkMode, setIsLinkMode] = createSignal(false);
|
|
||||||
const [dragActive, setDragActive] = createSignal(false);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape' && props.isOpen) {
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleFileSelect = (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
if (target.files && target.files.length > 0) {
|
|
||||||
setSelectedFile(target.files[0]);
|
|
||||||
setIsLinkMode(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrag = (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.type === "dragenter" || e.type === "dragover") {
|
|
||||||
setDragActive(true);
|
|
||||||
} else if (e.type === "dragleave") {
|
|
||||||
setDragActive(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setDragActive(false);
|
|
||||||
|
|
||||||
if (e.dataTransfer?.files && e.dataTransfer.files[0]) {
|
|
||||||
setSelectedFile(e.dataTransfer.files[0]);
|
|
||||||
setIsLinkMode(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTag = () => {
|
|
||||||
const tag = tagInput().trim();
|
|
||||||
if (tag && !tags().includes(tag)) {
|
|
||||||
setTags([...tags(), tag]);
|
|
||||||
setTagInput('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeTag = (tagToRemove: string) => {
|
|
||||||
setTags(tags().filter(tag => tag !== tagToRemove));
|
|
||||||
};
|
|
||||||
|
|
||||||
const addAssociation = (type: Association['type']) => {
|
|
||||||
// Mock association - in real app, this would open a picker
|
|
||||||
const mockAssociation: Association = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
type,
|
|
||||||
title: `Sample ${type} ${Date.now()}`
|
|
||||||
};
|
|
||||||
setAssociations([...associations(), mockAssociation]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeAssociation = (id: string) => {
|
|
||||||
setAssociations(associations().filter(assoc => assoc.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpload = () => {
|
|
||||||
const fileData = {
|
|
||||||
file: selectedFile(),
|
|
||||||
linkUrl: linkUrl(),
|
|
||||||
description: description(),
|
|
||||||
tags: tags(),
|
|
||||||
associations: associations(),
|
|
||||||
isLinkMode: isLinkMode()
|
|
||||||
};
|
|
||||||
|
|
||||||
props.onUpload(fileData);
|
|
||||||
props.onClose();
|
|
||||||
|
|
||||||
// Reset form
|
|
||||||
setSelectedFile(null);
|
|
||||||
setDescription('');
|
|
||||||
setTags([]);
|
|
||||||
setTagInput('');
|
|
||||||
setAssociations([]);
|
|
||||||
setLinkUrl('');
|
|
||||||
setIsLinkMode(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFileIcon = (file?: File) => {
|
|
||||||
if (!file) return IconFolder;
|
|
||||||
|
|
||||||
if (file.type.startsWith('image/')) return IconPhoto;
|
|
||||||
if (file.type.startsWith('video/')) return IconVideo;
|
|
||||||
if (file.type.startsWith('audio/')) return IconMusic;
|
|
||||||
return IconFileText;
|
|
||||||
};
|
|
||||||
|
|
||||||
const isValidUrl = (url: string) => {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const canUpload = () => {
|
|
||||||
if (isLinkMode()) {
|
|
||||||
return linkUrl() && isValidUrl(linkUrl());
|
|
||||||
}
|
|
||||||
return selectedFile() !== null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalPortal>
|
|
||||||
<Show when={props.isOpen}>
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
||||||
onClick={props.onClose}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="bg-card rounded-lg border border-border p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4 my-4"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div class="flex items-center justify-between mb-6">
|
|
||||||
<h2 class="text-xl font-semibold">Upload File</h2>
|
|
||||||
<Button variant="ghost" onClick={props.onClose}>
|
|
||||||
<IconX class="size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Upload Mode Toggle */}
|
|
||||||
<div class="flex gap-2 mb-6">
|
|
||||||
<Button
|
|
||||||
variant={!isLinkMode() ? "default" : "outline"}
|
|
||||||
onClick={() => setIsLinkMode(false)}
|
|
||||||
class="flex-1"
|
|
||||||
>
|
|
||||||
<IconUpload class="size-4 mr-2" />
|
|
||||||
Upload File
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={isLinkMode() ? "default" : "outline"}
|
|
||||||
onClick={() => setIsLinkMode(true)}
|
|
||||||
class="flex-1"
|
|
||||||
>
|
|
||||||
<IconLink class="size-4 mr-2" />
|
|
||||||
Add Link
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* File Upload Area */}
|
|
||||||
<Show when={!isLinkMode()}>
|
|
||||||
<Card class="p-8 mb-6">
|
|
||||||
<div
|
|
||||||
class={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
|
|
||||||
dragActive()
|
|
||||||
? 'border-primary bg-primary/10'
|
|
||||||
: 'border-border hover:border-primary/50'
|
|
||||||
}`}
|
|
||||||
onDragEnter={handleDrag}
|
|
||||||
onDragLeave={handleDrag}
|
|
||||||
onDragOver={handleDrag}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
class="hidden"
|
|
||||||
id="file-input"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={selectedFile()}
|
|
||||||
fallback={
|
|
||||||
<div>
|
|
||||||
<IconUpload class="size-12 mx-auto mb-4 text-muted-foreground" />
|
|
||||||
<p class="text-lg font-medium mb-2">Drop file here or click to browse</p>
|
|
||||||
<p class="text-sm text-muted-foreground mb-4">
|
|
||||||
Supports all file types
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => document.getElementById('file-input')?.click()}>
|
|
||||||
Choose File
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-4 justify-center">
|
|
||||||
<div class="text-4xl text-primary">
|
|
||||||
{(() => {
|
|
||||||
const IconComponent = getFileIcon(selectedFile()!);
|
|
||||||
return <IconComponent size={48} />;
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
<div class="text-left">
|
|
||||||
<p class="font-medium">{selectedFile()!.name}</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
{(selectedFile()!.size / 1024 / 1024).toFixed(2)} MB
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => document.getElementById('file-input')?.click()}
|
|
||||||
class="mt-2"
|
|
||||||
>
|
|
||||||
Change File
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Link Input */}
|
|
||||||
<Show when={isLinkMode()}>
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block text-sm font-medium mb-2">File URL</label>
|
|
||||||
<Input
|
|
||||||
type="url"
|
|
||||||
placeholder="https://example.com/file.pdf"
|
|
||||||
value={linkUrl()}
|
|
||||||
onInput={(e: any) => setLinkUrl(e.currentTarget.value)}
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block text-sm font-medium mb-2">Description</label>
|
|
||||||
<textarea
|
|
||||||
class="w-full px-3 py-2 border border-border rounded-lg bg-background resize-none"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Optional description..."
|
|
||||||
value={description()}
|
|
||||||
onInput={(e: any) => setDescription(e.currentTarget.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block text-sm font-medium mb-2">Tags</label>
|
|
||||||
<div class="flex gap-2 mb-3">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Add tag..."
|
|
||||||
value={tagInput()}
|
|
||||||
onInput={(e: any) => setTagInput(e.currentTarget.value)}
|
|
||||||
onKeyDown={(e: any) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
addTag();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<Button onClick={addTag} disabled={!tagInput().trim()}>
|
|
||||||
<IconTag class="size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<For each={tags()}>
|
|
||||||
{(tag) => (
|
|
||||||
<span class="inline-flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm">
|
|
||||||
{tag}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => removeTag(tag)}
|
|
||||||
class="h-4 w-4 p-0 hover:bg-primary/20"
|
|
||||||
>
|
|
||||||
<IconX class="size-3" />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Associations */}
|
|
||||||
<div class="mb-6">
|
|
||||||
<label class="block text-sm font-medium mb-2">Link to</label>
|
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => addAssociation('task')}
|
|
||||||
>
|
|
||||||
Task
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => addAssociation('bookmark')}
|
|
||||||
>
|
|
||||||
Bookmark
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => addAssociation('note')}
|
|
||||||
>
|
|
||||||
Note
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => addAssociation('project')}
|
|
||||||
>
|
|
||||||
Project
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<For each={associations()}>
|
|
||||||
{(assoc) => (
|
|
||||||
<div class="flex items-center justify-between p-2 bg-muted rounded-md">
|
|
||||||
<span class="text-sm">
|
|
||||||
<span class="font-medium capitalize">{assoc.type}:</span> {assoc.title}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => removeAssociation(assoc.id)}
|
|
||||||
class="h-6 w-6 p-0"
|
|
||||||
>
|
|
||||||
<IconX class="size-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div class="flex gap-3 pt-4 border-t border-border">
|
|
||||||
<Button variant="outline" onClick={props.onClose} class="flex-1">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleUpload} disabled={!canUpload()} class="flex-1">
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</ModalPortal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
import { createSignal } from 'solid-js';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
import { ModalPortal } from '@/components/ui/ModalPortal';
|
|
||||||
import { IconX } from '@tabler/icons-solidjs';
|
|
||||||
|
|
||||||
interface LearningPathFormData {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
category: string;
|
|
||||||
difficulty: 'beginner' | 'intermediate' | 'advanced';
|
|
||||||
duration: string;
|
|
||||||
thumbnail?: string;
|
|
||||||
is_featured?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LearningPathModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (learningPath: LearningPathFormData) => Promise<void>;
|
|
||||||
learningPath?: LearningPathFormData | null;
|
|
||||||
isEdit?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LearningPathModal = (props: LearningPathModalProps) => {
|
|
||||||
const [learningPathData, setLearningPathData] = createSignal<LearningPathFormData>({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
category: '',
|
|
||||||
difficulty: 'beginner',
|
|
||||||
duration: '',
|
|
||||||
thumbnail: '',
|
|
||||||
is_featured: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false);
|
|
||||||
|
|
||||||
// Reset form when modal opens/closes or learningPath changes
|
|
||||||
const resetForm = () => {
|
|
||||||
if (props.learningPath && props.isEdit) {
|
|
||||||
setLearningPathData({
|
|
||||||
title: props.learningPath.title,
|
|
||||||
description: props.learningPath.description,
|
|
||||||
category: props.learningPath.category,
|
|
||||||
difficulty: props.learningPath.difficulty,
|
|
||||||
duration: props.learningPath.duration,
|
|
||||||
thumbnail: props.learningPath.thumbnail || '',
|
|
||||||
is_featured: props.learningPath.is_featured || false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setLearningPathData({
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
category: '',
|
|
||||||
difficulty: 'beginner',
|
|
||||||
duration: '',
|
|
||||||
thumbnail: '',
|
|
||||||
is_featured: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset form when modal opens/closes
|
|
||||||
if (props.isOpen) {
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!learningPathData().title.trim() || !learningPathData().description.trim()) {
|
|
||||||
// Display inline error instead of alert
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await props.onSubmit(learningPathData());
|
|
||||||
props.onClose();
|
|
||||||
resetForm();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save learning path:', error);
|
|
||||||
// Let the parent handle the error display
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (field: keyof LearningPathFormData) => {
|
|
||||||
return (e: Event) => {
|
|
||||||
const target = e.currentTarget as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
||||||
if (target) {
|
|
||||||
setLearningPathData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: target.type === 'checkbox' ? (target as HTMLInputElement).checked : target.value
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!props.isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalPortal>
|
|
||||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
||||||
<div class="bg-[#1a1a1a] rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto mx-4 my-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div class="flex items-center justify-between p-6 border-b border-[#404040]">
|
|
||||||
<h2 class="text-xl font-semibold text-[#fafafa]">
|
|
||||||
{props.isEdit ? 'Edit Learning Path' : 'Create New Learning Path'}
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={props.onClose}
|
|
||||||
class="text-[#a3a3a3] hover:text-[#fafafa]"
|
|
||||||
>
|
|
||||||
<IconX class="size-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<form onSubmit={handleSubmit} class="p-6 space-y-6">
|
|
||||||
{/* Title */}
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-[#fafafa] mb-2">
|
|
||||||
Title *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={learningPathData().title}
|
|
||||||
onInput={handleInputChange('title')}
|
|
||||||
placeholder="Enter learning path title"
|
|
||||||
required
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-[#fafafa] mb-2">
|
|
||||||
Description *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={learningPathData().description}
|
|
||||||
onInput={handleInputChange('description')}
|
|
||||||
placeholder="Describe what students will learn in this path"
|
|
||||||
rows={4}
|
|
||||||
required
|
|
||||||
class="w-full px-3 py-2 bg-[#262626] text-[#fafafa] border border-[#404040] rounded-lg focus:outline-none focus:border-blue-500 resize-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Category and Difficulty */}
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-[#fafafa] mb-2">
|
|
||||||
Category *
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={learningPathData().category}
|
|
||||||
onChange={handleInputChange('category')}
|
|
||||||
required
|
|
||||||
class="w-full px-3 py-2 bg-[#262626] text-[#fafafa] border border-[#404040] rounded-lg focus:outline-none focus:border-blue-500"
|
|
||||||
>
|
|
||||||
<option value="">Select a category</option>
|
|
||||||
<option value="programming">Programming</option>
|
|
||||||
<option value="web-development">Web Development</option>
|
|
||||||
<option value="mobile-development">Mobile Development</option>
|
|
||||||
<option value="data-science">Data Science</option>
|
|
||||||
<option value="machine-learning">Machine Learning</option>
|
|
||||||
<option value="cybersecurity">Cybersecurity</option>
|
|
||||||
<option value="design">Design</option>
|
|
||||||
<option value="business">Business</option>
|
|
||||||
<option value="marketing">Marketing</option>
|
|
||||||
<option value="photography">Photography</option>
|
|
||||||
<option value="music">Music</option>
|
|
||||||
<option value="writing">Writing</option>
|
|
||||||
<option value="languages">Languages</option>
|
|
||||||
<option value="other">Other</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-[#fafafa] mb-2">
|
|
||||||
Difficulty *
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={learningPathData().difficulty}
|
|
||||||
onChange={handleInputChange('difficulty')}
|
|
||||||
required
|
|
||||||
class="w-full px-3 py-2 bg-[#262626] text-[#fafafa] border border-[#404040] rounded-lg focus:outline-none focus:border-blue-500"
|
|
||||||
>
|
|
||||||
<option value="beginner">Beginner</option>
|
|
||||||
<option value="intermediate">Intermediate</option>
|
|
||||||
<option value="advanced">Advanced</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Duration */}
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-[#fafafa] mb-2">
|
|
||||||
Duration *
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={learningPathData().duration}
|
|
||||||
onInput={handleInputChange('duration')}
|
|
||||||
placeholder="e.g., 8 weeks, 3 months"
|
|
||||||
required
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Thumbnail */}
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-[#fafafa] mb-2">
|
|
||||||
Thumbnail URL (optional)
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
value={learningPathData().thumbnail}
|
|
||||||
onInput={handleInputChange('thumbnail')}
|
|
||||||
placeholder="https://example.com/image.jpg"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Featured */}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="featured"
|
|
||||||
checked={learningPathData().is_featured}
|
|
||||||
onChange={handleInputChange('is_featured')}
|
|
||||||
class="w-4 h-4 text-blue-600 bg-[#262626] border-[#404040] rounded focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
<label for="featured" class="text-sm font-medium text-[#fafafa]">
|
|
||||||
Featured Learning Path
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div class="flex justify-end gap-3 pt-4 border-t border-[#404040]">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={props.onClose}
|
|
||||||
disabled={isSubmitting()}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={(e) => handleSubmit(e)}
|
|
||||||
disabled={isSubmitting()}
|
|
||||||
class="min-w-[100px]"
|
|
||||||
>
|
|
||||||
{isSubmitting() ? (
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<span class="w-4 h-4 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin"></span>
|
|
||||||
{props.isEdit ? 'Updating...' : 'Creating...'}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
props.isEdit ? 'Update Learning Path' : 'Create Learning Path'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalPortal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
import { createSignal, For, Show } from 'solid-js'
|
|
||||||
import { Button } from './Button'
|
|
||||||
import { Input } from './Input'
|
|
||||||
import { IconSearch, IconFilter, IconX, IconCalendar, IconTag, IconFlag } from '@tabler/icons-solidjs'
|
|
||||||
|
|
||||||
export interface SearchFiltersProps {
|
|
||||||
onSearchChange: (query: string) => void
|
|
||||||
onFiltersChange: (filters: Record<string, any>) => void
|
|
||||||
placeholder?: string
|
|
||||||
showFilters?: boolean
|
|
||||||
filterOptions?: {
|
|
||||||
tags?: string[]
|
|
||||||
statuses?: string[]
|
|
||||||
priorities?: string[]
|
|
||||||
dateRanges?: string[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchFilters = (props: SearchFiltersProps) => {
|
|
||||||
const [searchQuery, setSearchQuery] = createSignal('')
|
|
||||||
const [showAdvancedFilters, setShowAdvancedFilters] = createSignal(props.showFilters || false)
|
|
||||||
const [activeFilters, setActiveFilters] = createSignal<Record<string, any>>({})
|
|
||||||
|
|
||||||
const handleSearchChange = (value: string) => {
|
|
||||||
setSearchQuery(value)
|
|
||||||
props.onSearchChange(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFilterChange = (filterKey: string, value: any) => {
|
|
||||||
const newFilters = { ...activeFilters(), [filterKey]: value }
|
|
||||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
|
||||||
delete newFilters[filterKey]
|
|
||||||
}
|
|
||||||
setActiveFilters(newFilters)
|
|
||||||
props.onFiltersChange(newFilters)
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
|
||||||
setActiveFilters({})
|
|
||||||
setSearchQuery('')
|
|
||||||
props.onSearchChange('')
|
|
||||||
props.onFiltersChange({})
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeFilterCount = () => {
|
|
||||||
const filters = activeFilters()
|
|
||||||
return Object.keys(filters).length + (searchQuery() ? 1 : 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="space-y-4">
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div class="relative">
|
|
||||||
<IconSearch class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
type="search"
|
|
||||||
placeholder={props.placeholder || "Search..."}
|
|
||||||
value={searchQuery()}
|
|
||||||
onInput={(e) => e.target && handleSearchChange((e.target as HTMLInputElement).value)}
|
|
||||||
class="pl-10 bg-gray-800 border-gray-700 text-white placeholder-gray-400"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Filter Toggle */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setShowAdvancedFilters(!showAdvancedFilters())}
|
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white"
|
|
||||||
>
|
|
||||||
<IconFilter class="h-4 w-4" />
|
|
||||||
<Show when={activeFilterCount() > 0}>
|
|
||||||
<span class="ml-1 text-xs bg-blue-600 text-white rounded-full px-2 py-0.5">
|
|
||||||
{activeFilterCount()}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Advanced Filters */}
|
|
||||||
<Show when={showAdvancedFilters()}>
|
|
||||||
<div class="bg-gray-800 border border-gray-700 rounded-lg p-4 space-y-4">
|
|
||||||
{/* Filter Header */}
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h3 class="text-sm font-medium text-white">Advanced Filters</h3>
|
|
||||||
<div class="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={clearAllFilters}
|
|
||||||
class="text-gray-400 hover:text-white"
|
|
||||||
>
|
|
||||||
<IconX class="mr-1 h-3 w-3" />
|
|
||||||
Clear All
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
{/* Tags Filter */}
|
|
||||||
<Show when={props.filterOptions?.tags && props.filterOptions.tags.length > 0}>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
|
||||||
<IconTag class="inline h-4 w-4 mr-1" />
|
|
||||||
Tags
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-md px-3 py-2 text-sm"
|
|
||||||
onChange={(e) => handleFilterChange('tag', (e.target as HTMLSelectElement).value)}
|
|
||||||
>
|
|
||||||
<option value="">All Tags</option>
|
|
||||||
<For each={props.filterOptions!.tags}>
|
|
||||||
{(tag) => (
|
|
||||||
<option value={tag} selected={activeFilters().tag === tag}>
|
|
||||||
{tag}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Status Filter */}
|
|
||||||
<Show when={props.filterOptions?.statuses && props.filterOptions.statuses.length > 0}>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">Status</label>
|
|
||||||
<select
|
|
||||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-md px-3 py-2 text-sm"
|
|
||||||
onChange={(e) => handleFilterChange('status', (e.target as HTMLSelectElement).value)}
|
|
||||||
>
|
|
||||||
<option value="">All Statuses</option>
|
|
||||||
<For each={props.filterOptions!.statuses}>
|
|
||||||
{(status) => (
|
|
||||||
<option value={status} selected={activeFilters().status === status}>
|
|
||||||
{status.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Priority Filter */}
|
|
||||||
<Show when={props.filterOptions?.priorities && props.filterOptions.priorities.length > 0}>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
|
||||||
<IconFlag class="inline h-4 w-4 mr-1" />
|
|
||||||
Priority
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-md px-3 py-2 text-sm"
|
|
||||||
onChange={(e) => handleFilterChange('priority', (e.target as HTMLSelectElement).value)}
|
|
||||||
>
|
|
||||||
<option value="">All Priorities</option>
|
|
||||||
<For each={props.filterOptions!.priorities}>
|
|
||||||
{(priority) => (
|
|
||||||
<option value={priority} selected={activeFilters().priority === priority}>
|
|
||||||
{priority.charAt(0).toUpperCase() + priority.slice(1)}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Date Range Filter */}
|
|
||||||
<Show when={props.filterOptions?.dateRanges && props.filterOptions.dateRanges.length > 0}>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-300 mb-2">
|
|
||||||
<IconCalendar class="inline h-4 w-4 mr-1" />
|
|
||||||
Date Range
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
class="w-full bg-gray-700 border border-gray-600 text-white rounded-md px-3 py-2 text-sm"
|
|
||||||
onChange={(e) => handleFilterChange('dateRange', (e.target as HTMLSelectElement).value)}
|
|
||||||
>
|
|
||||||
<option value="">Any Time</option>
|
|
||||||
<For each={props.filterOptions!.dateRanges}>
|
|
||||||
{(range) => (
|
|
||||||
<option value={range} selected={activeFilters().dateRange === range}>
|
|
||||||
{range}
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Active Filters Display */}
|
|
||||||
<Show when={activeFilterCount() > 0}>
|
|
||||||
<div class="flex flex-wrap gap-2 pt-2 border-t border-gray-700">
|
|
||||||
<For each={Object.entries(activeFilters())}>
|
|
||||||
{([key, value]) => (
|
|
||||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-600 text-white">
|
|
||||||
{key}: {value}
|
|
||||||
<button
|
|
||||||
onClick={() => handleFilterChange(key, null)}
|
|
||||||
class="ml-1 hover:text-blue-200"
|
|
||||||
>
|
|
||||||
<IconX class="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
<Show when={searchQuery()}>
|
|
||||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs bg-blue-600 text-white">
|
|
||||||
Search: {searchQuery()}
|
|
||||||
<button
|
|
||||||
onClick={() => handleSearchChange('')}
|
|
||||||
class="ml-1 hover:text-blue-200"
|
|
||||||
>
|
|
||||||
<IconX class="h-3 w-3" />
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
import { createSignal, Show, createMemo } from 'solid-js';
|
|
||||||
import {
|
|
||||||
IconRefresh,
|
|
||||||
IconCheck,
|
|
||||||
IconAlertTriangle,
|
|
||||||
IconDownload,
|
|
||||||
IconLoader2
|
|
||||||
} from '@tabler/icons-solidjs';
|
|
||||||
import { updateStore } from '../../stores/updateStore';
|
|
||||||
import { ModalPortal } from './ModalPortal';
|
|
||||||
|
|
||||||
interface UpdateCheckerProps {
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UpdateChecker(props: UpdateCheckerProps) {
|
|
||||||
const [showUpdateModal, setShowUpdateModal] = createSignal(false);
|
|
||||||
|
|
||||||
// Initialize update store
|
|
||||||
updateStore.ensureInitialized().catch(console.error);
|
|
||||||
|
|
||||||
const installUpdate = () => {
|
|
||||||
updateStore.installUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelUpdate = () => {
|
|
||||||
updateStore.cancelUpdate();
|
|
||||||
setShowUpdateModal(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create reactive computed values
|
|
||||||
const buttonClasses = createMemo(() => {
|
|
||||||
const updateAvailable = updateStore.updateAvailable();
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
const error = updateStore.error();
|
|
||||||
|
|
||||||
return {
|
|
||||||
"bg-blue-500/20 text-blue-400": updateAvailable && !updateStatus.downloading && !updateStatus.installing,
|
|
||||||
"hover:bg-blue-500/30": updateAvailable && !updateStatus.downloading && !updateStatus.installing,
|
|
||||||
"bg-orange-500/20 text-orange-400": updateStatus.downloading || updateStatus.installing,
|
|
||||||
"hover:bg-orange-500/30": updateStatus.downloading || updateStatus.installing,
|
|
||||||
"bg-green-500/20 text-green-400": updateStatus.completed,
|
|
||||||
"hover:bg-green-500/30": updateStatus.completed,
|
|
||||||
"bg-red-500/20 text-red-400": !!error,
|
|
||||||
"hover:bg-red-500/30": !!error,
|
|
||||||
"hover:bg-[#262626] hover:text-white text-[#a3a3a3]": !updateAvailable && !updateStatus.downloading && !updateStatus.installing && !updateStatus.completed && !error
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDisabled = createMemo(() => {
|
|
||||||
const isChecking = updateStore.isChecking();
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
return isChecking || updateStatus.downloading || updateStatus.installing;
|
|
||||||
});
|
|
||||||
|
|
||||||
const getStatusIcon = () => {
|
|
||||||
const isChecking = updateStore.isChecking();
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
const updateAvailable = updateStore.updateAvailable();
|
|
||||||
const error = updateStore.error();
|
|
||||||
|
|
||||||
if (isChecking) return <IconLoader2 class="size-4 animate-spin" />;
|
|
||||||
if (updateStatus.downloading || updateStatus.installing) return <IconLoader2 class="size-4 animate-spin" />;
|
|
||||||
if (updateStatus.completed) return <IconCheck class="size-4 text-green-500" />;
|
|
||||||
if (updateAvailable) return <IconDownload class="size-4 text-blue-500" />;
|
|
||||||
if (error) return <IconAlertTriangle class="size-4 text-red-500" />;
|
|
||||||
return <IconRefresh class="size-4" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = () => {
|
|
||||||
const isChecking = updateStore.isChecking();
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
const updateAvailable = updateStore.updateAvailable();
|
|
||||||
const error = updateStore.error();
|
|
||||||
|
|
||||||
if (isChecking) return 'Checking...';
|
|
||||||
if (updateStatus.downloading) return `Downloading... ${Math.round(updateStatus.progress)}%`;
|
|
||||||
if (updateStatus.installing) return `Installing... ${Math.round(updateStatus.progress)}%`;
|
|
||||||
if (updateStatus.completed) return 'Update Complete';
|
|
||||||
if (updateAvailable) return 'Update Available';
|
|
||||||
if (error) return 'Update Failed';
|
|
||||||
return 'Check Updates';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class={`flex flex-col gap-2 ${props.class || ''}`}>
|
|
||||||
{/* Current Version Display */}
|
|
||||||
<div class="text-xs text-muted-foreground px-2 text-center">
|
|
||||||
Version {updateStore.currentVersion()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Check Updates Button */}
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const updateAvailable = updateStore.updateAvailable();
|
|
||||||
if (updateAvailable) {
|
|
||||||
setShowUpdateModal(true);
|
|
||||||
} else {
|
|
||||||
updateStore.checkForUpdates();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class="group inline-flex rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 justify-start items-center gap-2 truncate relative overflow-hidden w-full"
|
|
||||||
classList={buttonClasses()}
|
|
||||||
disabled={isDisabled()}
|
|
||||||
>
|
|
||||||
<div class="relative z-10 flex items-center gap-2">
|
|
||||||
{getStatusIcon()}
|
|
||||||
<div class="transition-colors truncate">
|
|
||||||
{getStatusText()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200"></div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Update Modal */}
|
|
||||||
<Show when={showUpdateModal() && updateStore.updateInfo()}>
|
|
||||||
<ModalPortal>
|
|
||||||
<div class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
|
||||||
<div class="bg-card border border-border rounded-lg shadow-lg max-w-md w-full max-h-[80vh] overflow-auto">
|
|
||||||
<div class="p-6">
|
|
||||||
<div class="flex items-center gap-3 mb-4">
|
|
||||||
<IconDownload class="size-6 text-blue-500" />
|
|
||||||
<h2 class="text-lg font-semibold">Update Available</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<div class="flex justify-between items-center mb-2">
|
|
||||||
<span class="text-sm text-muted-foreground">Current Version</span>
|
|
||||||
<span class="text-sm font-medium">{updateStore.currentVersion()}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<span class="text-sm text-muted-foreground">Latest Version</span>
|
|
||||||
<span class="text-sm font-medium text-blue-500">{updateStore.updateInfo()!.version}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="text-sm font-medium mb-2">Release Notes</h3>
|
|
||||||
<div class="text-sm text-muted-foreground whitespace-pre-line bg-muted/30 rounded p-3">
|
|
||||||
{updateStore.updateInfo()!.releaseNotes}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-between items-center text-sm">
|
|
||||||
<span class="text-muted-foreground">Download Size</span>
|
|
||||||
<span>{updateStore.updateInfo()!.size}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={(() => {
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
return updateStatus.downloading || updateStatus.installing;
|
|
||||||
})()}>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex justify-between text-sm">
|
|
||||||
<span class="text-muted-foreground">
|
|
||||||
{(() => {
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
return updateStatus.downloading ? 'Downloading' : 'Installing';
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
<span>{(() => Math.round(updateStore.updateStatus().progress))()}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-muted rounded-full h-2">
|
|
||||||
<div
|
|
||||||
class="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
|
||||||
style={{ width: `${updateStore.updateStatus().progress}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={updateStore.error()}>
|
|
||||||
<div class="bg-red-500/10 border border-red-500/20 rounded p-3">
|
|
||||||
<div class="flex items-center gap-2 text-red-500 text-sm">
|
|
||||||
<IconAlertTriangle class="size-4" />
|
|
||||||
<span>{updateStore.error()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={(() => updateStore.updateStatus().completed)}>
|
|
||||||
<div class="bg-green-500/10 border border-green-500/20 rounded p-3">
|
|
||||||
<div class="flex items-center gap-2 text-green-500 text-sm">
|
|
||||||
<IconCheck class="size-4" />
|
|
||||||
<span>Update completed successfully! Restarting...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-3 mt-6">
|
|
||||||
<Show when={(() => {
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
return !updateStatus.downloading && !updateStatus.installing && !updateStatus.completed;
|
|
||||||
})()}>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowUpdateModal(false)}
|
|
||||||
class="flex-1 px-4 py-2 text-sm border border-border rounded-md hover:bg-muted transition-colors"
|
|
||||||
>
|
|
||||||
Later
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={installUpdate}
|
|
||||||
disabled={(() => {
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
return updateStatus.downloading || updateStatus.installing;
|
|
||||||
})()}
|
|
||||||
class="flex-1 px-4 py-2 text-sm bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<Show when={(() => {
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
return updateStatus.downloading || updateStatus.installing;
|
|
||||||
})()}>
|
|
||||||
<IconLoader2 class="size-4 animate-spin" />
|
|
||||||
</Show>
|
|
||||||
{(() => {
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
return updateStatus.downloading || updateStatus.installing ? 'Installing...' : 'Install Update';
|
|
||||||
})()}
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={(() => {
|
|
||||||
const updateStatus = updateStore.updateStatus();
|
|
||||||
const error = updateStore.error();
|
|
||||||
return updateStatus.downloading || updateStatus.installing || error;
|
|
||||||
})()}>
|
|
||||||
<button
|
|
||||||
onClick={cancelUpdate}
|
|
||||||
class="px-4 py-2 text-sm border border-border rounded-md hover:bg-muted transition-colors"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={(() => updateStore.updateStatus().completed)}>
|
|
||||||
<button
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
class="w-full px-4 py-2 text-sm bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
|
|
||||||
>
|
|
||||||
Reload Application
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalPortal>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { createSignal, For, Show } from 'solid-js'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface VirtualListProps<T> {
|
|
||||||
items: T[]
|
|
||||||
itemHeight: number
|
|
||||||
containerHeight: number
|
|
||||||
renderItem: (item: T, index: number) => any
|
|
||||||
overscan?: number
|
|
||||||
class?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VirtualList<T>(props: VirtualListProps<T>) {
|
|
||||||
const [scrollTop, setScrollTop] = createSignal(0)
|
|
||||||
|
|
||||||
const overscan = props.overscan || 5
|
|
||||||
const itemHeight = props.itemHeight
|
|
||||||
|
|
||||||
const visibleRange = () => {
|
|
||||||
const start = Math.floor(scrollTop() / itemHeight)
|
|
||||||
const visibleCount = Math.ceil(props.containerHeight / itemHeight)
|
|
||||||
const end = start + visibleCount
|
|
||||||
|
|
||||||
return {
|
|
||||||
start: Math.max(0, start - overscan),
|
|
||||||
end: Math.min(props.items.length, end + overscan)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalHeight = () => props.items.length * itemHeight
|
|
||||||
|
|
||||||
const offsetY = () => visibleRange().start * itemHeight
|
|
||||||
|
|
||||||
const visibleItems = () => {
|
|
||||||
const { start, end } = visibleRange()
|
|
||||||
return props.items.slice(start, end).map((item, index) => ({
|
|
||||||
item,
|
|
||||||
index: start + index
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScroll = (e: Event) => {
|
|
||||||
const target = e.target as HTMLElement
|
|
||||||
setScrollTop(target.scrollTop)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={cn('overflow-auto', props.class)}
|
|
||||||
style={{ height: `${props.containerHeight}px` }}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
>
|
|
||||||
<div style={{ height: `${totalHeight()}px`, position: 'relative' }}>
|
|
||||||
<div style={{ transform: `translateY(${offsetY()}px)` }}>
|
|
||||||
<For each={visibleItems()}>
|
|
||||||
{({ item, index }) => (
|
|
||||||
<div style={{ height: `${itemHeight}px` }}>
|
|
||||||
{props.renderItem(item, index)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InfiniteScrollProps<T> {
|
|
||||||
items: T[]
|
|
||||||
loading: boolean
|
|
||||||
hasMore: boolean
|
|
||||||
onLoadMore: () => void
|
|
||||||
renderItem: (item: T, index: number) => any
|
|
||||||
loader?: any
|
|
||||||
class?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InfiniteScroll<T>(props: InfiniteScrollProps<T>) {
|
|
||||||
const handleScroll = (e: Event) => {
|
|
||||||
const target = e.target as HTMLElement
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = target
|
|
||||||
|
|
||||||
// Load more when user is near the bottom
|
|
||||||
if (
|
|
||||||
!props.loading &&
|
|
||||||
props.hasMore &&
|
|
||||||
scrollHeight - scrollTop - clientHeight < 200
|
|
||||||
) {
|
|
||||||
props.onLoadMore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={cn('overflow-auto', props.class)}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
>
|
|
||||||
<For each={props.items}>
|
|
||||||
{(item, index) => props.renderItem(item, index())}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Show when={props.loading}>
|
|
||||||
{props.loader || (
|
|
||||||
<div class="p-4 text-center">
|
|
||||||
<div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={!props.hasMore && props.items.length > 0}>
|
|
||||||
<div class="p-4 text-center text-muted-foreground text-sm">
|
|
||||||
No more items to load
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,373 +0,0 @@
|
|||||||
import { createQuery, useQueryClient, createMutation } from '@tanstack/solid-query';
|
|
||||||
import { getAuthHeaders } from './auth';
|
|
||||||
|
|
||||||
// API base URL
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1';
|
|
||||||
|
|
||||||
// Retry configuration
|
|
||||||
const DEFAULT_RETRY_CONFIG = {
|
|
||||||
retry: 3,
|
|
||||||
retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000),
|
|
||||||
networkMode: 'online' as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generic API client with retry logic
|
|
||||||
const apiClient = {
|
|
||||||
async get<T>(endpoint: string): Promise<T> {
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.error || `API Error: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
async post<T>(endpoint: string, data?: any): Promise<T> {
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
...getAuthHeaders(),
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.error || `API Error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
async put<T>(endpoint: string, data?: any): Promise<T> {
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
...getAuthHeaders(),
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.error || `API Error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
async delete<T>(endpoint: string): Promise<T> {
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.error || `API Error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export interface Bookmark {
|
|
||||||
id: number;
|
|
||||||
user_id: number;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
description?: string;
|
|
||||||
is_read: boolean;
|
|
||||||
is_favorite: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Task {
|
|
||||||
id: number;
|
|
||||||
user_id: number;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
status: 'pending' | 'in_progress' | 'completed';
|
|
||||||
priority: 'low' | 'medium' | 'high';
|
|
||||||
progress: number;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Note {
|
|
||||||
id: number;
|
|
||||||
user_id: number;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
content_type: string;
|
|
||||||
is_pinned: boolean;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileItem {
|
|
||||||
id: number;
|
|
||||||
user_id: number;
|
|
||||||
filename: string;
|
|
||||||
original_name: string;
|
|
||||||
file_size: number;
|
|
||||||
mime_type: string;
|
|
||||||
file_path: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bookmarks API
|
|
||||||
export const bookmarksApi = {
|
|
||||||
useGetAll: () => createQuery(() => ({
|
|
||||||
queryKey: ['bookmarks'],
|
|
||||||
queryFn: () => apiClient.get<Bookmark[]>('/bookmarks'),
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useGetById: (id: number) => createQuery(() => ({
|
|
||||||
queryKey: ['bookmarks', id],
|
|
||||||
queryFn: () => apiClient.get<Bookmark>(`/bookmarks/${id}`),
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useCreate: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: (data: Omit<Bookmark, 'id' | 'user_id' | 'created_at' | 'updated_at'>) =>
|
|
||||||
apiClient.post<Bookmark>('/bookmarks', data),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['bookmarks'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to create bookmark:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
useUpdate: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<Bookmark> }) =>
|
|
||||||
apiClient.put<Bookmark>(`/bookmarks/${id}`, data),
|
|
||||||
onSuccess: (_, { id }) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['bookmarks'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['bookmarks', id] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to update bookmark:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
useDelete: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: (id: number) => apiClient.delete(`/bookmarks/${id}`),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['bookmarks'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to delete bookmark:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tasks API
|
|
||||||
export const tasksApi = {
|
|
||||||
useGetAll: () => createQuery(() => ({
|
|
||||||
queryKey: ['tasks'],
|
|
||||||
queryFn: () => apiClient.get<Task[]>('/tasks'),
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useGetById: (id: number) => createQuery(() => ({
|
|
||||||
queryKey: ['tasks', id],
|
|
||||||
queryFn: () => apiClient.get<Task>(`/tasks/${id}`),
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useCreate: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: (data: Omit<Task, 'id' | 'user_id' | 'created_at' | 'updated_at'>) =>
|
|
||||||
apiClient.post<Task>('/tasks', data),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to create task:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
useUpdate: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<Task> }) =>
|
|
||||||
apiClient.put<Task>(`/tasks/${id}`, data),
|
|
||||||
onSuccess: (_, { id }) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks', id] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to update task:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
useDelete: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: (id: number) => apiClient.delete(`/tasks/${id}`),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['tasks'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to delete task:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Notes API
|
|
||||||
export const notesApi = {
|
|
||||||
useGetAll: (search?: string, tag?: string) => createQuery(() => ({
|
|
||||||
queryKey: ['notes', search, tag],
|
|
||||||
queryFn: () => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (search) params.append('search', search);
|
|
||||||
if (tag) params.append('tag', tag);
|
|
||||||
const queryString = params.toString();
|
|
||||||
return apiClient.get<Note[]>(`/notes${queryString ? `?${queryString}` : ''}`);
|
|
||||||
},
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useGetById: (id: number) => createQuery(() => ({
|
|
||||||
queryKey: ['notes', id],
|
|
||||||
queryFn: () => apiClient.get<Note>(`/notes/${id}`),
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useCreate: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: (data: Omit<Note, 'id' | 'user_id' | 'created_at' | 'updated_at'>) =>
|
|
||||||
apiClient.post<Note>('/notes', data),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['notes'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to create note:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
useUpdate: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<Note> }) =>
|
|
||||||
apiClient.put<Note>(`/notes/${id}`, data),
|
|
||||||
onSuccess: (_, { id }) => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['notes'] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['notes', id] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to update note:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
useDelete: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: (id: number) => apiClient.delete(`/notes/${id}`),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['notes'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to delete note:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Files API
|
|
||||||
export const filesApi = {
|
|
||||||
useGetAll: () => createQuery(() => ({
|
|
||||||
queryKey: ['files'],
|
|
||||||
queryFn: () => apiClient.get<FileItem[]>('/files'),
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useGetById: (id: number) => createQuery(() => ({
|
|
||||||
queryKey: ['files', id],
|
|
||||||
queryFn: () => apiClient.get<FileItem>(`/files/${id}`),
|
|
||||||
...DEFAULT_RETRY_CONFIG,
|
|
||||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
|
||||||
})),
|
|
||||||
|
|
||||||
useUpload: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: async (file: globalThis.File) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/files/upload`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': getAuthHeaders().Authorization || '',
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(errorData.error || 'Upload failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to upload file:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
useDelete: () => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return createMutation(() => ({
|
|
||||||
mutationFn: (id: number) => apiClient.delete(`/files/${id}`),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Failed to delete file:', error);
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import type { Bookmark, Task, Note, FileItem } from './api-client'
|
|
||||||
|
|
||||||
export interface ExportData {
|
|
||||||
version: string
|
|
||||||
exportDate: string
|
|
||||||
bookmarks: Bookmark[]
|
|
||||||
tasks: Task[]
|
|
||||||
notes: Note[]
|
|
||||||
files: FileItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const exportData = async (data: {
|
|
||||||
bookmarks?: Bookmark[]
|
|
||||||
tasks?: Task[]
|
|
||||||
notes?: Note[]
|
|
||||||
files?: FileItem[]
|
|
||||||
}, filename?: string) => {
|
|
||||||
const exportData: ExportData = {
|
|
||||||
version: '1.0.0',
|
|
||||||
exportDate: new Date().toISOString(),
|
|
||||||
bookmarks: data.bookmarks || [],
|
|
||||||
tasks: data.tasks || [],
|
|
||||||
notes: data.notes || [],
|
|
||||||
files: data.files || []
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonString = JSON.stringify(exportData, null, 2)
|
|
||||||
const blob = new Blob([jsonString], { type: 'application/json' })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = filename || `trackeep-export-${new Date().toISOString().split('T')[0]}.json`
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const importData = async (file: File): Promise<ExportData> => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.onload = (e) => {
|
|
||||||
try {
|
|
||||||
const content = e.target?.result as string
|
|
||||||
const data = JSON.parse(content) as ExportData
|
|
||||||
|
|
||||||
// Validate the structure
|
|
||||||
if (!data.version || !data.exportDate) {
|
|
||||||
throw new Error('Invalid export file format')
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(data)
|
|
||||||
} catch (error) {
|
|
||||||
reject(new Error('Failed to parse export file: ' + (error as Error).message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.onerror = () => {
|
|
||||||
reject(new Error('Failed to read file'))
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.readAsText(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const validateImportData = (data: ExportData): { isValid: boolean; errors: string[] } => {
|
|
||||||
const errors: string[] = []
|
|
||||||
|
|
||||||
// Check version compatibility
|
|
||||||
if (!data.version) {
|
|
||||||
errors.push('Missing version information')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check required fields
|
|
||||||
if (!data.exportDate) {
|
|
||||||
errors.push('Missing export date')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate data types
|
|
||||||
if (data.bookmarks && !Array.isArray(data.bookmarks)) {
|
|
||||||
errors.push('Bookmarks data is not an array')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.tasks && !Array.isArray(data.tasks)) {
|
|
||||||
errors.push('Tasks data is not an array')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.notes && !Array.isArray(data.notes)) {
|
|
||||||
errors.push('Notes data is not an array')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.files && !Array.isArray(data.files)) {
|
|
||||||
errors.push('Files data is not an array')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
isValid: errors.length === 0,
|
|
||||||
errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getImportSummary = (data: ExportData): string => {
|
|
||||||
const summary = []
|
|
||||||
|
|
||||||
if (data.bookmarks.length > 0) {
|
|
||||||
summary.push(`${data.bookmarks.length} bookmarks`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.tasks.length > 0) {
|
|
||||||
summary.push(`${data.tasks.length} tasks`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.notes.length > 0) {
|
|
||||||
summary.push(`${data.notes.length} notes`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.files.length > 0) {
|
|
||||||
summary.push(`${data.files.length} files`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (summary.length === 0) {
|
|
||||||
return 'No data to import'
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Import contains: ${summary.join(', ')}`
|
|
||||||
}
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -514,7 +514,10 @@ export const Bookmarks = () => {
|
|||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const img = e.currentTarget;
|
const img = e.currentTarget;
|
||||||
img.style.display = 'none';
|
img.style.display = 'none';
|
||||||
img.parentElement!.innerHTML = `<span class="text-xs text-muted-foreground font-medium">${getBookmarkInitial(bookmark.title)}</span>`;
|
const span = document.createElement('span');
|
||||||
|
span.className = 'text-xs text-muted-foreground font-medium';
|
||||||
|
span.textContent = getBookmarkInitial(bookmark.title);
|
||||||
|
img.parentElement!.appendChild(span);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -335,7 +335,12 @@ export const Files = () => {
|
|||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = data.install_url as string;
|
const installUrl = data.install_url as string;
|
||||||
|
if (installUrl && (installUrl.startsWith('https://github.com/') || installUrl.startsWith('https://api.github.com/'))) {
|
||||||
|
window.location.href = installUrl;
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid install URL received');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : 'Failed to start GitHub App installation';
|
const message = error instanceof Error ? error.message : 'Failed to start GitHub App installation';
|
||||||
setGitHubError(message);
|
setGitHubError(message);
|
||||||
@@ -464,7 +464,12 @@ export const GitHub = () => {
|
|||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.location.href = data.install_url as string;
|
const installUrl = data.install_url as string;
|
||||||
|
if (installUrl && (installUrl.startsWith('https://github.com/') || installUrl.startsWith('https://api.github.com/'))) {
|
||||||
|
window.location.href = installUrl;
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid install URL received');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start GitHub App installation:', error);
|
console.error('Failed to start GitHub App installation:', error);
|
||||||
setBackupError(error instanceof Error ? error.message : 'Failed to start GitHub App installation');
|
setBackupError(error instanceof Error ? error.message : 'Failed to start GitHub App installation');
|
||||||
File renamed without changes.
@@ -112,12 +112,10 @@ export const Notes = () => {
|
|||||||
|
|
||||||
// Check if we should use demo mode or real API
|
// Check if we should use demo mode or real API
|
||||||
if (isDemoMode() && !shouldUseRealBackend()) {
|
if (isDemoMode() && !shouldUseRealBackend()) {
|
||||||
console.log('[Notes] Loading demo notes data');
|
|
||||||
// Load mock notes data for demo mode
|
// Load mock notes data for demo mode
|
||||||
const mockNotesData = getMockNotes();
|
const mockNotesData = getMockNotes();
|
||||||
notesData = mockNotesData;
|
notesData = mockNotesData;
|
||||||
} else {
|
} else {
|
||||||
console.log('[Notes] Loading notes from real API');
|
|
||||||
// Load from real API
|
// Load from real API
|
||||||
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
|
const token = localStorage.getItem('trackeep_token') || localStorage.getItem('token');
|
||||||
const response = await fetch(`${API_BASE_URL}/notes`, {
|
const response = await fetch(`${API_BASE_URL}/notes`, {
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -41,7 +41,11 @@ export const RemovedStuff = () => {
|
|||||||
// Load auto-remove settings from localStorage
|
// Load auto-remove settings from localStorage
|
||||||
const savedSettings = localStorage.getItem('autoRemoveSettings');
|
const savedSettings = localStorage.getItem('autoRemoveSettings');
|
||||||
if (savedSettings) {
|
if (savedSettings) {
|
||||||
setAutoRemoveSettings(JSON.parse(savedSettings));
|
try {
|
||||||
|
setAutoRemoveSettings(JSON.parse(savedSettings));
|
||||||
|
} catch {
|
||||||
|
console.warn('Failed to parse autoRemoveSettings');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from API first, then fallback to localStorage
|
// Try to load from API first, then fallback to localStorage
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,245 +0,0 @@
|
|||||||
// Update service for handling application updates
|
|
||||||
import { isDemoMode } from '@/lib/demo-mode';
|
|
||||||
|
|
||||||
export interface UpdateInfo {
|
|
||||||
version: string;
|
|
||||||
releaseNotes: string;
|
|
||||||
downloadUrl: string;
|
|
||||||
mandatory: boolean;
|
|
||||||
size: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateStatus {
|
|
||||||
available: boolean;
|
|
||||||
downloading: boolean;
|
|
||||||
installing: boolean;
|
|
||||||
completed: boolean;
|
|
||||||
error?: string;
|
|
||||||
progress: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateCheckResponse {
|
|
||||||
updateAvailable: boolean;
|
|
||||||
currentVersion: string;
|
|
||||||
latestVersion: string;
|
|
||||||
updateInfo?: UpdateInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
export const updateService = {
|
|
||||||
// Check for available updates
|
|
||||||
async checkForUpdates(): Promise<UpdateCheckResponse> {
|
|
||||||
// If in demo mode, return mock update data
|
|
||||||
if (isDemoMode()) {
|
|
||||||
return {
|
|
||||||
updateAvailable: true,
|
|
||||||
currentVersion: '1.0.0',
|
|
||||||
latestVersion: '1.0.1',
|
|
||||||
updateInfo: {
|
|
||||||
version: '1.0.1',
|
|
||||||
releaseNotes: '• New AI features added\n• Performance improvements\n• Bug fixes and security patches\n• Enhanced user interface',
|
|
||||||
downloadUrl: 'https://github.com/trackeep/trackeep/releases/latest',
|
|
||||||
mandatory: false,
|
|
||||||
size: '~25MB'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('[Real Mode] Checking for updates at:', `${API_BASE}/api/updates/check`);
|
|
||||||
const response = await fetch(`${API_BASE}/api/updates/check`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
throw new Error('Update check timed out');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Failed to check for updates:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Install an update
|
|
||||||
async installUpdate(version: string): Promise<{ message: string; version: string }> {
|
|
||||||
// If in demo mode, simulate update installation
|
|
||||||
if (isDemoMode()) {
|
|
||||||
return {
|
|
||||||
message: 'Update started',
|
|
||||||
version: version
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/api/updates/install`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ version }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to install update:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get update progress
|
|
||||||
async getUpdateProgress(): Promise<UpdateStatus> {
|
|
||||||
// If in demo mode, return mock progress
|
|
||||||
if (isDemoMode()) {
|
|
||||||
return {
|
|
||||||
available: true,
|
|
||||||
downloading: false,
|
|
||||||
installing: false,
|
|
||||||
completed: false,
|
|
||||||
error: '',
|
|
||||||
progress: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE}/api/updates/progress`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get update progress:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Get current app version from package.json
|
|
||||||
async getCurrentVersion(): Promise<string> {
|
|
||||||
// Try to get version from package.json first, then fallback
|
|
||||||
try {
|
|
||||||
const response = await fetch('/package.json');
|
|
||||||
if (response.ok) {
|
|
||||||
const packageJson = await response.json();
|
|
||||||
if (packageJson.version) {
|
|
||||||
console.log('Version from package.json:', packageJson.version);
|
|
||||||
return packageJson.version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Could not read package.json:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to environment variable or default
|
|
||||||
return import.meta.env.VITE_APP_VERSION || '1.2.5';
|
|
||||||
},
|
|
||||||
|
|
||||||
// Poll for update progress during installation
|
|
||||||
pollUpdateProgress(callback: (progress: UpdateStatus) => void, interval: number = 2000): () => void {
|
|
||||||
let isActive = true;
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
if (!isActive) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const progress = await this.getUpdateProgress();
|
|
||||||
callback(progress);
|
|
||||||
|
|
||||||
// Stop polling if update is completed or failed
|
|
||||||
if (progress.completed || progress.error) {
|
|
||||||
isActive = false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error polling update progress:', error);
|
|
||||||
isActive = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start polling
|
|
||||||
poll();
|
|
||||||
const intervalId = setInterval(poll, interval);
|
|
||||||
|
|
||||||
// Return cleanup function
|
|
||||||
return () => {
|
|
||||||
isActive = false;
|
|
||||||
clearInterval(intervalId);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// Simulate update progress for demo mode
|
|
||||||
simulateUpdateProgress(callback: (progress: UpdateStatus) => void): () => void {
|
|
||||||
let isActive = true;
|
|
||||||
let progress = 0;
|
|
||||||
let phase = 'downloading'; // 'downloading' -> 'installing' -> 'completed'
|
|
||||||
|
|
||||||
const simulate = () => {
|
|
||||||
if (!isActive) return;
|
|
||||||
|
|
||||||
progress += Math.random() * 15 + 5; // Random progress increment
|
|
||||||
|
|
||||||
if (progress >= 100) {
|
|
||||||
progress = 100;
|
|
||||||
if (phase === 'downloading') {
|
|
||||||
phase = 'installing';
|
|
||||||
progress = 0;
|
|
||||||
} else if (phase === 'installing') {
|
|
||||||
phase = 'completed';
|
|
||||||
isActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateStatus: UpdateStatus = {
|
|
||||||
available: true,
|
|
||||||
downloading: phase === 'downloading',
|
|
||||||
installing: phase === 'installing',
|
|
||||||
completed: phase === 'completed',
|
|
||||||
error: '',
|
|
||||||
progress: progress
|
|
||||||
};
|
|
||||||
|
|
||||||
callback(updateStatus);
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
const delay = phase === 'downloading' ? 500 : 1000; // Faster download, slower install
|
|
||||||
setTimeout(simulate, delay);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start simulation
|
|
||||||
simulate();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isActive = false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import { createSignal } from 'solid-js';
|
|
||||||
import { updateService, type UpdateInfo, type UpdateStatus } from '../services/updateService';
|
|
||||||
import { isDemoMode } from '@/lib/demo-mode';
|
|
||||||
|
|
||||||
// Global update state store
|
|
||||||
const [updateAvailable, setUpdateAvailable] = createSignal(false);
|
|
||||||
const [updateInfo, setUpdateInfo] = createSignal<UpdateInfo | null>(null);
|
|
||||||
const [updateStatus, setUpdateStatus] = createSignal<UpdateStatus>({
|
|
||||||
available: false,
|
|
||||||
downloading: false,
|
|
||||||
installing: false,
|
|
||||||
completed: false,
|
|
||||||
progress: 0
|
|
||||||
});
|
|
||||||
const [isChecking, setIsChecking] = createSignal(false);
|
|
||||||
const [error, setError] = createSignal<string | null>(null);
|
|
||||||
const [currentVersion, setCurrentVersion] = createSignal('1.0.0');
|
|
||||||
const [lastCheckTime, setLastCheckTime] = createSignal<number>(0);
|
|
||||||
|
|
||||||
let pollCleanup: (() => void) | null = null;
|
|
||||||
let checkInterval: number | null = null;
|
|
||||||
|
|
||||||
let checkInProgress = false;
|
|
||||||
|
|
||||||
// Check for updates
|
|
||||||
const checkForUpdates = async () => {
|
|
||||||
// Prevent multiple simultaneous checks with both signal and flag
|
|
||||||
if (isChecking() || checkInProgress) return;
|
|
||||||
|
|
||||||
checkInProgress = true;
|
|
||||||
setIsChecking(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await updateService.checkForUpdates();
|
|
||||||
setUpdateAvailable(response.updateAvailable);
|
|
||||||
setUpdateInfo(response.updateInfo || null);
|
|
||||||
setCurrentVersion(response.currentVersion);
|
|
||||||
setLastCheckTime(Date.now());
|
|
||||||
|
|
||||||
// Save last check time to localStorage
|
|
||||||
localStorage.setItem('lastUpdateCheck', Date.now().toString());
|
|
||||||
|
|
||||||
if (response.updateAvailable && response.updateInfo) {
|
|
||||||
setUpdateStatus(prev => ({ ...prev, available: true }));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to check for updates:', err);
|
|
||||||
setError('Failed to check for updates');
|
|
||||||
} finally {
|
|
||||||
setIsChecking(false);
|
|
||||||
checkInProgress = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Install update
|
|
||||||
const installUpdate = async () => {
|
|
||||||
if (!updateInfo()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setError(null);
|
|
||||||
await updateService.installUpdate(updateInfo()!.version);
|
|
||||||
|
|
||||||
// Start polling for progress or simulation in demo mode
|
|
||||||
if (isDemoMode()) {
|
|
||||||
pollCleanup = updateService.simulateUpdateProgress((progress: UpdateStatus) => {
|
|
||||||
setUpdateStatus(progress);
|
|
||||||
|
|
||||||
if (progress.completed) {
|
|
||||||
// Show success notification or trigger reload
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress.error) {
|
|
||||||
setError(progress.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
pollCleanup = updateService.pollUpdateProgress((progress: UpdateStatus) => {
|
|
||||||
setUpdateStatus(progress);
|
|
||||||
|
|
||||||
if (progress.completed) {
|
|
||||||
// Show success notification or trigger reload
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress.error) {
|
|
||||||
setError(progress.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to install update:', err);
|
|
||||||
setError('Failed to install update');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cancel update
|
|
||||||
const cancelUpdate = () => {
|
|
||||||
if (pollCleanup) {
|
|
||||||
pollCleanup();
|
|
||||||
pollCleanup = null;
|
|
||||||
}
|
|
||||||
setUpdateStatus({
|
|
||||||
available: updateAvailable(),
|
|
||||||
downloading: false,
|
|
||||||
installing: false,
|
|
||||||
completed: false,
|
|
||||||
progress: 0
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize update checking
|
|
||||||
const initializeUpdateChecking = async () => {
|
|
||||||
// Set current version
|
|
||||||
setCurrentVersion(await updateService.getCurrentVersion());
|
|
||||||
|
|
||||||
// Check if last check was more than 24 hours ago
|
|
||||||
const lastCheckTimeStr = localStorage.getItem('lastUpdateCheck');
|
|
||||||
const now = Date.now();
|
|
||||||
const twentyFourHours = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
if (!lastCheckTimeStr || (now - parseInt(lastCheckTimeStr)) > twentyFourHours) {
|
|
||||||
// Check for updates on initialization if it's been more than 24 hours
|
|
||||||
checkForUpdates();
|
|
||||||
} else {
|
|
||||||
setLastCheckTime(parseInt(lastCheckTimeStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up periodic checking (every 24 hours)
|
|
||||||
checkInterval = setInterval(checkForUpdates, twentyFourHours);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
const cleanup = () => {
|
|
||||||
if (checkInterval) {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
checkInterval = null;
|
|
||||||
}
|
|
||||||
if (pollCleanup) {
|
|
||||||
pollCleanup();
|
|
||||||
pollCleanup = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-initialize when store is imported
|
|
||||||
let initialized = false;
|
|
||||||
const ensureInitialized = async () => {
|
|
||||||
if (!initialized) {
|
|
||||||
await initializeUpdateChecking();
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Export store functions and signals
|
|
||||||
export const updateStore = {
|
|
||||||
// Signals
|
|
||||||
updateAvailable,
|
|
||||||
updateInfo,
|
|
||||||
updateStatus,
|
|
||||||
isChecking,
|
|
||||||
error,
|
|
||||||
currentVersion,
|
|
||||||
lastCheckTime,
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
checkForUpdates,
|
|
||||||
installUpdate,
|
|
||||||
cancelUpdate,
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
ensureInitialized,
|
|
||||||
cleanup
|
|
||||||
};
|
|
||||||
|
|
||||||
// Auto-cleanup on page unload
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('beforeunload', cleanup);
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
|||||||
|
{"files": {"code": ["/home/tdvorak/Desktop/PROG+HTML/Trackeep/browser-extension/background.js", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/browser-extension/popup.js", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/browser-extension/options.js", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/browser-extension/youtube-content.js", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/browser-extension/browser-polyfill.js", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/index.ts", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/App.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/types.ts", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/MainTabs.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/TasksScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/NotesScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/SettingsScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/WebAppScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/FilesScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/ConnectionSetupScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/TimeEntriesScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/screens/AuthScreen.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/components/UI.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/context/AppContext.tsx", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/lib/format.ts", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/lib/api.ts", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/lib/url.ts", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/mobile/src/lib/share.ts", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/main.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/seed.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/examples/user_service_example.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/tools/favicon_cli.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/youtube_channels.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/message_detector.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/youtube_cache.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/favicon_fetcher.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/performance.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/ai_recommendation_service.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/metadata.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/favicon_fetcher_test.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/youtube.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/ai_service.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/computer_vision_service.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/messages_realtime.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/video_bookmark.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/youtube_integrated.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/services/message_detector_test.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/ai_features.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/search_settings.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/community.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/audit_log.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/note.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/user.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/web_scraping.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/task.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/knowledge_base.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/teams.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/analytics.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/bookmark.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/messages.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/learning_path.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/file.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/browser_extension.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/chat.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/tag.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/github_user_auth.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/youtube_cache.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/goals_habits.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/integration.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/models.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/backend/models/file_analysis.go", "/home/tdvorak/Desktop/PROG+HTML/Trackeep/bLine truncated
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/usr/bin/python3
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/home/tdvorak/Desktop/PROG+HTML/Trackeep
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
# Graph Report - Trackeep (2026-04-30)
|
||||||
|
|
||||||
|
## Corpus Check
|
||||||
|
- 330 files · ~395,125 words
|
||||||
|
- Verdict: corpus is large enough that graph structure adds value.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- 2549 nodes · 3694 edges · 99 communities detected
|
||||||
|
- Extraction: 85% EXTRACTED · 15% INFERRED · 0% AMBIGUOUS · INFERRED: 562 edges (avg confidence: 0.8)
|
||||||
|
- Token cost: 0 input · 0 output
|
||||||
|
|
||||||
|
## Community Hubs (Navigation)
|
||||||
|
- [[_COMMUNITY_Community 0|Community 0]]
|
||||||
|
- [[_COMMUNITY_Community 1|Community 1]]
|
||||||
|
- [[_COMMUNITY_Community 2|Community 2]]
|
||||||
|
- [[_COMMUNITY_Community 3|Community 3]]
|
||||||
|
- [[_COMMUNITY_Community 4|Community 4]]
|
||||||
|
- [[_COMMUNITY_Community 5|Community 5]]
|
||||||
|
- [[_COMMUNITY_Community 6|Community 6]]
|
||||||
|
- [[_COMMUNITY_Community 7|Community 7]]
|
||||||
|
- [[_COMMUNITY_Community 8|Community 8]]
|
||||||
|
- [[_COMMUNITY_Community 9|Community 9]]
|
||||||
|
- [[_COMMUNITY_Community 10|Community 10]]
|
||||||
|
- [[_COMMUNITY_Community 11|Community 11]]
|
||||||
|
- [[_COMMUNITY_Community 12|Community 12]]
|
||||||
|
- [[_COMMUNITY_Community 13|Community 13]]
|
||||||
|
- [[_COMMUNITY_Community 14|Community 14]]
|
||||||
|
- [[_COMMUNITY_Community 15|Community 15]]
|
||||||
|
- [[_COMMUNITY_Community 16|Community 16]]
|
||||||
|
- [[_COMMUNITY_Community 17|Community 17]]
|
||||||
|
- [[_COMMUNITY_Community 18|Community 18]]
|
||||||
|
- [[_COMMUNITY_Community 19|Community 19]]
|
||||||
|
- [[_COMMUNITY_Community 20|Community 20]]
|
||||||
|
- [[_COMMUNITY_Community 21|Community 21]]
|
||||||
|
- [[_COMMUNITY_Community 22|Community 22]]
|
||||||
|
- [[_COMMUNITY_Community 23|Community 23]]
|
||||||
|
- [[_COMMUNITY_Community 24|Community 24]]
|
||||||
|
- [[_COMMUNITY_Community 25|Community 25]]
|
||||||
|
- [[_COMMUNITY_Community 26|Community 26]]
|
||||||
|
- [[_COMMUNITY_Community 27|Community 27]]
|
||||||
|
- [[_COMMUNITY_Community 28|Community 28]]
|
||||||
|
- [[_COMMUNITY_Community 29|Community 29]]
|
||||||
|
- [[_COMMUNITY_Community 30|Community 30]]
|
||||||
|
- [[_COMMUNITY_Community 31|Community 31]]
|
||||||
|
- [[_COMMUNITY_Community 32|Community 32]]
|
||||||
|
- [[_COMMUNITY_Community 33|Community 33]]
|
||||||
|
- [[_COMMUNITY_Community 34|Community 34]]
|
||||||
|
- [[_COMMUNITY_Community 35|Community 35]]
|
||||||
|
- [[_COMMUNITY_Community 36|Community 36]]
|
||||||
|
- [[_COMMUNITY_Community 37|Community 37]]
|
||||||
|
- [[_COMMUNITY_Community 38|Community 38]]
|
||||||
|
- [[_COMMUNITY_Community 39|Community 39]]
|
||||||
|
- [[_COMMUNITY_Community 40|Community 40]]
|
||||||
|
- [[_COMMUNITY_Community 41|Community 41]]
|
||||||
|
- [[_COMMUNITY_Community 42|Community 42]]
|
||||||
|
- [[_COMMUNITY_Community 43|Community 43]]
|
||||||
|
- [[_COMMUNITY_Community 44|Community 44]]
|
||||||
|
- [[_COMMUNITY_Community 45|Community 45]]
|
||||||
|
- [[_COMMUNITY_Community 46|Community 46]]
|
||||||
|
- [[_COMMUNITY_Community 47|Community 47]]
|
||||||
|
- [[_COMMUNITY_Community 48|Community 48]]
|
||||||
|
- [[_COMMUNITY_Community 49|Community 49]]
|
||||||
|
- [[_COMMUNITY_Community 50|Community 50]]
|
||||||
|
- [[_COMMUNITY_Community 51|Community 51]]
|
||||||
|
- [[_COMMUNITY_Community 52|Community 52]]
|
||||||
|
- [[_COMMUNITY_Community 53|Community 53]]
|
||||||
|
- [[_COMMUNITY_Community 54|Community 54]]
|
||||||
|
- [[_COMMUNITY_Community 55|Community 55]]
|
||||||
|
- [[_COMMUNITY_Community 56|Community 56]]
|
||||||
|
- [[_COMMUNITY_Community 57|Community 57]]
|
||||||
|
- [[_COMMUNITY_Community 58|Community 58]]
|
||||||
|
- [[_COMMUNITY_Community 60|Community 60]]
|
||||||
|
- [[_COMMUNITY_Community 61|Community 61]]
|
||||||
|
- [[_COMMUNITY_Community 62|Community 62]]
|
||||||
|
- [[_COMMUNITY_Community 63|Community 63]]
|
||||||
|
- [[_COMMUNITY_Community 64|Community 64]]
|
||||||
|
- [[_COMMUNITY_Community 66|Community 66]]
|
||||||
|
- [[_COMMUNITY_Community 67|Community 67]]
|
||||||
|
- [[_COMMUNITY_Community 68|Community 68]]
|
||||||
|
- [[_COMMUNITY_Community 69|Community 69]]
|
||||||
|
- [[_COMMUNITY_Community 70|Community 70]]
|
||||||
|
- [[_COMMUNITY_Community 71|Community 71]]
|
||||||
|
- [[_COMMUNITY_Community 74|Community 74]]
|
||||||
|
- [[_COMMUNITY_Community 75|Community 75]]
|
||||||
|
- [[_COMMUNITY_Community 77|Community 77]]
|
||||||
|
- [[_COMMUNITY_Community 79|Community 79]]
|
||||||
|
- [[_COMMUNITY_Community 81|Community 81]]
|
||||||
|
- [[_COMMUNITY_Community 83|Community 83]]
|
||||||
|
- [[_COMMUNITY_Community 86|Community 86]]
|
||||||
|
- [[_COMMUNITY_Community 87|Community 87]]
|
||||||
|
- [[_COMMUNITY_Community 95|Community 95]]
|
||||||
|
- [[_COMMUNITY_Community 96|Community 96]]
|
||||||
|
- [[_COMMUNITY_Community 97|Community 97]]
|
||||||
|
- [[_COMMUNITY_Community 99|Community 99]]
|
||||||
|
- [[_COMMUNITY_Community 101|Community 101]]
|
||||||
|
- [[_COMMUNITY_Community 102|Community 102]]
|
||||||
|
- [[_COMMUNITY_Community 103|Community 103]]
|
||||||
|
- [[_COMMUNITY_Community 104|Community 104]]
|
||||||
|
- [[_COMMUNITY_Community 105|Community 105]]
|
||||||
|
- [[_COMMUNITY_Community 106|Community 106]]
|
||||||
|
- [[_COMMUNITY_Community 112|Community 112]]
|
||||||
|
- [[_COMMUNITY_Community 119|Community 119]]
|
||||||
|
- [[_COMMUNITY_Community 120|Community 120]]
|
||||||
|
- [[_COMMUNITY_Community 130|Community 130]]
|
||||||
|
- [[_COMMUNITY_Community 131|Community 131]]
|
||||||
|
- [[_COMMUNITY_Community 132|Community 132]]
|
||||||
|
- [[_COMMUNITY_Community 133|Community 133]]
|
||||||
|
- [[_COMMUNITY_Community 134|Community 134]]
|
||||||
|
- [[_COMMUNITY_Community 135|Community 135]]
|
||||||
|
- [[_COMMUNITY_Community 136|Community 136]]
|
||||||
|
- [[_COMMUNITY_Community 138|Community 138]]
|
||||||
|
|
||||||
|
## God Nodes (most connected - your core abstractions)
|
||||||
|
1. `GetDB()` - 95 edges
|
||||||
|
2. `New()` - 50 edges
|
||||||
|
3. `contains()` - 42 edges
|
||||||
|
4. `main()` - 41 edges
|
||||||
|
5. `toLower()` - 34 edges
|
||||||
|
6. `demoFetch()` - 32 edges
|
||||||
|
7. `Queries` - 28 edges
|
||||||
|
8. `IntegrationHandler` - 26 edges
|
||||||
|
9. `setError()` - 25 edges
|
||||||
|
10. `getAuthUserID()` - 24 edges
|
||||||
|
|
||||||
|
## Surprising Connections (you probably didn't know these)
|
||||||
|
- `createTask()` --calls--> `setError()` [INFERRED]
|
||||||
|
mobile/src/screens/TasksScreen.tsx → desktop/src/main.js
|
||||||
|
- `toggleTaskStatus()` --calls--> `setError()` [INFERRED]
|
||||||
|
mobile/src/screens/TasksScreen.tsx → desktop/src/main.js
|
||||||
|
- `deleteTask()` --calls--> `setError()` [INFERRED]
|
||||||
|
mobile/src/screens/TasksScreen.tsx → desktop/src/main.js
|
||||||
|
- `startEntry()` --calls--> `setError()` [INFERRED]
|
||||||
|
mobile/src/screens/TimeEntriesScreen.tsx → desktop/src/main.js
|
||||||
|
- `stopEntry()` --calls--> `setError()` [INFERRED]
|
||||||
|
mobile/src/screens/TimeEntriesScreen.tsx → desktop/src/main.js
|
||||||
|
|
||||||
|
## Communities
|
||||||
|
|
||||||
|
### Community 0 - "Community 0"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (147): DB, NewDB(), centralizedOAuthUser, buildControlServiceCallbackURL(), buildControlServiceGitHubStartURL(), buildGitHubAppInstallCallbackURL(), clearControlServiceAuthFlowState(), controlServiceClient() (+139 more)
|
||||||
|
|
||||||
|
### Community 1 - "Community 1"
|
||||||
|
Cohesion: 0.02
|
||||||
|
Nodes (74): applySmartSuggestions(), detectAndApplySmartData(), handleQuickSave(), hideMessage(), saveBookmark(), setButtonLoading(), showMessage(), uploadFile() (+66 more)
|
||||||
|
|
||||||
|
### Community 2 - "Community 2"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (62): handleDemoMode(), ProtectedRoute(), DemoStatus(), Header(), getAuthHeaders(), isDemoMode(), useAuth(), DemoModeApiClient (+54 more)
|
||||||
|
|
||||||
|
### Community 3 - "Community 3"
|
||||||
|
Cohesion: 0.02
|
||||||
|
Nodes (53): initializeDragonflyDB(), main(), AppConfig, Config, getDurationEnv(), getEnvWithDefault(), Load(), DatabaseConfig (+45 more)
|
||||||
|
|
||||||
|
### Community 4 - "Community 4"
|
||||||
|
Cohesion: 0.03
|
||||||
|
Nodes (84): SeedData(), GetDB(), AdminDeleteLearningPath(), AdminGetAllLearningPaths(), AdminGetStats(), AdminGetUsers(), AdminMiddleware(), AdminReviewLearningPath() (+76 more)
|
||||||
|
|
||||||
|
### Community 5 - "Community 5"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (78): AddConversationMemberRequest, AdminCreateUser(), AttachmentInput, conversationListItem, CreateConversationRequest, CreateMessageRequest, CreateReactionRequest, CreateVaultItemRequest (+70 more)
|
||||||
|
|
||||||
|
### Community 6 - "Community 6"
|
||||||
|
Cohesion: 0.04
|
||||||
|
Nodes (46): AcceptTaskSuggestion(), buildTaskContext(), generateAIContent(), generateAISummary(), GenerateContent(), GenerateTagSuggestions(), generateTaskSuggestions(), GetAIProviders() (+38 more)
|
||||||
|
|
||||||
|
### Community 7 - "Community 7"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (45): disableTOTP(), enableTOTP(), fetchTOTPStatus(), getAuthHeaders(), regenerateBackupCodes(), setupTOTP(), verifyBackupCode(), verifyTOTPCode() (+37 more)
|
||||||
|
|
||||||
|
### Community 8 - "Community 8"
|
||||||
|
Cohesion: 0.07
|
||||||
|
Nodes (60): Ae(), ar(), at(), Bt(), children(), cr(), Ct(), de() (+52 more)
|
||||||
|
|
||||||
|
### Community 9 - "Community 9"
|
||||||
|
Cohesion: 0.04
|
||||||
|
Nodes (50): GetAISettings(), getDefaultAISettings(), isMasked(), UpdateAISettings(), AISettings, AuthMiddleware(), ChangePassword(), CheckUsers() (+42 more)
|
||||||
|
|
||||||
|
### Community 10 - "Community 10"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (33): GetChannelVideos(), GetFireshipVideos(), GetNetworkChuckVideos(), GetYouTubeChannelVideosFromURL(), GetYouTubeTrending(), SearchYouTube(), YouTubeSearchTest(), YouTubeChannelRequest (+25 more)
|
||||||
|
|
||||||
|
### Community 11 - "Community 11"
|
||||||
|
Cohesion: 0.05
|
||||||
|
Nodes (27): downloadUpdate(), containsMaliciousContent(), InputValidationMiddleware(), sanitizeInput(), ValidateRequestBody(), generateMemoryCacheKey(), InvalidateMemoryCache(), MemoryCacheInvalidationMiddleware() (+19 more)
|
||||||
|
|
||||||
|
### Community 12 - "Community 12"
|
||||||
|
Cohesion: 0.06
|
||||||
|
Nodes (28): initializeSecuritySecrets(), GenerateAPIKey(), generateRandomString(), createFileShareRequest, buildPublicShareURL(), CreateFileShare(), determineFileType(), generateSecureShareToken() (+20 more)
|
||||||
|
|
||||||
|
### Community 13 - "Community 13"
|
||||||
|
Cohesion: 0.08
|
||||||
|
Nodes (41): getControlServiceTokenForUser(), CreateEncryptedNote(), DecryptNoteContent(), determineFileTypeForEncryption(), DownloadEncryptedFile(), EncryptNoteContent(), generateRandomStringForFile(), GetEncryptedNote() (+33 more)
|
||||||
|
|
||||||
|
### Community 14 - "Community 14"
|
||||||
|
Cohesion: 0.06
|
||||||
|
Nodes (18): InitDatabase(), shouldRunLegacySQLMigrations(), InitLogger(), Logger(), logPerformanceEvent(), logRequestBody(), logSecurityEvent(), PerformanceLogger() (+10 more)
|
||||||
|
|
||||||
|
### Community 15 - "Community 15"
|
||||||
|
Cohesion: 0.1
|
||||||
|
Nodes (24): getApiOrigin(), getApiV1BaseUrl(), trimApiSuffix(), trimTrailingSlash(), getAuthCallbackUrl(), startGitHubSignIn(), backupSelectedRepositories(), checkGitHubConnection() (+16 more)
|
||||||
|
|
||||||
|
### Community 16 - "Community 16"
|
||||||
|
Cohesion: 0.07
|
||||||
|
Nodes (12): calculateFocusScore(), NewAnalyticsHandler(), AnalyticsHandler, getIntValue(), getStringValue(), logSearchAnalytics(), min(), performEnhancedSearch() (+4 more)
|
||||||
|
|
||||||
|
### Community 17 - "Community 17"
|
||||||
|
Cohesion: 0.1
|
||||||
|
Nodes (23): progressWriter, UpdateRequest, backupUserData(), broadcastProgress(), CheckForUpdates(), checkForUpdatesWithDocker(), checkForUpdatesWithDockerRegistry(), copyDirectory() (+15 more)
|
||||||
|
|
||||||
|
### Community 18 - "Community 18"
|
||||||
|
Cohesion: 0.1
|
||||||
|
Nodes (25): BraveNewsResponse, BraveSearchResponse, BraveSearchResult, SearchNews(), SearchWeb(), getBoolEnvWithDefault(), getDefaultSearchSettings(), getEnvWithDefault() (+17 more)
|
||||||
|
|
||||||
|
### Community 19 - "Community 19"
|
||||||
|
Cohesion: 0.07
|
||||||
|
Nodes (2): DBTX, Queries
|
||||||
|
|
||||||
|
### Community 20 - "Community 20"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (6): NewAIRecommendationHandler(), AIRecommendationHandler, NewAIRecommendationService(), AIRecommendationService, RecommendationRequest, RecommendationScore
|
||||||
|
|
||||||
|
### Community 21 - "Community 21"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (10): BoundingBox, ComputerVisionService, DocumentAnalysis, DocumentSection, DocumentTable, FaceDetection, ImageAnalysisRequest, ImageAnalysisResponse (+2 more)
|
||||||
|
|
||||||
|
### Community 22 - "Community 22"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (6): NewVideoBookmarkHandler(), VideoBookmarkHandler, SaveVideoRequest, NewVideoBookmarkService(), VideoBookmarkService, VideoInfo
|
||||||
|
|
||||||
|
### Community 23 - "Community 23"
|
||||||
|
Cohesion: 0.13
|
||||||
|
Nodes (1): IntegrationHandler
|
||||||
|
|
||||||
|
### Community 24 - "Community 24"
|
||||||
|
Cohesion: 0.09
|
||||||
|
Nodes (5): adaptBookmarkFromApi(), editBookmark(), handleAddBookmark(), handleEditBookmark(), handleSubmit()
|
||||||
|
|
||||||
|
### Community 25 - "Community 25"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (10): handlePageChange(), stats(), storagePercentage(), taskCompletionRate(), totalPages(), weeklyActivityTotal(), handleRefresh(), loadStats() (+2 more)
|
||||||
|
|
||||||
|
### Community 26 - "Community 26"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (11): containsString(), GetAllFavicons(), GetFavicon(), NewFaviconFetcher(), BenchmarkFaviconFetch(), TestExtractHeadSection(), TestFaviconFetcher(), TestMakeAbsoluteURL() (+3 more)
|
||||||
|
|
||||||
|
### Community 27 - "Community 27"
|
||||||
|
Cohesion: 0.13
|
||||||
|
Nodes (7): fetchWithAuth(), getAuthToken(), handleBackupSelectedRepos(), handleInstallGitHubApp(), loadFiles(), loadGitHubBackupWorkspace(), parseRepoPayload()
|
||||||
|
|
||||||
|
### Community 28 - "Community 28"
|
||||||
|
Cohesion: 0.1
|
||||||
|
Nodes (12): Challenge, ChallengeMilestone, ChallengeMilestoneCompletion, ChallengeParticipant, ChallengeResource, ChallengeTag, ChallengeTeam, Mentorship (+4 more)
|
||||||
|
|
||||||
|
### Community 29 - "Community 29"
|
||||||
|
Cohesion: 0.16
|
||||||
|
Nodes (14): getAuthHeaders(), getQuickSearchSuggestions(), searchBrave(), searchNews(), searchWeb(), extractVideoId(), getVideoInfo(), handleKeyPress() (+6 more)
|
||||||
|
|
||||||
|
### Community 30 - "Community 30"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (3): looksLikeYouTube(), saveBookmark(), submitManualShare()
|
||||||
|
|
||||||
|
### Community 31 - "Community 31"
|
||||||
|
Cohesion: 0.13
|
||||||
|
Nodes (10): hexToHsl(), isChatOpen(), toggleChat(), applyCustomColors(), applyScheme(), resetColors(), toggleDarkMode(), updateSchemesForTheme() (+2 more)
|
||||||
|
|
||||||
|
### Community 32 - "Community 32"
|
||||||
|
Cohesion: 0.11
|
||||||
|
Nodes (9): Analytics, AnalyticsReport, ContentAnalytics, GitHubAnalytics, Goal, HabitAnalytics, LearningAnalytics, Milestone (+1 more)
|
||||||
|
|
||||||
|
### Community 33 - "Community 33"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (17): GenerateEmbeddingRequest, GenerateEmbeddingResponse, buildSemanticSearchResult(), compactSemanticText(), cosineSimilarity(), findSimilarContent(), GenerateEmbedding(), generateHighlights() (+9 more)
|
||||||
|
|
||||||
|
### Community 34 - "Community 34"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (16): DiscordConfig, GitHubConfig, GoogleConfig, Integration, IntegrationConfig, IntegrationStatus, IntegrationType, NotionConfig (+8 more)
|
||||||
|
|
||||||
|
### Community 35 - "Community 35"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (3): calculateMatchScore(), NewCommunityHandler(), CommunityHandler
|
||||||
|
|
||||||
|
### Community 36 - "Community 36"
|
||||||
|
Cohesion: 0.13
|
||||||
|
Nodes (4): Metrics, copyDurationMap(), copyMap(), GetMetrics()
|
||||||
|
|
||||||
|
### Community 37 - "Community 37"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (6): isSupported(), triggerHaptic(), useHaptics(), Messages(), Profile(), Button()
|
||||||
|
|
||||||
|
### Community 38 - "Community 38"
|
||||||
|
Cohesion: 0.2
|
||||||
|
Nodes (10): completeSetup(), generateApiKey(), saveSettings(), setButtonLoading(), showConnectionStatus(), showMessage(), showSetupConnectionStatus(), testConnection() (+2 more)
|
||||||
|
|
||||||
|
### Community 39 - "Community 39"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (2): NewGoalsHabitsHandler(), GoalsHabitsHandler
|
||||||
|
|
||||||
|
### Community 40 - "Community 40"
|
||||||
|
Cohesion: 0.12
|
||||||
|
Nodes (2): NewMarketplaceHandler(), MarketplaceHandler
|
||||||
|
|
||||||
|
### Community 41 - "Community 41"
|
||||||
|
Cohesion: 0.19
|
||||||
|
Nodes (8): generateCalendarDays(), getCellClass(), getDateClass(), getDaysInMonth(), getFirstDayOfMonth(), isDateEnd(), isDateInRange(), isDateStart()
|
||||||
|
|
||||||
|
### Community 42 - "Community 42"
|
||||||
|
Cohesion: 0.23
|
||||||
|
Nodes (11): closeCreateWorkspaceModal(), createDefaultWorkspace(), getAuthToken(), getWorkspaceIcon(), handleCreateWorkspace(), handleWorkspaceSelect(), loadWorkspaces(), normalizeWorkspace() (+3 more)
|
||||||
|
|
||||||
|
### Community 43 - "Community 43"
|
||||||
|
Cohesion: 0.27
|
||||||
|
Nodes (14): cleanupHistory(), detectContentType(), getTrackeepConfig(), getYouTubeHistory(), openPopupWithContext(), parseResponseError(), parseYouTubeVideoMeta(), saveYouTubeBookmark() (+6 more)
|
||||||
|
|
||||||
|
### Community 44 - "Community 44"
|
||||||
|
Cohesion: 0.13
|
||||||
|
Nodes (1): Validator
|
||||||
|
|
||||||
|
### Community 45 - "Community 45"
|
||||||
|
Cohesion: 0.2
|
||||||
|
Nodes (9): Category, estimateReadingTime(), generateSlug(), Template, TemplateVariable, WikiAttachment, WikiBacklink, WikiPage (+1 more)
|
||||||
|
|
||||||
|
### Community 46 - "Community 46"
|
||||||
|
Cohesion: 0.19
|
||||||
|
Nodes (6): generateId(), getFileExtension(), handleDrop(), handleFileSelect(), handleUrlImport(), simulateUpload()
|
||||||
|
|
||||||
|
### Community 47 - "Community 47"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (6): GoalTag, GoalTemplate, GoalTemplateMilestone, Habit, HabitEntry, HabitTag
|
||||||
|
|
||||||
|
### Community 48 - "Community 48"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (2): canUpload(), isValidUrl()
|
||||||
|
|
||||||
|
### Community 49 - "Community 49"
|
||||||
|
Cohesion: 0.24
|
||||||
|
Nodes (8): handleHexChange(), handleMouseMove(), handleSavedColorClick(), handleSliderMouseDown(), hexToHSL(), hslToHex(), updateColorFromHue(), updateHueFromPosition()
|
||||||
|
|
||||||
|
### Community 50 - "Community 50"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (6): fetchSessions(), getProviderFromModel(), getToken(), handleSendMessage(), loadSessionMessages(), scrollToHighlightedMessage()
|
||||||
|
|
||||||
|
### Community 51 - "Community 51"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (11): Team, TeamActivity, TeamBookmark, TeamFile, TeamInvitation, TeamMember, TeamNote, TeamProject (+3 more)
|
||||||
|
|
||||||
|
### Community 52 - "Community 52"
|
||||||
|
Cohesion: 0.17
|
||||||
|
Nodes (11): AuditLog, Bookmark, BookmarkTag, File, FileTag, Note, NoteTag, Tag (+3 more)
|
||||||
|
|
||||||
|
### Community 53 - "Community 53"
|
||||||
|
Cohesion: 0.2
|
||||||
|
Nodes (6): ContentShare, generateShareToken(), MarketplaceItem, MarketplacePurchase, MarketplaceReview, MarketplaceTag
|
||||||
|
|
||||||
|
### Community 54 - "Community 54"
|
||||||
|
Cohesion: 0.18
|
||||||
|
Nodes (10): AddTaskTagParams, CreateTaskParams, DeleteTaskParams, GetTaskByIDParams, GetTasksByStatusParams, GetTasksByTagParams, GetTasksByUserParams, RemoveTaskTagParams (+2 more)
|
||||||
|
|
||||||
|
### Community 55 - "Community 55"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (4): getMockCalendarEvents(), createEvent(), fetchCalendarData(), toggleEventCompletion()
|
||||||
|
|
||||||
|
### Community 56 - "Community 56"
|
||||||
|
Cohesion: 0.2
|
||||||
|
Nodes (3): CalendarEvent, CalendarSettings, RecurrenceRule
|
||||||
|
|
||||||
|
### Community 57 - "Community 57"
|
||||||
|
Cohesion: 0.2
|
||||||
|
Nodes (9): AddBookmarkTagParams, CreateBookmarkParams, DeleteBookmarkParams, GetBookmarkByIDParams, GetBookmarksByTagParams, GetBookmarksByUserParams, RemoveBookmarkTagParams, SearchBookmarksParams (+1 more)
|
||||||
|
|
||||||
|
### Community 58 - "Community 58"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (6): getToken(), handleAddMember(), handleDeleteMember(), loadMembers(), onWorkspaceChanged(), resolveWorkspaceId()
|
||||||
|
|
||||||
|
### Community 60 - "Community 60"
|
||||||
|
Cohesion: 0.31
|
||||||
|
Nodes (4): apiRequest(), getToken(), MessagesRealtimeClient, uploadChatFile()
|
||||||
|
|
||||||
|
### Community 61 - "Community 61"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (3): AIRecommendation, RecommendationInteraction, UserPreference
|
||||||
|
|
||||||
|
### Community 62 - "Community 62"
|
||||||
|
Cohesion: 0.22
|
||||||
|
Nodes (8): CreateUserParams, CreateUserRow, GetUserByEmailRow, GetUserByIDRow, ListUsersParams, ListUsersRow, UpdateUserParams, UpdateUserRow
|
||||||
|
|
||||||
|
### Community 63 - "Community 63"
|
||||||
|
Cohesion: 0.46
|
||||||
|
Nodes (7): closePrompt(), detectAndNotify(), escapeHtml(), initDetection(), parseYouTubeVideo(), renderPrompt(), sendMessage()
|
||||||
|
|
||||||
|
### Community 64 - "Community 64"
|
||||||
|
Cohesion: 0.25
|
||||||
|
Nodes (5): ScrapedContent, ScrapedImage, ScrapedLink, ScrapedVideo, ScrapingJob
|
||||||
|
|
||||||
|
### Community 66 - "Community 66"
|
||||||
|
Cohesion: 0.48
|
||||||
|
Nodes (5): buildShareDraft(), firstCandidateTitleFromText(), firstUrlFromText(), normalizeUrl(), titleFromUrl()
|
||||||
|
|
||||||
|
### Community 67 - "Community 67"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (1): UserServiceExample
|
||||||
|
|
||||||
|
### Community 68 - "Community 68"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (6): AICodeReview, AIContentGeneration, AILearningRecommendation, AISummary, AITagSuggestion, AITaskSuggestion
|
||||||
|
|
||||||
|
### Community 69 - "Community 69"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (6): Follow, Project, ProjectTag, Skill, SocialLink, UserProfileStats
|
||||||
|
|
||||||
|
### Community 70 - "Community 70"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (1): TimeEntry
|
||||||
|
|
||||||
|
### Community 71 - "Community 71"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (5): ContentEmbedding, SavedSearch, SavedSearchTag, SearchAnalytics, SearchSuggestion
|
||||||
|
|
||||||
|
### Community 74 - "Community 74"
|
||||||
|
Cohesion: 0.29
|
||||||
|
Nodes (2): importData(), ExportImport()
|
||||||
|
|
||||||
|
### Community 75 - "Community 75"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (2): callAIAPI(), handleSendMessage()
|
||||||
|
|
||||||
|
### Community 77 - "Community 77"
|
||||||
|
Cohesion: 0.33
|
||||||
|
Nodes (5): Enrollment, LearningModule, LearningPath, ModuleResource, Progress
|
||||||
|
|
||||||
|
### Community 79 - "Community 79"
|
||||||
|
Cohesion: 0.53
|
||||||
|
Nodes (4): addTag(), filteredTags(), handleInputKeyDown(), removeTag()
|
||||||
|
|
||||||
|
### Community 81 - "Community 81"
|
||||||
|
Cohesion: 0.47
|
||||||
|
Nodes (3): CodeBlock(), inferCodeLanguage(), normalizeCodeLanguage()
|
||||||
|
|
||||||
|
### Community 83 - "Community 83"
|
||||||
|
Cohesion: 0.6
|
||||||
|
Nodes (5): breakDownDuration(), formatDuration(), formatDurationDetailed(), formatDurationShort(), getLargestTimeUnit()
|
||||||
|
|
||||||
|
### Community 86 - "Community 86"
|
||||||
|
Cohesion: 0.4
|
||||||
|
Nodes (1): GracefulShutdown
|
||||||
|
|
||||||
|
### Community 87 - "Community 87"
|
||||||
|
Cohesion: 0.4
|
||||||
|
Nodes (1): ErrorResponse
|
||||||
|
|
||||||
|
### Community 95 - "Community 95"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): YouTubeChannelCache
|
||||||
|
|
||||||
|
### Community 96 - "Community 96"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (1): FileAnalysis
|
||||||
|
|
||||||
|
### Community 97 - "Community 97"
|
||||||
|
Cohesion: 0.5
|
||||||
|
Nodes (3): GitHubAppInstallation, GitHubAppInstallState, GitHubRepoBackup
|
||||||
|
|
||||||
|
### Community 99 - "Community 99"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): handleSubmit(), resetForm()
|
||||||
|
|
||||||
|
### Community 101 - "Community 101"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): File, FileType
|
||||||
|
|
||||||
|
### Community 102 - "Community 102"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): APIKey, BrowserExtension
|
||||||
|
|
||||||
|
### Community 103 - "Community 103"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): ChatMessage, ChatSession
|
||||||
|
|
||||||
|
### Community 104 - "Community 104"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (1): VideoBookmark
|
||||||
|
|
||||||
|
### Community 105 - "Community 105"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (2): Course, LearningPathCourse
|
||||||
|
|
||||||
|
### Community 106 - "Community 106"
|
||||||
|
Cohesion: 0.67
|
||||||
|
Nodes (1): ProductionConfig
|
||||||
|
|
||||||
|
### Community 112 - "Community 112"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): extractVideoId(), handleSubmit()
|
||||||
|
|
||||||
|
### Community 119 - "Community 119"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): handleKeyPress(), handleSendMessage()
|
||||||
|
|
||||||
|
### Community 120 - "Community 120"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (2): handleKeyPress(), handleSendMessage()
|
||||||
|
|
||||||
|
### Community 130 - "Community 130"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Note
|
||||||
|
|
||||||
|
### Community 131 - "Community 131"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): User
|
||||||
|
|
||||||
|
### Community 132 - "Community 132"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Bookmark
|
||||||
|
|
||||||
|
### Community 133 - "Community 133"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Tag
|
||||||
|
|
||||||
|
### Community 134 - "Community 134"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): GitHubUserAuth
|
||||||
|
|
||||||
|
### Community 135 - "Community 135"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): ControlServiceSession
|
||||||
|
|
||||||
|
### Community 136 - "Community 136"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): UserAISettings
|
||||||
|
|
||||||
|
### Community 138 - "Community 138"
|
||||||
|
Cohesion: 1.0
|
||||||
|
Nodes (1): Querier
|
||||||
|
|
||||||
|
## Knowledge Gaps
|
||||||
|
- **290 isolated node(s):** `Channel`, `ChannelInfo`, `DetectedSuggestion`, `DetectedAttachment`, `CacheEntry` (+285 more)
|
||||||
|
These have ≤1 connection - possible missing edges or undocumented components.
|
||||||
|
- **Thin community `Community 19`** (30 nodes): `db.go`, `DBTX`, `Queries`, `.AddBookmarkTag()`, `.AddTaskTag()`, `.CreateBookmark()`, `.CreateTask()`, `.CreateUser()`, `.DeleteBookmark()`, `.DeleteTask()`, `.DeleteUser()`, `.GetBookmarkByID()`, `.GetBookmarksByTag()`, `.GetBookmarksByUser()`, `.GetTaskByID()`, `.GetTasksByStatus()`, `.GetTasksByTag()`, `.GetTasksByUser()`, `.GetUserByEmail()`, `.GetUserByID()`, `.ListUsers()`, `.RemoveBookmarkTag()`, `.RemoveTaskTag()`, `.SearchBookmarks()`, `.SearchTasks()`, `.UpdateBookmark()`, `.UpdateLastLogin()`, `.UpdateTask()`, `.UpdateUser()`, `.WithTx()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 23`** (26 nodes): `IntegrationHandler`, `.AuthorizeIntegration()`, `.CreateIntegration()`, `.DeleteIntegration()`, `.exchangeDiscordCode()`, `.exchangeGitHubCode()`, `.exchangeGoogleCode()`, `.exchangeNotionCode()`, `.exchangeSlackCode()`, `.getDiscordAuthURL()`, `.getGitHubAuthURL()`, `.getGoogleAuthURL()`, `.GetIntegration()`, `.GetIntegrations()`, `.getNotionAuthURL()`, `.getSlackAuthURL()`, `.GetSyncLogs()`, `.OAuthCallback()`, `.performSync()`, `.syncDiscord()`, `.syncGitHub()`, `.syncGoogle()`, `.SyncIntegration()`, `.syncNotion()`, `.syncSlack()`, `.UpdateIntegration()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 39`** (16 nodes): `goals_habits.go`, `NewGoalsHabitsHandler()`, `GoalsHabitsHandler`, `.CreateGoal()`, `.CreateHabit()`, `.CreateHabitEntry()`, `.DeleteGoal()`, `.DeleteHabit()`, `.GetDashboardStats()`, `.GetGoal()`, `.GetGoals()`, `.GetHabit()`, `.GetHabitEntries()`, `.GetHabits()`, `.UpdateGoal()`, `.UpdateHabit()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 40`** (16 nodes): `marketplace.go`, `NewMarketplaceHandler()`, `MarketplaceHandler`, `.CreateContentShare()`, `.CreateMarketplaceItem()`, `.CreateMarketplaceReview()`, `.DeleteContentShare()`, `.DeleteMarketplaceItem()`, `.GetContentShare()`, `.GetMarketplaceItem()`, `.GetMarketplaceItems()`, `.GetMarketplaceReviews()`, `.GetMarketplaceStats()`, `.GetMyContentShares()`, `.GetMyMarketplaceItems()`, `.UpdateMarketplaceItem()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 44`** (15 nodes): `validator.go`, `Validator`, `.Clear()`, `.Email()`, `.GetError()`, `.GetErrors()`, `.HasErrors()`, `.In()`, `.Match()`, `.MaxLength()`, `.MinLength()`, `NewValidator()`, `.Required()`, `.URL()`, `.ValidatePassword()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 48`** (13 nodes): `FileUploadModal.tsx`, `addAssociation()`, `addTag()`, `canUpload()`, `getFileIcon()`, `handleDrag()`, `handleDrop()`, `handleFileSelect()`, `handleKeyDown()`, `handleUpload()`, `isValidUrl()`, `removeAssociation()`, `removeTag()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 67`** (7 nodes): `user_service_example.go`, `NewUserServiceExample()`, `UserServiceExample`, `.CreateUserExample()`, `.GetUserExample()`, `.SearchUsersExample()`, `.TransactionExample()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 70`** (7 nodes): `time_entry.go`, `TimeEntry`, `.BeforeCreate()`, `.BeforeUpdate()`, `.GetDuration()`, `.GetFormattedDuration()`, `.Stop()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 74`** (7 nodes): `ExportImport.tsx`, `export-import.ts`, `exportData()`, `getImportSummary()`, `importData()`, `validateImportData()`, `ExportImport()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 75`** (7 nodes): `AIChat.tsx`, `callAIAPI()`, `checkMobile()`, `handleClickOutside()`, `handleSendMessage()`, `initializeAIModels()`, `startNewChat()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 86`** (5 nodes): `graceful_shutdown.go`, `NewGracefulShutdown()`, `GracefulShutdown`, `.AddCleanupFunc()`, `.Wait()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 87`** (5 nodes): `error_handler.go`, `ErrorHandlerMiddleware()`, `MethodNotAllowedHandler()`, `NotFoundHandler()`, `ErrorResponse`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 95`** (4 nodes): `youtube_cache.go`, `YouTubeChannelCache`, `.IsExpired()`, `.TableName()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 96`** (4 nodes): `file_analysis.go`, `FileAnalysis`, `.BeforeCreate()`, `.TableName()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 99`** (4 nodes): `LearningPathModal.tsx`, `handleInputChange()`, `handleSubmit()`, `resetForm()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 101`** (3 nodes): `file.go`, `File`, `FileType`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 102`** (3 nodes): `browser_extension.go`, `APIKey`, `BrowserExtension`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 103`** (3 nodes): `chat.go`, `ChatMessage`, `ChatSession`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 104`** (3 nodes): `video_bookmark.go`, `VideoBookmark`, `.TableName()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 105`** (3 nodes): `course.go`, `Course`, `LearningPathCourse`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 106`** (3 nodes): `production.go`, `DefaultProductionConfig()`, `ProductionConfig`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 112`** (3 nodes): `VideoUploadModal.tsx`, `extractVideoId()`, `handleSubmit()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 119`** (3 nodes): `FloatingAI.tsx`, `handleKeyPress()`, `handleSendMessage()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 120`** (3 nodes): `AIChatPanel.tsx`, `handleKeyPress()`, `handleSendMessage()`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 130`** (2 nodes): `note.go`, `Note`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 131`** (2 nodes): `user.go`, `User`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 132`** (2 nodes): `bookmark.go`, `Bookmark`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 133`** (2 nodes): `tag.go`, `Tag`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 134`** (2 nodes): `github_user_auth.go`, `GitHubUserAuth`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 135`** (2 nodes): `control_service_auth.go`, `ControlServiceSession`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 136`** (2 nodes): `ai_settings.go`, `UserAISettings`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
- **Thin community `Community 138`** (2 nodes): `querier.go`, `Querier`
|
||||||
|
Too small to be a meaningful cluster - may be noise or needs more connections extracted.
|
||||||
|
|
||||||
|
## Suggested Questions
|
||||||
|
_Questions this graph is uniquely positioned to answer:_
|
||||||
|
|
||||||
|
- **Why does `GetDB()` connect `Community 4` to `Community 0`, `Community 33`, `Community 1`, `Community 3`, `Community 5`, `Community 9`, `Community 10`, `Community 12`, `Community 13`, `Community 14`, `Community 22`?**
|
||||||
|
_High betweenness centrality (0.165) - this node is a cross-community bridge._
|
||||||
|
- **Why does `main()` connect `Community 3` to `Community 1`, `Community 35`, `Community 4`, `Community 39`, `Community 40`, `Community 9`, `Community 11`, `Community 12`, `Community 14`, `Community 16`, `Community 20`, `Community 22`?**
|
||||||
|
_High betweenness centrality (0.149) - this node is a cross-community bridge._
|
||||||
|
- **Why does `contains()` connect `Community 1` to `Community 0`, `Community 3`, `Community 4`, `Community 5`, `Community 6`, `Community 9`, `Community 10`, `Community 11`, `Community 12`, `Community 13`, `Community 17`, `Community 21`, `Community 26`?**
|
||||||
|
_High betweenness centrality (0.117) - this node is a cross-community bridge._
|
||||||
|
- **Are the 94 inferred relationships involving `GetDB()` (e.g. with `main()` and `SeedData()`) actually correct?**
|
||||||
|
_`GetDB()` has 94 INFERRED edges - model-reasoned connections that need verification._
|
||||||
|
- **Are the 49 inferred relationships involving `New()` (e.g. with `InitLogger()` and `applySuggestionAction()`) actually correct?**
|
||||||
|
_`New()` has 49 INFERRED edges - model-reasoned connections that need verification._
|
||||||
|
- **Are the 40 inferred relationships involving `contains()` (e.g. with `DetectMessageContent()` and `getDefaultFavicon()`) actually correct?**
|
||||||
|
_`contains()` has 40 INFERRED edges - model-reasoned connections that need verification._
|
||||||
|
- **Are the 38 inferred relationships involving `main()` (e.g. with `Load()` and `InitDatabase()`) actually correct?**
|
||||||
|
_`main()` has 38 INFERRED edges - model-reasoned connections that need verification._
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>graphify - /home/tdvorak/Desktop/PROG+HTML/Trackeep/graphify-out/graph.html</title>
|
||||||
|
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { background: #0f0f1a; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; height: 100vh; overflow: hidden; }
|
||||||
|
#graph { flex: 1; }
|
||||||
|
#sidebar { width: 280px; background: #1a1a2e; border-left: 1px solid #2a2a4e; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
#search-wrap { padding: 12px; border-bottom: 1px solid #2a2a4e; }
|
||||||
|
#search { width: 100%; background: #0f0f1a; border: 1px solid #3a3a5e; color: #e0e0e0; padding: 7px 10px; border-radius: 6px; font-size: 13px; outline: none; }
|
||||||
|
#search:focus { border-color: #4E79A7; }
|
||||||
|
#search-results { max-height: 140px; overflow-y: auto; padding: 4px 12px; border-bottom: 1px solid #2a2a4e; display: none; }
|
||||||
|
.search-item { padding: 4px 6px; cursor: pointer; border-radius: 4px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.search-item:hover { background: #2a2a4e; }
|
||||||
|
#info-panel { padding: 14px; border-bottom: 1px solid #2a2a4e; min-height: 140px; }
|
||||||
|
#info-panel h3 { font-size: 13px; color: #aaa; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
#info-content { font-size: 13px; color: #ccc; line-height: 1.6; }
|
||||||
|
#info-content .field { margin-bottom: 5px; }
|
||||||
|
#info-content .field b { color: #e0e0e0; }
|
||||||
|
#info-content .empty { color: #555; font-style: italic; }
|
||||||
|
.neighbor-link { display: block; padding: 2px 6px; margin: 2px 0; border-radius: 3px; cursor: pointer; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-left: 3px solid #333; }
|
||||||
|
.neighbor-link:hover { background: #2a2a4e; }
|
||||||
|
#neighbors-list { max-height: 160px; overflow-y: auto; margin-top: 4px; }
|
||||||
|
#legend-wrap { flex: 1; overflow-y: auto; padding: 12px; }
|
||||||
|
#legend-wrap h3 { font-size: 13px; color: #aaa; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.legend-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; cursor: pointer; border-radius: 4px; font-size: 12px; }
|
||||||
|
.legend-item:hover { background: #2a2a4e; padding-left: 4px; }
|
||||||
|
.legend-item.dimmed { opacity: 0.35; }
|
||||||
|
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.legend-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.legend-count { color: #666; font-size: 11px; }
|
||||||
|
#stats { padding: 10px 14px; border-top: 1px solid #2a2a4e; font-size: 11px; color: #555; }
|
||||||
|
#legend-controls { display: flex; gap: 6px; margin-bottom: 8px; }
|
||||||
|
#legend-controls button { flex: 1; background: #0f0f1a; border: 1px solid #3a3a5e; color: #aaa; padding: 4px 0; border-radius: 4px; font-size: 11px; cursor: pointer; }
|
||||||
|
#legend-controls button:hover { border-color: #4E79A7; color: #e0e0e0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="graph"></div>
|
||||||
|
<div id="sidebar">
|
||||||
|
<div id="search-wrap">
|
||||||
|
<input id="search" type="text" placeholder="Search nodes..." autocomplete="off">
|
||||||
|
<div id="search-results"></div>
|
||||||
|
</div>
|
||||||
|
<div id="info-panel">
|
||||||
|
<h3>Node Info</h3>
|
||||||
|
<div id="info-content"><span class="empty">Click a node to inspect it</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="legend-wrap">
|
||||||
|
<h3>Communities</h3>
|
||||||
|
<div id="legend-controls">
|
||||||
|
<button onclick="toggleAllCommunities(false)">Show All</button>
|
||||||
|
<button onclick="toggleAllCommunities(true)">Hide All</button>
|
||||||
|
</div>
|
||||||
|
<div id="legend"></div>
|
||||||
|
</div>
|
||||||
|
<div id="stats">2549 nodes · 3694 edges · 221 communities</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const RAW_NODES = [{"id": "browser_extension_background_js", "label": "background.js", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 14.4, "font": {"size": 0, "color": "#ffffff"}, "title": "background.js", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 14}, {"id": "browser_extension_background_storagesyncget", "label": "storageSyncGet()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "storageSyncGet()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_storagelocalget", "label": "storageLocalGet()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "storageLocalGet()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_storagelocalset", "label": "storageLocalSet()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.9, "font": {"size": 0, "color": "#ffffff"}, "title": "storageLocalSet()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 3}, {"id": "browser_extension_background_parseresponseerror", "label": "parseResponseError()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "parseResponseError()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_parseyoutubevideometa", "label": "parseYouTubeVideoMeta()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "parseYouTubeVideoMeta()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_cleanuphistory", "label": "cleanupHistory()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "cleanupHistory()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_getyoutubehistory", "label": "getYouTubeHistory()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color": "#ffffff"}, "title": "getYouTubeHistory()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 4}, {"id": "browser_extension_background_updateyoutubehistory", "label": "updateYouTubeHistory()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.9, "font": {"size": 0, "color": "#ffffff"}, "title": "updateYouTubeHistory()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 6}, {"id": "browser_extension_background_gettrackeepconfig", "label": "getTrackeepConfig()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color": "#ffffff"}, "title": "getTrackeepConfig()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 4}, {"id": "browser_extension_background_shouldpromptforyoutubevideo", "label": "shouldPromptForYouTubeVideo()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color": "#ffffff"}, "title": "shouldPromptForYouTubeVideo()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 4}, {"id": "browser_extension_background_saveyoutubebookmark", "label": "saveYouTubeBookmark()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color"Line truncated
|
||||||
|
const RAW_EDGES = [{"from": "browser_extension_background_js", "to": "browser_extension_background_storagesyncget", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_storagelocalget", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_storagelocalset", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_parseresponseerror", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_parseyoutubevideometa", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_cleanuphistory", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_getyoutubehistory", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_updateyoutubehistory", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_gettrackeepconfig", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_shouldpromptforyoutubevideo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_saveyoutubebookmark", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_openpopupwithcontext", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_setcontextandopenpopup", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_detectcontenttype", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagesyncget", "to": "browser_extension_background_gettrackeepconfig", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagelocalget", "to": "browser_extension_background_getyoutubehistory", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagelocalset", "to": "browser_extension_background_updateyoutubehistory", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagelocalset", "to": "browser_extension_background_openpopupwithcontext", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_parseresponseerror", "to": "browser_extension_background_saveyoutubebookmark", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_parseyoutubevideometa", "to": "browser_extension_background_detectcontenttype", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_cleanuphistory", "to": "browser_extension_background_updateyoutubehistory", "label": "calls", "title": "cLine truncated
|
||||||
|
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "Community 0", "count": 161}, {"cid": 1, "color": "#F28E2B", "label": "Community 1", "count": 143}, {"cid": 2, "color": "#E15759", "label": "Community 2", "count": 123}, {"cid": 3, "color": "#76B7B2", "label": "Community 3", "count": 116}, {"cid": 4, "color": "#59A14F", "label": "Community 4", "count": 103}, {"cid": 5, "color": "#EDC948", "label": "Community 5", "count": 82}, {"cid": 6, "color": "#B07AA1", "label": "Community 6", "count": 71}, {"cid": 7, "color": "#FF9DA7", "label": "Community 7", "count": 69}, {"cid": 8, "color": "#9C755F", "label": "Community 8", "count": 67}, {"cid": 9, "color": "#BAB0AC", "label": "Community 9", "count": 65}, {"cid": 10, "color": "#4E79A7", "label": "Community 10", "count": 61}, {"cid": 11, "color": "#F28E2B", "label": "Community 11", "count": 55}, {"cid": 12, "color": "#E15759", "label": "Community 12", "count": 48}, {"cid": 13, "color": "#76B7B2", "label": "Community 13", "count": 47}, {"cid": 14, "color": "#59A14F", "label": "Community 14", "count": 39}, {"cid": 15, "color": "#EDC948", "label": "Community 15", "count": 37}, {"cid": 16, "color": "#B07AA1", "label": "Community 16", "count": 36}, {"cid": 17, "color": "#FF9DA7", "label": "Community 17", "count": 31}, {"cid": 18, "color": "#9C755F", "label": "Community 18", "count": 31}, {"cid": 19, "color": "#BAB0AC", "label": "Community 19", "count": 30}, {"cid": 20, "color": "#4E79A7", "label": "Community 20", "count": 29}, {"cid": 21, "color": "#F28E2B", "label": "Community 21", "count": 27}, {"cid": 22, "color": "#E15759", "label": "Community 22", "count": 27}, {"cid": 23, "color": "#76B7B2", "label": "Community 23", "count": 26}, {"cid": 24, "color": "#59A14F", "label": "Community 24", "count": 24}, {"cid": 25, "color": "#EDC948", "label": "Community 25", "count": 24}, {"cid": 26, "color": "#B07AA1", "label": "Community 26", "count": 23}, {"cid": 27, "color": "#FF9DA7", "label": "Community 27", "count": 22}, {"cid": 28, "color": "#9C755F", "label": "Community 28", "count": 21}, {"cid": 29, "color": "#BAB0AC", "label": "Community 29", "count": 21}, {"cid": 30, "color": "#4E79A7", "label": "Community 30", "count": 20}, {"cid": 31, "color": "#F28E2B", "label": "Community 31", "count": 19}, {"cid": 32, "color": "#E15759", "label": "Community 32", "count": 18}, {"cid": 33, "color": "#76B7B2", "label": "Community 33", "count": 18}, {"cid": 34, "color": "#59A14F", "label": "Community 34", "count": 17}, {"cid": 35, "color": "#EDC948", "label": "Community 35", "count": 17}, {"cid": 36, "color": "#B07AA1", "label": "Community 36", "count": 17}, {"cid": 37, "color": "#FF9DA7", "label": "Community 37", "count": 17}, {"cid": 38, "color": "#9C755F", "label": "Community 38", "count": 16}, {"cid": 39, "color": "#BAB0AC", "label": "Community 39", "count": 16}, {"cid": 40, "color": "#4E79A7", "label": "Community 40", "count": 16}, {"cid": 41, "color": "#F28E2B", "label": "Community 41", "count": 16}, {"cid": 42, "color": "#E15759", "label": "Community 42", "count": 16}, {"cid": 43, "color": "#76B7B2", "label": "Community 43", "count": 15}, {"cid": 44, "color": "#59A14F", "label": "Community 44", "count": 15}, {"cid": 45, "color": "#EDC948", "label": "Community 45", "count": 14}, {"cid": 46, "color": "#B07AA1", "label": "Community 46", "count": 14}, {"cid": 47, "color": "#FF9DA7", "label": "Community 47", "count": 13}, {"cid": 48, "color": "#9C755F", "label": "Community 48", "count": 13}, {"cid": 49, "color": "#BAB0AC", "label": "Community 49", "count": 13}, {"cid": 50, "color": "#4E79A7", "label": "Community 50", "count": 13}, {"cid": 51, "color": "#F28E2B", "label": "Community 51", "count": 12}, {"cid": 52, "color": "#E15759", "label": "Community 52", "count": 12}, {"cid": 53, "color": "#76B7B2", "label": "Community 53", "count": 11}, {"cid": 54, "color": "#59A14F", "label": "Community 54", "count": 11}, {"cid": 55, "color": "#EDC948", "label": "Community 55", "count": 11}, {"cid": 56, "color": "#B07AA1", "label": "Community 56", "count": 10}, {"cid": 57, "color": "#FF9DA7", "label": "Community 57", "count": 10}, {"cid": 58, "color": "#9C755F", "label": "Community 58", "count": 10}, {"cid": 59, "color": "#BAB0AC", "label": "Community 59", "count": 10}, {"cid": 60, "color": "#4E79A7", "label": "Community 60", "count": 10}, {"cid": 61, "color": "#F28E2B", "label": "Community 61", "count": 9}, {"cid": 62, "color": "#E15759", "label": "Community 62", "count": 9}, {"cid": 63, "color": "#76B7B2", "label": "Community 63", "count": 8}, {"cid": 64, "color": "#59A14F", "label": "Community 64", "count": 8}, {"cid": 65, "color": "#EDC948", "label": "Community 65", "count": 8}, {"cid": 66, "color": "#B07AA1", "label": "Community 66", "count": 7}, {"cid": 67, "color": "#FF9DA7", "label": "Community 67", "count": 7}, {"cid": 68, "color": "#9C755F", "label": "Community 68", "count": 7}, {"cid": 69, "color": "#BAB0AC", "label": "Community 69", "count": 7}, {"cid": 70, "color": "#4E79A7", "labeLine truncated
|
||||||
|
|
||||||
|
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||||
|
function esc(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build vis datasets
|
||||||
|
const nodesDS = new vis.DataSet(RAW_NODES.map(n => ({
|
||||||
|
id: n.id, label: n.label, color: n.color, size: n.size,
|
||||||
|
font: n.font, title: n.title,
|
||||||
|
_community: n.community, _community_name: n.community_name,
|
||||||
|
_source_file: n.source_file, _file_type: n.file_type, _degree: n.degree,
|
||||||
|
})));
|
||||||
|
|
||||||
|
const edgesDS = new vis.DataSet(RAW_EDGES.map((e, i) => ({
|
||||||
|
id: i, from: e.from, to: e.to,
|
||||||
|
label: '',
|
||||||
|
title: e.title,
|
||||||
|
dashes: e.dashes,
|
||||||
|
width: e.width,
|
||||||
|
color: e.color,
|
||||||
|
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
||||||
|
})));
|
||||||
|
|
||||||
|
const container = document.getElementById('graph');
|
||||||
|
const network = new vis.Network(container, { nodes: nodesDS, edges: edgesDS }, {
|
||||||
|
physics: {
|
||||||
|
enabled: true,
|
||||||
|
solver: 'forceAtlas2Based',
|
||||||
|
forceAtlas2Based: {
|
||||||
|
gravitationalConstant: -60,
|
||||||
|
centralGravity: 0.005,
|
||||||
|
springLength: 120,
|
||||||
|
springConstant: 0.08,
|
||||||
|
damping: 0.4,
|
||||||
|
avoidOverlap: 0.8,
|
||||||
|
},
|
||||||
|
stabilization: { iterations: 200, fit: true },
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
hover: true,
|
||||||
|
tooltipDelay: 100,
|
||||||
|
hideEdgesOnDrag: true,
|
||||||
|
navigationButtons: false,
|
||||||
|
keyboard: false,
|
||||||
|
},
|
||||||
|
nodes: { shape: 'dot', borderWidth: 1.5 },
|
||||||
|
edges: { smooth: { type: 'continuous', roundness: 0.2 }, selectionWidth: 3 },
|
||||||
|
});
|
||||||
|
|
||||||
|
network.once('stabilizationIterationsDone', () => {
|
||||||
|
network.setOptions({ physics: { enabled: false } });
|
||||||
|
});
|
||||||
|
|
||||||
|
function showInfo(nodeId) {
|
||||||
|
const n = nodesDS.get(nodeId);
|
||||||
|
if (!n) return;
|
||||||
|
const neighborIds = network.getConnectedNodes(nodeId);
|
||||||
|
const neighborItems = neighborIds.map(nid => {
|
||||||
|
const nb = nodesDS.get(nid);
|
||||||
|
const color = nb ? nb.color.background : '#555';
|
||||||
|
return `<span class="neighbor-link" style="border-left-color:${esc(color)}" onclick="focusNode(${JSON.stringify(nid)})">${esc(nb ? nb.label : nid)}</span>`;
|
||||||
|
}).join('');
|
||||||
|
document.getElementById('info-content').innerHTML = `
|
||||||
|
<div class="field"><b>${esc(n.label)}</b></div>
|
||||||
|
<div class="field">Type: ${esc(n._file_type || 'unknown')}</div>
|
||||||
|
<div class="field">Community: ${esc(n._community_name)}</div>
|
||||||
|
<div class="field">Source: ${esc(n._source_file || '-')}</div>
|
||||||
|
<div class="field">Degree: ${n._degree}</div>
|
||||||
|
${neighborIds.length ? `<div class="field" style="margin-top:8px;color:#aaa;font-size:11px">Neighbors (${neighborIds.length})</div><div id="neighbors-list">${neighborItems}</div>` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNode(nodeId) {
|
||||||
|
network.focus(nodeId, { scale: 1.4, animation: true });
|
||||||
|
network.selectNodes([nodeId]);
|
||||||
|
showInfo(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track hovered node — hover detection is more reliable than click params
|
||||||
|
let hoveredNodeId = null;
|
||||||
|
network.on('hoverNode', params => {
|
||||||
|
hoveredNodeId = params.node;
|
||||||
|
container.style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
network.on('blurNode', () => {
|
||||||
|
hoveredNodeId = null;
|
||||||
|
container.style.cursor = 'default';
|
||||||
|
});
|
||||||
|
container.addEventListener('click', () => {
|
||||||
|
if (hoveredNodeId !== null) {
|
||||||
|
showInfo(hoveredNodeId);
|
||||||
|
network.selectNodes([hoveredNodeId]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
network.on('click', params => {
|
||||||
|
if (params.nodes.length > 0) {
|
||||||
|
showInfo(params.nodes[0]);
|
||||||
|
} else if (hoveredNodeId === null) {
|
||||||
|
document.getElementById('info-content').innerHTML = '<span class="empty">Click a node to inspect it</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = document.getElementById('search');
|
||||||
|
const searchResults = document.getElementById('search-results');
|
||||||
|
searchInput.addEventListener('input', () => {
|
||||||
|
const q = searchInput.value.toLowerCase().trim();
|
||||||
|
searchResults.innerHTML = '';
|
||||||
|
if (!q) { searchResults.style.display = 'none'; return; }
|
||||||
|
const matches = RAW_NODES.filter(n => n.label.toLowerCase().includes(q)).slice(0, 20);
|
||||||
|
if (!matches.length) { searchResults.style.display = 'none'; return; }
|
||||||
|
searchResults.style.display = 'block';
|
||||||
|
matches.forEach(n => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'search-item';
|
||||||
|
el.textContent = n.label;
|
||||||
|
el.style.borderLeft = `3px solid ${n.color.background}`;
|
||||||
|
el.style.paddingLeft = '8px';
|
||||||
|
el.onclick = () => {
|
||||||
|
network.focus(n.id, { scale: 1.5, animation: true });
|
||||||
|
network.selectNodes([n.id]);
|
||||||
|
showInfo(n.id);
|
||||||
|
searchResults.style.display = 'none';
|
||||||
|
searchInput.value = '';
|
||||||
|
};
|
||||||
|
searchResults.appendChild(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (!searchResults.contains(e.target) && e.target !== searchInput)
|
||||||
|
searchResults.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
const hiddenCommunities = new Set();
|
||||||
|
|
||||||
|
function toggleAllCommunities(hide) {
|
||||||
|
document.querySelectorAll('.legend-item').forEach(item => {
|
||||||
|
hide ? item.classList.add('dimmed') : item.classList.remove('dimmed');
|
||||||
|
});
|
||||||
|
LEGEND.forEach(c => {
|
||||||
|
if (hide) hiddenCommunities.add(c.cid); else hiddenCommunities.delete(c.cid);
|
||||||
|
});
|
||||||
|
const updates = RAW_NODES.map(n => ({ id: n.id, hidden: hide }));
|
||||||
|
nodesDS.update(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const legendEl = document.getElementById('legend');
|
||||||
|
LEGEND.forEach(c => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'legend-item';
|
||||||
|
item.innerHTML = `<div class="legend-dot" style="background:${c.color}"></div>
|
||||||
|
<span class="legend-label">${c.label}</span>
|
||||||
|
<span class="legend-count">${c.count}</span>`;
|
||||||
|
item.onclick = () => {
|
||||||
|
if (hiddenCommunities.has(c.cid)) {
|
||||||
|
hiddenCommunities.delete(c.cid);
|
||||||
|
item.classList.remove('dimmed');
|
||||||
|
} else {
|
||||||
|
hiddenCommunities.add(c.cid);
|
||||||
|
item.classList.add('dimmed');
|
||||||
|
}
|
||||||
|
const updates = RAW_NODES
|
||||||
|
.filter(n => n.community === c.cid)
|
||||||
|
.map(n => ({ id: n.id, hidden: hiddenCommunities.has(c.cid) }));
|
||||||
|
nodesDS.update(updates);
|
||||||
|
};
|
||||||
|
legendEl.appendChild(item);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// Render hyperedges as shaded regions
|
||||||
|
const hyperedges = [];
|
||||||
|
// afterDrawing passes ctx already transformed to network coordinate space.
|
||||||
|
// Draw node positions raw — no manual pan/zoom/DPR math needed.
|
||||||
|
network.on('afterDrawing', function(ctx) {
|
||||||
|
hyperedges.forEach(h => {
|
||||||
|
const positions = h.nodes
|
||||||
|
.map(nid => network.getPositions([nid])[nid])
|
||||||
|
.filter(p => p !== undefined);
|
||||||
|
if (positions.length < 2) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 0.12;
|
||||||
|
ctx.fillStyle = '#6366f1';
|
||||||
|
ctx.strokeStyle = '#6366f1';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
// Centroid and expanded hull in network coordinates
|
||||||
|
const cx = positions.reduce((s, p) => s + p.x, 0) / positions.length;
|
||||||
|
const cy = positions.reduce((s, p) => s + p.y, 0) / positions.length;
|
||||||
|
const expanded = positions.map(p => ({
|
||||||
|
x: cx + (p.x - cx) * 1.15,
|
||||||
|
y: cy + (p.y - cy) * 1.15
|
||||||
|
}));
|
||||||
|
ctx.moveTo(expanded[0].x, expanded[0].y);
|
||||||
|
expanded.slice(1).forEach(p => ctx.lineTo(p.x, p.y));
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.globalAlpha = 0.4;
|
||||||
|
ctx.stroke();
|
||||||
|
// Label
|
||||||
|
ctx.globalAlpha = 0.8;
|
||||||
|
ctx.fillStyle = '#4f46e5';
|
||||||
|
ctx.font = 'bold 11px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(h.label, cx, cy - 5);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+67279
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>graphify - graphify-out/index.html</title>
|
||||||
|
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body { background: #0f0f1a; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; display: flex; height: 100vh; overflow: hidden; }
|
||||||
|
#graph { flex: 1; }
|
||||||
|
#sidebar { width: 280px; background: #1a1a2e; border-left: 1px solid #2a2a4e; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
#search-wrap { padding: 12px; border-bottom: 1px solid #2a2a4e; }
|
||||||
|
#search { width: 100%; background: #0f0f1a; border: 1px solid #3a3a5e; color: #e0e0e0; padding: 7px 10px; border-radius: 6px; font-size: 13px; outline: none; }
|
||||||
|
#search:focus { border-color: #4E79A7; }
|
||||||
|
#search-results { max-height: 140px; overflow-y: auto; padding: 4px 12px; border-bottom: 1px solid #2a2a4e; display: none; }
|
||||||
|
.search-item { padding: 4px 6px; cursor: pointer; border-radius: 4px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.search-item:hover { background: #2a2a4e; }
|
||||||
|
#info-panel { padding: 14px; border-bottom: 1px solid #2a2a4e; min-height: 140px; }
|
||||||
|
#info-panel h3 { font-size: 13px; color: #aaa; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
#info-content { font-size: 13px; color: #ccc; line-height: 1.6; }
|
||||||
|
#info-content .field { margin-bottom: 5px; }
|
||||||
|
#info-content .field b { color: #e0e0e0; }
|
||||||
|
#info-content .empty { color: #555; font-style: italic; }
|
||||||
|
.neighbor-link { display: block; padding: 2px 6px; margin: 2px 0; border-radius: 3px; cursor: pointer; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-left: 3px solid #333; }
|
||||||
|
.neighbor-link:hover { background: #2a2a4e; }
|
||||||
|
#neighbors-list { max-height: 160px; overflow-y: auto; margin-top: 4px; }
|
||||||
|
#legend-wrap { flex: 1; overflow-y: auto; padding: 12px; }
|
||||||
|
#legend-wrap h3 { font-size: 13px; color: #aaa; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.legend-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; cursor: pointer; border-radius: 4px; font-size: 12px; }
|
||||||
|
.legend-item:hover { background: #2a2a4e; padding-left: 4px; }
|
||||||
|
.legend-item.dimmed { opacity: 0.35; }
|
||||||
|
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
|
||||||
|
.legend-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.legend-count { color: #666; font-size: 11px; }
|
||||||
|
#stats { padding: 10px 14px; border-top: 1px solid #2a2a4e; font-size: 11px; color: #555; }
|
||||||
|
#legend-controls { display: flex; gap: 6px; margin-bottom: 8px; }
|
||||||
|
#legend-controls button { flex: 1; background: #0f0f1a; border: 1px solid #3a3a5e; color: #aaa; padding: 4px 0; border-radius: 4px; font-size: 11px; cursor: pointer; }
|
||||||
|
#legend-controls button:hover { border-color: #4E79A7; color: #e0e0e0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="graph"></div>
|
||||||
|
<div id="sidebar">
|
||||||
|
<div id="search-wrap">
|
||||||
|
<input id="search" type="text" placeholder="Search nodes..." autocomplete="off">
|
||||||
|
<div id="search-results"></div>
|
||||||
|
</div>
|
||||||
|
<div id="info-panel">
|
||||||
|
<h3>Node Info</h3>
|
||||||
|
<div id="info-content"><span class="empty">Click a node to inspect it</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="legend-wrap">
|
||||||
|
<h3>Communities</h3>
|
||||||
|
<div id="legend-controls">
|
||||||
|
<button onclick="toggleAllCommunities(false)">Show All</button>
|
||||||
|
<button onclick="toggleAllCommunities(true)">Hide All</button>
|
||||||
|
</div>
|
||||||
|
<div id="legend"></div>
|
||||||
|
</div>
|
||||||
|
<div id="stats">2549 nodes · 3694 edges · 221 communities</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const RAW_NODES = [{"id": "browser_extension_background_js", "label": "background.js", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 14.4, "font": {"size": 0, "color": "#ffffff"}, "title": "background.js", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 14}, {"id": "browser_extension_background_storagesyncget", "label": "storageSyncGet()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "storageSyncGet()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_storagelocalget", "label": "storageLocalGet()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "storageLocalGet()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_storagelocalset", "label": "storageLocalSet()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.9, "font": {"size": 0, "color": "#ffffff"}, "title": "storageLocalSet()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 3}, {"id": "browser_extension_background_parseresponseerror", "label": "parseResponseError()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "parseResponseError()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_parseyoutubevideometa", "label": "parseYouTubeVideoMeta()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "parseYouTubeVideoMeta()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_cleanuphistory", "label": "cleanupHistory()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 10.6, "font": {"size": 0, "color": "#ffffff"}, "title": "cleanupHistory()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 2}, {"id": "browser_extension_background_getyoutubehistory", "label": "getYouTubeHistory()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color": "#ffffff"}, "title": "getYouTubeHistory()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 4}, {"id": "browser_extension_background_updateyoutubehistory", "label": "updateYouTubeHistory()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.9, "font": {"size": 0, "color": "#ffffff"}, "title": "updateYouTubeHistory()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 6}, {"id": "browser_extension_background_gettrackeepconfig", "label": "getTrackeepConfig()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color": "#ffffff"}, "title": "getTrackeepConfig()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 4}, {"id": "browser_extension_background_shouldpromptforyoutubevideo", "label": "shouldPromptForYouTubeVideo()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color": "#ffffff"}, "title": "shouldPromptForYouTubeVideo()", "community": 43, "community_name": "Community 43", "source_file": "browser-extension/background.js", "file_type": "code", "degree": 4}, {"id": "browser_extension_background_saveyoutubebookmark", "label": "saveYouTubeBookmark()", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 11.3, "font": {"size": 0, "color"Line truncated
|
||||||
|
const RAW_EDGES = [{"from": "browser_extension_background_js", "to": "browser_extension_background_storagesyncget", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_storagelocalget", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_storagelocalset", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_parseresponseerror", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_parseyoutubevideometa", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_cleanuphistory", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_getyoutubehistory", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_updateyoutubehistory", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_gettrackeepconfig", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_shouldpromptforyoutubevideo", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_saveyoutubebookmark", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_openpopupwithcontext", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_setcontextandopenpopup", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_js", "to": "browser_extension_background_detectcontenttype", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagesyncget", "to": "browser_extension_background_gettrackeepconfig", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagelocalget", "to": "browser_extension_background_getyoutubehistory", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagelocalset", "to": "browser_extension_background_updateyoutubehistory", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_storagelocalset", "to": "browser_extension_background_openpopupwithcontext", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_parseresponseerror", "to": "browser_extension_background_saveyoutubebookmark", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_parseyoutubevideometa", "to": "browser_extension_background_detectcontenttype", "label": "calls", "title": "calls [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "browser_extension_background_cleanuphistory", "to": "browser_extension_background_updateyoutubehistory", "label": "calls", "title": "cLine truncated
|
||||||
|
const LEGEND = [];
|
||||||
|
|
||||||
|
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||||
|
function esc(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build vis datasets
|
||||||
|
const nodesDS = new vis.DataSet(RAW_NODES.map(n => ({
|
||||||
|
id: n.id, label: n.label, color: n.color, size: n.size,
|
||||||
|
font: n.font, title: n.title,
|
||||||
|
_community: n.community, _community_name: n.community_name,
|
||||||
|
_source_file: n.source_file, _file_type: n.file_type, _degree: n.degree,
|
||||||
|
})));
|
||||||
|
|
||||||
|
const edgesDS = new vis.DataSet(RAW_EDGES.map((e, i) => ({
|
||||||
|
id: i, from: e.from, to: e.to,
|
||||||
|
label: '',
|
||||||
|
title: e.title,
|
||||||
|
dashes: e.dashes,
|
||||||
|
width: e.width,
|
||||||
|
color: e.color,
|
||||||
|
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
||||||
|
})));
|
||||||
|
|
||||||
|
const container = document.getElementById('graph');
|
||||||
|
const network = new vis.Network(container, { nodes: nodesDS, edges: edgesDS }, {
|
||||||
|
physics: {
|
||||||
|
enabled: true,
|
||||||
|
solver: 'forceAtlas2Based',
|
||||||
|
forceAtlas2Based: {
|
||||||
|
gravitationalConstant: -60,
|
||||||
|
centralGravity: 0.005,
|
||||||
|
springLength: 120,
|
||||||
|
springConstant: 0.08,
|
||||||
|
damping: 0.4,
|
||||||
|
avoidOverlap: 0.8,
|
||||||
|
},
|
||||||
|
stabilization: { iterations: 200, fit: true },
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
hover: true,
|
||||||
|
tooltipDelay: 100,
|
||||||
|
hideEdgesOnDrag: true,
|
||||||
|
navigationButtons: false,
|
||||||
|
keyboard: false,
|
||||||
|
},
|
||||||
|
nodes: { shape: 'dot', borderWidth: 1.5 },
|
||||||
|
edges: { smooth: { type: 'continuous', roundness: 0.2 }, selectionWidth: 3 },
|
||||||
|
});
|
||||||
|
|
||||||
|
network.once('stabilizationIterationsDone', () => {
|
||||||
|
network.setOptions({ physics: { enabled: false } });
|
||||||
|
});
|
||||||
|
|
||||||
|
function showInfo(nodeId) {
|
||||||
|
const n = nodesDS.get(nodeId);
|
||||||
|
if (!n) return;
|
||||||
|
const neighborIds = network.getConnectedNodes(nodeId);
|
||||||
|
const neighborItems = neighborIds.map(nid => {
|
||||||
|
const nb = nodesDS.get(nid);
|
||||||
|
const color = nb ? nb.color.background : '#555';
|
||||||
|
return `<span class="neighbor-link" style="border-left-color:${esc(color)}" onclick="focusNode(${JSON.stringify(nid)})">${esc(nb ? nb.label : nid)}</span>`;
|
||||||
|
}).join('');
|
||||||
|
document.getElementById('info-content').innerHTML = `
|
||||||
|
<div class="field"><b>${esc(n.label)}</b></div>
|
||||||
|
<div class="field">Type: ${esc(n._file_type || 'unknown')}</div>
|
||||||
|
<div class="field">Community: ${esc(n._community_name)}</div>
|
||||||
|
<div class="field">Source: ${esc(n._source_file || '-')}</div>
|
||||||
|
<div class="field">Degree: ${n._degree}</div>
|
||||||
|
${neighborIds.length ? `<div class="field" style="margin-top:8px;color:#aaa;font-size:11px">Neighbors (${neighborIds.length})</div><div id="neighbors-list">${neighborItems}</div>` : ''}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNode(nodeId) {
|
||||||
|
network.focus(nodeId, { scale: 1.4, animation: true });
|
||||||
|
network.selectNodes([nodeId]);
|
||||||
|
showInfo(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track hovered node — hover detection is more reliable than click params
|
||||||
|
let hoveredNodeId = null;
|
||||||
|
network.on('hoverNode', params => {
|
||||||
|
hoveredNodeId = params.node;
|
||||||
|
container.style.cursor = 'pointer';
|
||||||
|
});
|
||||||
|
network.on('blurNode', () => {
|
||||||
|
hoveredNodeId = null;
|
||||||
|
container.style.cursor = 'default';
|
||||||
|
});
|
||||||
|
container.addEventListener('click', () => {
|
||||||
|
if (hoveredNodeId !== null) {
|
||||||
|
showInfo(hoveredNodeId);
|
||||||
|
network.selectNodes([hoveredNodeId]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
network.on('click', params => {
|
||||||
|
if (params.nodes.length > 0) {
|
||||||
|
showInfo(params.nodes[0]);
|
||||||
|
} else if (hoveredNodeId === null) {
|
||||||
|
document.getElementById('info-content').innerHTML = '<span class="empty">Click a node to inspect it</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = document.getElementById('search');
|
||||||
|
const searchResults = document.getElementById('search-results');
|
||||||
|
searchInput.addEventListener('input', () => {
|
||||||
|
const q = searchInput.value.toLowerCase().trim();
|
||||||
|
searchResults.innerHTML = '';
|
||||||
|
if (!q) { searchResults.style.display = 'none'; return; }
|
||||||
|
const matches = RAW_NODES.filter(n => n.label.toLowerCase().includes(q)).slice(0, 20);
|
||||||
|
if (!matches.length) { searchResults.style.display = 'none'; return; }
|
||||||
|
searchResults.style.display = 'block';
|
||||||
|
matches.forEach(n => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'search-item';
|
||||||
|
el.textContent = n.label;
|
||||||
|
el.style.borderLeft = `3px solid ${n.color.background}`;
|
||||||
|
el.style.paddingLeft = '8px';
|
||||||
|
el.onclick = () => {
|
||||||
|
network.focus(n.id, { scale: 1.5, animation: true });
|
||||||
|
network.selectNodes([n.id]);
|
||||||
|
showInfo(n.id);
|
||||||
|
searchResults.style.display = 'none';
|
||||||
|
searchInput.value = '';
|
||||||
|
};
|
||||||
|
searchResults.appendChild(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
if (!searchResults.contains(e.target) && e.target !== searchInput)
|
||||||
|
searchResults.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
const hiddenCommunities = new Set();
|
||||||
|
|
||||||
|
function toggleAllCommunities(hide) {
|
||||||
|
document.querySelectorAll('.legend-item').forEach(item => {
|
||||||
|
hide ? item.classList.add('dimmed') : item.classList.remove('dimmed');
|
||||||
|
});
|
||||||
|
LEGEND.forEach(c => {
|
||||||
|
if (hide) hiddenCommunities.add(c.cid); else hiddenCommunities.delete(c.cid);
|
||||||
|
});
|
||||||
|
const updates = RAW_NODES.map(n => ({ id: n.id, hidden: hide }));
|
||||||
|
nodesDS.update(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const legendEl = document.getElementById('legend');
|
||||||
|
LEGEND.forEach(c => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'legend-item';
|
||||||
|
item.innerHTML = `<div class="legend-dot" style="background:${c.color}"></div>
|
||||||
|
<span class="legend-label">${c.label}</span>
|
||||||
|
<span class="legend-count">${c.count}</span>`;
|
||||||
|
item.onclick = () => {
|
||||||
|
if (hiddenCommunities.has(c.cid)) {
|
||||||
|
hiddenCommunities.delete(c.cid);
|
||||||
|
item.classList.remove('dimmed');
|
||||||
|
} else {
|
||||||
|
hiddenCommunities.add(c.cid);
|
||||||
|
item.classList.add('dimmed');
|
||||||
|
}
|
||||||
|
const updates = RAW_NODES
|
||||||
|
.filter(n => n.community === c.cid)
|
||||||
|
.map(n => ({ id: n.id, hidden: hiddenCommunities.has(c.cid) }));
|
||||||
|
nodesDS.update(updates);
|
||||||
|
};
|
||||||
|
legendEl.appendChild(item);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// Render hyperedges as shaded regions
|
||||||
|
const hyperedges = [];
|
||||||
|
// afterDrawing passes ctx already transformed to network coordinate space.
|
||||||
|
// Draw node positions raw — no manual pan/zoom/DPR math needed.
|
||||||
|
network.on('afterDrawing', function(ctx) {
|
||||||
|
hyperedges.forEach(h => {
|
||||||
|
const positions = h.nodes
|
||||||
|
.map(nid => network.getPositions([nid])[nid])
|
||||||
|
.filter(p => p !== undefined);
|
||||||
|
if (positions.length < 2) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 0.12;
|
||||||
|
ctx.fillStyle = '#6366f1';
|
||||||
|
ctx.strokeStyle = '#6366f1';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
// Centroid and expanded hull in network coordinates
|
||||||
|
const cx = positions.reduce((s, p) => s + p.x, 0) / positions.length;
|
||||||
|
const cy = positions.reduce((s, p) => s + p.y, 0) / positions.length;
|
||||||
|
const expanded = positions.map(p => ({
|
||||||
|
x: cx + (p.x - cx) * 1.15,
|
||||||
|
y: cy + (p.y - cy) * 1.15
|
||||||
|
}));
|
||||||
|
ctx.moveTo(expanded[0].x, expanded[0].y);
|
||||||
|
expanded.slice(1).forEach(p => ctx.lineTo(p.x, p.y));
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
ctx.globalAlpha = 0.4;
|
||||||
|
ctx.stroke();
|
||||||
|
// Label
|
||||||
|
ctx.globalAlpha = 0.8;
|
||||||
|
ctx.fillStyle = '#4f46e5';
|
||||||
|
ctx.font = 'bold 11px sans-serif';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(h.label, cx, cy - 5);
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user