mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +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:
+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,9 +37,6 @@ 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
|
||||||
|
|
||||||
@@ -57,7 +54,7 @@ jobs:
|
|||||||
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,18 +63,12 @@ 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 }}
|
||||||
@@ -87,15 +78,15 @@ jobs:
|
|||||||
- 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,26 +98,21 @@ 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
|
||||||
@@ -138,12 +124,10 @@ jobs:
|
|||||||
|
|
||||||
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:
|
||||||
@@ -168,17 +152,6 @@ jobs:
|
|||||||
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
|
||||||
|
|||||||
@@ -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: your@email.com
|
|
||||||
- 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;"
|
||||||
+2
-2
@@ -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';
|
||||||
|
|||||||
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(', ')}`
|
|
||||||
}
|
|
||||||
@@ -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');
|
||||||
@@ -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`, {
|
||||||
@@ -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
|
||||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -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._
|
||||||
File diff suppressed because one or more lines are too long
+67279
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user