9 Commits

Author SHA1 Message Date
Tomas Dvorak f3a835caa2 feat: fully integrate DragonflyDB with application
- Add Redis client initialization with DragonflyDB connection
- Update session middleware to use DragonflyDB with fallback to memory
- Update cache middleware to use DragonflyDB for persistent caching
- Add proper error handling and connection timeouts
- Implement session storage in DragonflyDB with 24-hour expiration
- Add cache invalidation middleware for DragonflyDB
- Maintain backward compatibility with in-memory fallbacks
2026-03-03 12:46:18 +01:00
Tomas Dvorak dee7011192 fix: add missing VITE_API_URL to production frontend
- Add VITE_API_URL environment variable to production frontend
- Ensures frontend can connect to backend API in production
2026-03-03 12:39:08 +01:00
Tomas Dvorak ebd4ba649d fix: update GitHub Actions to use correct go.sum path
- Add cache-dependency-path: backend/go.sum for Go setup action
- Fixes cache restore failures in CI/CD pipeline
2026-03-03 12:32:28 +01:00
Tomas Dvorak 9a580c77d2 feat: migrate to DragonflyDB and clean up environment configuration
- Replace Redis with DragonflyDB for better performance and memory efficiency
- Remove redundant environment variables (POSTGRES_*, ENCRYPTION_KEY, OAUTH_SERVICE_URL)
- Consolidate database configuration to use single DB_* variables
- Use JWT_SECRET for both JWT tokens and encryption
- Remove PORT variable redundancy, use BACKEND_PORT consistently
- Clean up docker-compose configurations for dev/prod consistency
- Add DragonflyDB configuration with optimized memory usage
- Remove redis.conf as it's no longer needed
- Update health checks to use Redis-compatible CLI for DragonflyDB
2026-03-03 12:20:08 +01:00
Tomas Dvorak fc913b5641 fix build 2026-03-03 11:11:55 +01:00
Tomas Dvorak 874efd5452 chore: update gitignore to ensure playwright-cli and desloppify are ignored 2026-03-03 11:07:11 +01:00
Tomáš Dvořák 1e8bf270a1 Delete .desloppify directory 2026-03-03 11:05:11 +01:00
Tomáš Dvořák d82e52ad98 Delete .playwright-cli directory 2026-03-03 11:04:19 +01:00
Tomas Dvorak 083373a24f feat: major feature updates and cleanup
- Add Redis architecture implementation
- Update browser extension functionality
- Clean up deprecated files and documentation
- Enhance backend handlers for auth, messages, search
- Add new configuration options and settings
- Update Docker and deployment configurations
2026-03-03 11:03:37 +01:00
205 changed files with 16063 additions and 51388 deletions
-31
View File
@@ -1,31 +0,0 @@
{
"languages": {},
"review_max_age_days": 30,
"holistic_max_age_days": 30,
"generate_scorecard": true,
"badge_path": "scorecard.png",
"exclude": [],
"ignore": [
"test_coverage::frontend/src/pages/Login.tsx",
"test_coverage::frontend/src/App.tsx"
],
"ignore_metadata": {
"test_coverage::frontend/src/pages/Login.tsx": {
"note": "Login page - test coverage is separate effort, permanently ignore",
"added_at": "2026-02-18T13:23:38+00:00"
},
"test_coverage::frontend/src/App.tsx": {
"note": "Main App component - test coverage is separate effort, permanently ignore",
"added_at": "2026-02-18T13:26:59+00:00"
}
},
"zone_overrides": {},
"review_dimensions": [],
"review_allow_custom_dimensions": false,
"review_custom_dimensions": [],
"large_files_threshold": 0,
"props_threshold": 0,
"finding_noise_budget": 10,
"finding_noise_global_budget": 0,
"target_strict_score": 95
}
-742
View File
@@ -1,742 +0,0 @@
{
"command": "status",
"overall_score": 75.0,
"objective_score": 100.0,
"strict_score": 59.3,
"strict_all_detected": 59.1,
"dimension_scores": {
"File health": {
"score": 100.0,
"strict": 87.6,
"checks": 143,
"issues": 0,
"tier": 3,
"detectors": {
"structural": {
"potential": 143,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
}
}
},
"Code quality": {
"score": 100.0,
"strict": 67.2,
"checks": 1211,
"issues": 0,
"tier": 3,
"detectors": {
"unused": {
"potential": 143,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"logs": {
"potential": 143,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"exports": {
"potential": 305,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"deprecated": {
"potential": 2,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"props": {
"potential": 76,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"smells": {
"potential": 143,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"react": {
"potential": 14,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"orphaned": {
"potential": 146,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"flat_dirs": {
"potential": 25,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"naming": {
"potential": 23,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"facade": {
"potential": 146,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"patterns": {
"potential": 3,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"single_use": {
"potential": 42,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
}
}
},
"Duplication": {
"score": 100.0,
"strict": 99.4,
"checks": 288,
"issues": 0,
"tier": 3,
"detectors": {
"dupes": {
"potential": 288,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
}
}
},
"Test health": {
"score": 100.0,
"strict": 48.6,
"checks": 2246,
"issues": 0,
"tier": 4,
"detectors": {
"test_coverage": {
"potential": 2109,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"subjective_review": {
"potential": 137,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
}
}
},
"Security": {
"score": 100.0,
"strict": 98.6,
"checks": 289,
"issues": 0,
"tier": 4,
"detectors": {
"security": {
"potential": 143,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
},
"cycles": {
"potential": 146,
"pass_rate": 1.0,
"issues": 0,
"weighted_failures": 0.0
}
}
},
"Naming Quality": {
"score": 0.0,
"strict": 0.0,
"checks": 10,
"issues": 0,
"tier": 4,
"detectors": {
"subjective_assessment": {
"potential": 10,
"pass_rate": 0.0,
"issues": 0,
"weighted_failures": 10.0
}
}
},
"Error Consistency": {
"score": 0.0,
"strict": 0.0,
"checks": 10,
"issues": 0,
"tier": 4,
"detectors": {
"subjective_assessment": {
"potential": 10,
"pass_rate": 0.0,
"issues": 0,
"weighted_failures": 10.0
}
}
},
"Abstraction Fit": {
"score": 0.0,
"strict": 0.0,
"checks": 10,
"issues": 0,
"tier": 4,
"detectors": {
"subjective_assessment": {
"potential": 10,
"pass_rate": 0.0,
"issues": 0,
"weighted_failures": 10.0
}
}
},
"Logic Clarity": {
"score": 0.0,
"strict": 0.0,
"checks": 10,
"issues": 0,
"tier": 4,
"detectors": {
"subjective_assessment": {
"potential": 10,
"pass_rate": 0.0,
"issues": 0,
"weighted_failures": 10.0
}
}
},
"AI Generated Debt": {
"score": 0.0,
"strict": 0.0,
"checks": 10,
"issues": 0,
"tier": 4,
"detectors": {
"subjective_assessment": {
"potential": 10,
"pass_rate": 0.0,
"issues": 0,
"weighted_failures": 10.0
}
}
},
"Type Safety": {
"score": 0.0,
"strict": 0.0,
"checks": 10,
"issues": 0,
"tier": 4,
"detectors": {
"subjective_assessment": {
"potential": 10,
"pass_rate": 0.0,
"issues": 0,
"weighted_failures": 10.0
}
}
},
"Contract Coherence": {
"score": 0.0,
"strict": 0.0,
"checks": 10,
"issues": 0,
"tier": 4,
"detectors": {
"subjective_assessment": {
"potential": 10,
"pass_rate": 0.0,
"issues": 0,
"weighted_failures": 10.0
}
}
}
},
"stats": {
"total": 873,
"open": 0,
"fixed": 20,
"auto_resolved": 1,
"wontfix": 768,
"false_positive": 84,
"by_tier": {
"1": {
"open": 0,
"fixed": 17,
"auto_resolved": 0,
"wontfix": 8,
"false_positive": 0
},
"2": {
"open": 0,
"fixed": 3,
"auto_resolved": 1,
"wontfix": 376,
"false_positive": 26
},
"3": {
"open": 0,
"fixed": 0,
"auto_resolved": 0,
"wontfix": 245,
"false_positive": 58
},
"4": {
"open": 0,
"fixed": 0,
"auto_resolved": 0,
"wontfix": 139,
"false_positive": 0
}
}
},
"scan_count": 10,
"last_scan": "2026-02-18T13:28:26+00:00",
"by_tier": {
"1": {
"open": 0,
"fixed": 17,
"auto_resolved": 0,
"wontfix": 8,
"false_positive": 0
},
"2": {
"open": 0,
"fixed": 3,
"auto_resolved": 1,
"wontfix": 376,
"false_positive": 26
},
"3": {
"open": 0,
"fixed": 0,
"auto_resolved": 0,
"wontfix": 245,
"false_positive": 58
},
"4": {
"open": 0,
"fixed": 0,
"auto_resolved": 0,
"wontfix": 139,
"false_positive": 0
}
},
"ignores": [
"test_coverage::frontend/src/pages/Login.tsx",
"test_coverage::frontend/src/App.tsx"
],
"suppression": {
"last_ignored": 1,
"last_raw_findings": 853,
"last_suppressed_pct": 0.1,
"last_ignore_patterns": 2,
"recent_scans": 5,
"recent_ignored": 1,
"recent_raw_findings": 4265,
"recent_suppressed_pct": 0.0
},
"detector_transparency": {
"rows": [
{
"detector": "exports",
"visible": 305,
"suppressed": 0,
"excluded": 0,
"total_detected": 305
},
{
"detector": "smells",
"visible": 215,
"suppressed": 0,
"excluded": 1,
"total_detected": 216
},
{
"detector": "subjective_review",
"visible": 138,
"suppressed": 0,
"excluded": 0,
"total_detected": 138
},
{
"detector": "test_coverage",
"visible": 49,
"suppressed": 1,
"excluded": 0,
"total_detected": 50
},
{
"detector": "structural",
"visible": 25,
"suppressed": 0,
"excluded": 0,
"total_detected": 25
},
{
"detector": "security",
"visible": 18,
"suppressed": 0,
"excluded": 0,
"total_detected": 18
},
{
"detector": "logs",
"visible": 6,
"suppressed": 0,
"excluded": 0,
"total_detected": 6
},
{
"detector": "dupes",
"visible": 3,
"suppressed": 0,
"excluded": 0,
"total_detected": 3
},
{
"detector": "deprecated",
"visible": 0,
"suppressed": 0,
"excluded": 2,
"total_detected": 2
},
{
"detector": "flat_dirs",
"visible": 2,
"suppressed": 0,
"excluded": 0,
"total_detected": 2
},
{
"detector": "unused",
"visible": 2,
"suppressed": 0,
"excluded": 0,
"total_detected": 2
},
{
"detector": "cycles",
"visible": 1,
"suppressed": 0,
"excluded": 0,
"total_detected": 1
},
{
"detector": "react",
"visible": 1,
"suppressed": 0,
"excluded": 0,
"total_detected": 1
}
],
"totals": {
"visible": 765,
"suppressed": 1,
"excluded": 3,
"detectors": 13
}
},
"potentials": {
"typescript": {
"logs": 143,
"unused": 143,
"exports": 305,
"deprecated": 2,
"structural": 143,
"flat_dirs": 25,
"props": 76,
"single_use": 42,
"coupling": 0,
"cycles": 146,
"orphaned": 146,
"patterns": 3,
"naming": 23,
"facade": 146,
"test_coverage": 2109,
"smells": 143,
"react": 14,
"security": 143,
"subjective_review": 137,
"dupes": 288
}
},
"codebase_metrics": {
"typescript": {
"total_files": 151,
"total_loc": 40054,
"total_directories": 25
}
},
"strict_target": {
"target": 95.0,
"current": 59.3,
"gap": 35.7,
"state": "below",
"warning": null
},
"narrative": {
"phase": "stagnation",
"headline": "All T1 and T2 items cleared!",
"dimensions": {
"lowest_dimensions": [
{
"name": "Naming Quality",
"strict": 0.0,
"issues": 0,
"impact": 0.0,
"subjective": true,
"impact_description": "re-review to improve"
},
{
"name": "Error Consistency",
"strict": 0.0,
"issues": 0,
"impact": 0.0,
"subjective": true,
"impact_description": "re-review to improve"
},
{
"name": "Abstraction Fit",
"strict": 0.0,
"issues": 0,
"impact": 0.0,
"subjective": true,
"impact_description": "re-review to improve"
}
],
"biggest_gap_dimensions": [
{
"name": "Test health",
"lenient": 100.0,
"strict": 48.6,
"gap": 51.4,
"wontfix_count": 187
},
{
"name": "Code quality",
"lenient": 100.0,
"strict": 67.2,
"gap": 32.8,
"wontfix_count": 534
},
{
"name": "File health",
"lenient": 100.0,
"strict": 87.6,
"gap": 12.4,
"wontfix_count": 25
}
],
"stagnant_dimensions": [
{
"name": "File health",
"strict": 87.6,
"stuck_scans": 5
},
{
"name": "Code quality",
"strict": 67.2,
"stuck_scans": 5
},
{
"name": "Duplication",
"strict": 99.4,
"stuck_scans": 5
},
{
"name": "Security",
"strict": 98.6,
"stuck_scans": 5
},
{
"name": "Naming Quality",
"strict": 0.0,
"stuck_scans": 5
},
{
"name": "Error Consistency",
"strict": 0.0,
"stuck_scans": 5
},
{
"name": "Abstraction Fit",
"strict": 0.0,
"stuck_scans": 5
},
{
"name": "Logic Clarity",
"strict": 0.0,
"stuck_scans": 5
},
{
"name": "AI Generated Debt",
"strict": 0.0,
"stuck_scans": 5
},
{
"name": "Type Safety",
"strict": 0.0,
"stuck_scans": 5
},
{
"name": "Contract Coherence",
"strict": 0.0,
"stuck_scans": 5
}
]
},
"actions": [
{
"priority": 1,
"type": "debt_review",
"detector": null,
"description": "7.8 pts of wontfix debt \u2014 review stale decisions",
"command": "desloppify show --status wontfix",
"gap": 7.8,
"lane": "debt_review"
}
],
"strategy": {
"fixer_leverage": {
"auto_fixable_count": 0,
"total_count": 0,
"coverage": 0.0,
"impact_ratio": 0.0,
"recommendation": "none"
},
"lanes": {
"debt_review": {
"actions": [
1
],
"file_count": 0,
"total_impact": 0.0,
"automation": "manual",
"run_first": false
}
},
"can_parallelize": false,
"hint": "Try a different dimension to break the plateau."
},
"tools": {
"fixers": [],
"move": {
"available": true,
"relevant": false,
"reason": null,
"usage": "desloppify move <source> <dest> [--dry-run]"
},
"plan": {
"command": "desloppify plan",
"description": "Generate prioritized markdown cleanup plan"
},
"badge": {
"generated": true,
"in_readme": true,
"path": "scorecard.png",
"recommendation": null
}
},
"debt": {
"overall_gap": 7.8,
"wontfix_count": 768,
"worst_dimension": "Test health",
"worst_gap": 51.4,
"trend": "stable"
},
"milestone": "All T1 and T2 items cleared!",
"primary_action": {
"priority": 1,
"type": "debt_review",
"detector": null,
"command": "desloppify show --status wontfix",
"description": "7.8 pts of wontfix debt \u2014 review stale decisions",
"impact": null,
"lane": "debt_review",
"count": null
},
"why_now": "Progress is plateaued, so the top action is the best chance to break the plateau.",
"verification_step": {
"command": "desloppify show --status wontfix",
"reason": "Re-check stale wontfix decisions before treating strict score as stable.",
"success_signal": "Wontfix list reflects only intentional and still-valid exceptions."
},
"risk_flags": [
{
"type": "wontfix_gap",
"severity": "medium",
"message": "7.8 strict-score points are masked by wontfix debt (768 items).",
"command": "desloppify show --status wontfix"
}
],
"strict_target": {
"target": 95.0,
"current": 59.3,
"gap": 35.7,
"state": "below",
"warning": null
},
"reminders": [],
"reminder_history": {
"report_scores": 10,
"auto_fixers_available": 3,
"dry_run_first": 3,
"zone_classification": 3,
"feedback_nudge": 3,
"stagnant_nudge": 10,
"fp_calibration_security_production": 3,
"wontfix_growing": 3,
"fp_calibration_orphaned_production": 3
}
},
"config": {
"review_max_age_days": 30,
"holistic_max_age_days": 30,
"generate_scorecard": true,
"badge_path": "scorecard.png",
"exclude": [],
"ignore": [
"test_coverage::frontend/src/pages/Login.tsx",
"test_coverage::frontend/src/App.tsx"
],
"ignore_metadata": {
"test_coverage::frontend/src/pages/Login.tsx": {
"note": "Login page - test coverage is separate effort, permanently ignore",
"added_at": "2026-02-18T13:23:38+00:00"
},
"test_coverage::frontend/src/App.tsx": {
"note": "Main App component - test coverage is separate effort, permanently ignore",
"added_at": "2026-02-18T13:26:59+00:00"
}
},
"zone_overrides": {},
"review_dimensions": [],
"review_allow_custom_dimensions": false,
"review_custom_dimensions": [],
"large_files_threshold": 0,
"props_threshold": 0,
"finding_noise_budget": 10,
"finding_noise_global_budget": 0,
"target_strict_score": 95,
"languages": {}
}
}
File diff suppressed because it is too large Load Diff
-1
View File
@@ -2,7 +2,6 @@ node_modules
.git .git
.gitignore .gitignore
README.md README.md
.env
.env.local .env.local
.env.production .env.production
Dockerfile Dockerfile
+12 -82
View File
@@ -1,10 +1,9 @@
# Server Configuration # Server Configuration
PORT=8080 FRONTEND_PORT=3000
BACKEND_PORT=8080
DB_PORT=5432
DRAGONFLY_PORT=6379
GIN_MODE=debug GIN_MODE=debug
READ_TIMEOUT=15s
WRITE_TIMEOUT=15s
IDLE_TIMEOUT=60s
SHUTDOWN_TIMEOUT=30s
# Database Configuration # Database Configuration
DB_TYPE=postgres DB_TYPE=postgres
@@ -15,20 +14,14 @@ DB_PASSWORD=your_password_here
DB_NAME=trackeep DB_NAME=trackeep
DB_SSL_MODE=disable DB_SSL_MODE=disable
# Docker Compose Database (used by docker-compose.yml) # DragonflyDB Configuration
POSTGRES_DB=trackeep DRAGONFLY_ADDR=dragonfly:6379
POSTGRES_USER=trackeep DRAGONFLY_PASSWORD=your_dragonfly_password_here
POSTGRES_PASSWORD=your_secure_password_here
# JWT Configuration # JWT Configuration (also used for encryption)
# JWT_SECRET is auto-generated on startup and stored in jwt_secret.key JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
# You can override by setting JWT_SECRET environment variable if needed
JWT_EXPIRES_IN=24h JWT_EXPIRES_IN=24h
# Encryption Configuration
# ENCRYPTION_KEY is auto-generated on startup and stored in encryption.key
# You can override by setting ENCRYPTION_KEY environment variable if needed
# File Upload Configuration # File Upload Configuration
UPLOAD_DIR=./uploads UPLOAD_DIR=./uploads
MAX_FILE_SIZE=10485760 MAX_FILE_SIZE=10485760
@@ -36,77 +29,14 @@ MAX_FILE_SIZE=10485760
# CORS Configuration # CORS Configuration
CORS_ALLOWED_ORIGINS=* CORS_ALLOWED_ORIGINS=*
# AI Services Configuration
LONGCAT_ON=false
LONGCAT_API_KEY=your_longcat_api_key_here
LONGCAT_BASE_URL=https://api.longcat.chat
LONGCAT_OPENAI_ENDPOINT=https://api.longcat.chat/openai
LONGCAT_ANTHROPIC_ENDPOINT=https://api.longcat.chat/anthropic
LONGCAT_MODEL=LongCat-Flash-Chat
LONGCAT_MODEL_THINKING=LongCat-Flash-Thinking
LONGCAT_FORMAT=openai
# Mistral AI Configuration
MISTRAL_ON=false
MISTRAL_API_KEY=your_mistral_api_key_here
MISTRAL_MODEL=mistral-small-latest
MISTRAL_MODEL_THINKING=mistral-large-latest
# Grok AI Configuration
GROK_ON=false
GROK_API_KEY=your_grok_api_key_here
GROK_BASE_URL=https://api.x.ai/v1
GROK_MODEL=grok-4-1-fast-non-reasoning-latest
GROK_MODEL_THINKING=grok-4-1-fast-reasoning-latest
# DeepSeek Configuration
DEEPSEEK_ON=false
DEEPSEEK_API_KEY=your_deepseek_api_key_here
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat
DEEPSEEK_MODEL_THINKING=deepseek-reasoner
# Ollama Configuration
OLLAMA_ON=false
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3.1
OLLAMA_MODEL_THINKING=llama3.1
# OpenRouter Configuration
OPENROUTER_ON=false
OPENROUTER_API_KEY=your_openrouter_api_key_here
OPENROUTER_BASE_URL=https://openrouter.ai/api
OPENROUTER_MODEL=openrouter/auto
OPENROUTER_MODEL_THINKING=openrouter/auto
# Demo Mode Configuration # Demo Mode Configuration
VITE_DEMO_MODE=false VITE_DEMO_MODE=false
# Browser Search API Configuration # AI Services Configuration
BRAVE_API_KEY=your_brave_api_key_here SEARCH_API_PROVIDER=demo
BRAVE_SEARCH_BASE_URL=https://api.search.brave.com/res/v1/web/search
SERPER_API_KEY=your_serper_api_key_here
SERPER_BASE_URL=https://google.serper.dev/search
SEARCH_API_PROVIDER=brave # Options: brave, serper, demo
# Search Configuration
SEARCH_RESULTS_LIMIT=10 SEARCH_RESULTS_LIMIT=10
SEARCH_CACHE_TTL=300
SEARCH_RATE_LIMIT=100
# Update Configuration # Auto Update Configuration
# Application version (used for update checking)
APP_VERSION=1.0.0
# OAuth service configuration (REQUIRED)
# The OAuth service must be running for updates to work
OAUTH_SERVICE_URL=http://localhost:9090
JWT_SECRET=your-jwt-secret-key
# Update settings
AUTO_UPDATE_CHECK=false AUTO_UPDATE_CHECK=false
UPDATE_CHECK_INTERVAL=24h UPDATE_CHECK_INTERVAL=24h
PRERELEASE_UPDATES=false PRERELEASE_UPDATES=false
# Note: No GitHub token configuration needed
# Updates use the OAuth service which handles GitHub authentication automatically
+4
View File
@@ -37,6 +37,8 @@ jobs:
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.24' go-version: '1.24'
cache: true
cache-dependency-path: backend/go.sum
- name: Install backend dependencies - name: Install backend dependencies
run: | run: |
@@ -91,6 +93,8 @@ jobs:
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: '1.24' go-version: '1.24'
cache: true
cache-dependency-path: backend/go.sum
- name: Run go vet - name: Run go vet
run: | run: |
+3
View File
@@ -26,6 +26,9 @@ pids
*.pid *.pid
*.seed *.seed
*.pid.lock *.pid.lock
.playwright
.playwright-cli
.desloppify
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage/ coverage/
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,18 +0,0 @@
- generic [ref=e26]:
- generic [ref=e27]:
- img "Trackeep Logo" [ref=e29]
- heading "Trackeep" [level=1] [ref=e30]
- paragraph [ref=e31]: Welcome back
- generic [ref=e32]:
- generic [ref=e35]: Registration Disabled
- paragraph [ref=e36]: Accounts can only be created by the administrator. Please contact your admin to get an account.
- generic [ref=e37]:
- generic [ref=e38]:
- generic [ref=e39]: Email
- textbox "Email" [ref=e40]:
- /placeholder: your@email.com
- generic [ref=e41]:
- generic [ref=e42]: Password
- textbox "Password" [ref=e43]:
- /placeholder: ••••••••
- button "Sign In" [ref=e44] [cursor=pointer]
@@ -1,21 +0,0 @@
- generic [ref=e26]:
- generic [ref=e27]:
- img "Trackeep Logo" [ref=e29]
- heading "Trackeep" [level=1] [ref=e30]
- paragraph [ref=e31]: Welcome back
- generic [ref=e32]:
- generic [ref=e35]: Registration Disabled
- paragraph [ref=e36]: Accounts can only be created by the administrator. Please contact your admin to get an account.
- generic [ref=e37]:
- generic [ref=e45]: Invalid credentials
- generic [ref=e38]:
- generic [ref=e39]: Email
- textbox "Email" [ref=e40]:
- /placeholder: your@email.com
- text: demo@trackeep.com
- generic [ref=e41]:
- generic [ref=e42]: Password
- textbox "Password" [ref=e43]:
- /placeholder: ••••••••
- text: password
- button "Sign In" [ref=e44] [cursor=pointer]
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1 +0,0 @@
- paragraph [ref=e6]: Checking authentication...
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,18 +0,0 @@
- generic [ref=e4]:
- generic [ref=e5]:
- img "Trackeep Logo" [ref=e7]
- heading "Trackeep" [level=1] [ref=e8]
- paragraph [ref=e9]: Welcome back
- generic [ref=e10]:
- generic [ref=e13]: Registration Disabled
- paragraph [ref=e14]: Accounts can only be created by the administrator. Please contact your admin to get an account.
- generic [ref=e15]:
- generic [ref=e16]:
- generic [ref=e17]: Email
- textbox "Email" [ref=e18]:
- /placeholder: your@email.com
- generic [ref=e19]:
- generic [ref=e20]: Password
- textbox "Password" [ref=e21]:
- /placeholder: ••••••••
- button "Sign In" [ref=e22] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,18 +0,0 @@
- generic [ref=e4]:
- generic [ref=e5]:
- img "Trackeep Logo" [ref=e7]
- heading "Trackeep" [level=1] [ref=e8]
- paragraph [ref=e9]: Welcome back
- generic [ref=e10]:
- generic [ref=e13]: Registration Disabled
- paragraph [ref=e14]: Accounts can only be created by the administrator. Please contact your admin to get an account.
- generic [ref=e15]:
- generic [ref=e16]:
- generic [ref=e17]: Email
- textbox "Email" [ref=e18]:
- /placeholder: your@email.com
- generic [ref=e19]:
- generic [ref=e20]: Password
- textbox "Password" [ref=e21]:
- /placeholder: ••••••••
- button "Sign In" [ref=e22] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,237 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:21 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,237 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:21 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,237 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:22 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,257 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:22 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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]
- generic [ref=e279]:
- generic [ref=e280]:
- heading "Create Workspace" [level=3] [ref=e281]
- paragraph [ref=e282]: Add a new workspace for your team or projects.
- generic [ref=e283]:
- generic [ref=e284]:
- text: Name
- textbox "Workspace name" [ref=e285]
- generic [ref=e286]:
- text: Description
- textbox "Description" [ref=e287]:
- /placeholder: Optional description
- generic [ref=e288]:
- generic [ref=e289]:
- paragraph [ref=e290]: Public workspace
- paragraph [ref=e291]: Allow all members to discover this workspace.
- switch [ref=e292] [cursor=pointer]
- generic [ref=e293]:
- button "Cancel" [ref=e294] [cursor=pointer]
- button "Create Workspace" [ref=e295] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,237 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:22 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,244 +0,0 @@
- generic [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" [expanded] [active] [ref=e14] [cursor=pointer]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e20]: Trackeep Workspace
- img [ref=e22]
- listbox [ref=e266]:
- option "Trackeep Workspace" [ref=e267] [cursor=pointer]:
- img [ref=e268]
- generic [ref=e271]: Trackeep Workspace
- button "Create Workspace" [ref=e274] [cursor=pointer]:
- img [ref=e275]
- generic [ref=e276]: Create Workspace
- 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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:22 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,244 +0,0 @@
- generic [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" [expanded] [active] [ref=e14] [cursor=pointer]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e20]: Trackeep Workspace
- img [ref=e22]
- listbox [ref=e266]:
- option "Trackeep Workspace" [ref=e267] [cursor=pointer]:
- img [ref=e268]
- generic [ref=e271]: Trackeep Workspace
- button "Create Workspace" [ref=e274] [cursor=pointer]:
- img [ref=e275]
- generic [ref=e276]: Create Workspace
- 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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:22 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,237 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:23 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,244 +0,0 @@
- generic [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" [expanded] [active] [ref=e14] [cursor=pointer]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e20]: Trackeep Workspace
- img [ref=e22]
- listbox [ref=e266]:
- option "Trackeep Workspace" [ref=e267] [cursor=pointer]:
- img [ref=e268]
- generic [ref=e271]: Trackeep Workspace
- button "Create Workspace" [ref=e274] [cursor=pointer]:
- img [ref=e275]
- generic [ref=e276]: Create Workspace
- 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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:23 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,257 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:23 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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]
- generic [ref=e279]:
- generic [ref=e280]:
- heading "Create Workspace" [level=3] [ref=e281]
- paragraph [ref=e282]: Add a new workspace for your team or projects.
- generic [ref=e283]:
- generic [ref=e284]:
- text: Name
- textbox "Workspace name" [ref=e285]
- generic [ref=e286]:
- text: Description
- textbox "Description" [ref=e287]:
- /placeholder: Optional description
- generic [ref=e288]:
- generic [ref=e289]:
- paragraph [ref=e290]: Public workspace
- paragraph [ref=e291]: Allow all members to discover this workspace.
- switch [ref=e292] [cursor=pointer]
- generic [ref=e293]:
- button "Cancel" [ref=e294] [cursor=pointer]
- button "Create Workspace" [ref=e295] [cursor=pointer]
@@ -1,257 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:23 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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]
- generic [ref=e279]:
- generic [ref=e280]:
- heading "Create Workspace" [level=3] [ref=e281]
- paragraph [ref=e282]: Add a new workspace for your team or projects.
- generic [ref=e283]:
- generic [ref=e284]:
- text: Name
- textbox "Workspace name" [ref=e285]
- generic [ref=e286]:
- text: Description
- textbox "Description" [ref=e287]:
- /placeholder: Optional description
- generic [ref=e288]:
- generic [ref=e289]:
- paragraph [ref=e290]: Public workspace
- paragraph [ref=e291]: Allow all members to discover this workspace.
- switch [ref=e292] [cursor=pointer]
- generic [ref=e293]:
- button "Cancel" [ref=e294] [cursor=pointer]
- button "Create Workspace" [ref=e295] [cursor=pointer]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,237 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:23 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,263 +0,0 @@
- generic [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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- generic [ref=e178]:
- button "AU" [active] [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- generic [ref=e267]:
- generic [ref=e269]:
- generic [ref=e270]: AU
- generic [ref=e271]:
- paragraph [ref=e272]: Admin User
- paragraph [ref=e273]: admin@trackeep.com
- generic [ref=e275]:
- generic [ref=e276]:
- paragraph [ref=e277]: "0"
- paragraph [ref=e278]: Bookmarks
- generic [ref=e279]:
- paragraph [ref=e280]: "0"
- paragraph [ref=e281]: Tasks
- button "Profile" [ref=e282] [cursor=pointer]:
- img [ref=e283]
- text: Profile
- button "Statistics" [ref=e286] [cursor=pointer]:
- img [ref=e287]
- text: Statistics
- button "Settings" [ref=e289] [cursor=pointer]:
- img [ref=e290]
- text: Settings
- button "Logout" [ref=e294] [cursor=pointer]:
- img [ref=e295]
- text: Logout
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:23 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,263 +0,0 @@
- generic [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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- generic [ref=e178]:
- button "AU" [active] [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- generic [ref=e267]:
- generic [ref=e269]:
- generic [ref=e270]: AU
- generic [ref=e271]:
- paragraph [ref=e272]: Admin User
- paragraph [ref=e273]: admin@trackeep.com
- generic [ref=e275]:
- generic [ref=e276]:
- paragraph [ref=e277]: "0"
- paragraph [ref=e278]: Bookmarks
- generic [ref=e279]:
- paragraph [ref=e280]: "0"
- paragraph [ref=e281]: Tasks
- button "Profile" [ref=e282] [cursor=pointer]:
- img [ref=e283]
- text: Profile
- button "Statistics" [ref=e286] [cursor=pointer]:
- img [ref=e287]
- text: Statistics
- button "Settings" [ref=e289] [cursor=pointer]:
- img [ref=e290]
- text: Settings
- button "Logout" [ref=e294] [cursor=pointer]:
- img [ref=e295]
- text: Logout
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Tasks" [level=1] [ref=e188]
- button "Add Task" [ref=e189] [cursor=pointer]
- generic [ref=e190]:
- generic [ref=e191]:
- paragraph [ref=e192]: "0"
- paragraph [ref=e193]: Total Tasks
- generic [ref=e194]:
- paragraph [ref=e195]: "0"
- paragraph [ref=e196]: Active
- generic [ref=e197]:
- paragraph [ref=e198]: "0"
- paragraph [ref=e199]: Completed
- generic [ref=e201]:
- textbox "Search tasks..." [ref=e202]
- combobox [ref=e203]:
- option "All Priorities" [selected]
- option "high"
- option "medium"
- option "low"
- generic [ref=e204]:
- button "all" [ref=e205] [cursor=pointer]
- button "active" [ref=e206] [cursor=pointer]
- button "completed" [ref=e207] [cursor=pointer]
- paragraph [ref=e210]: No tasks yet. Add your first task!
- button "AI Assistant" [ref=e211] [cursor=pointer]:
- img [ref=e212]
- generic [ref=e219]:
- generic [ref=e220]:
- generic [ref=e221]:
- img [ref=e223]
- generic [ref=e230]:
- heading "AI Assistant" [level=3] [ref=e231]
- paragraph [ref=e232]: Always here to help
- button [ref=e234] [cursor=pointer]:
- img [ref=e235]
- generic [ref=e239]:
- img [ref=e241]
- generic [ref=e248]:
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e250]: 04:23 PM
- generic [ref=e251]:
- generic [ref=e252]:
- textbox "Type your message..." [ref=e253]
- button [disabled]:
- img
- generic [ref=e255]:
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
- img "longcat icon" [ref=e258]
- generic [ref=e259]: LongCat
- img [ref=e260]
- generic [ref=e262]:
- generic [ref=e263]: longcat
- link "AI settings" [ref=e264] [cursor=pointer]:
- /url: /app/settings#ai
- generic:
- generic:
- generic:
- heading "Add New Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Add Task" [disabled]
- generic:
- generic:
- generic:
- heading "Edit Task" [level=3]
- button:
- img
- generic:
- textbox "Task title *"
- textbox "Description (optional)"
- generic:
- combobox:
- option "Low Priority"
- option "Medium Priority" [selected]
- option "High Priority"
- generic:
- button "Due date (optional)":
- generic: Due date (optional)
- img
- generic:
- button "Cancel"
- button "Save Changes" [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,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,197 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:24 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,197 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:24 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,197 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:24 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,197 +0,0 @@
- generic [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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:24 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,197 +0,0 @@
- generic [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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:24 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,197 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:25 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,197 +0,0 @@
- generic [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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:25 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,197 +0,0 @@
- generic [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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:25 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,14 +0,0 @@
- generic [ref=e5]:
- generic [ref=e7]:
- img "Trackeep Logo" [ref=e10]
- heading "Authentication Required" [level=1] [ref=e11]
- paragraph [ref=e12]: Please sign in to access Trackeep
- generic [ref=e13]:
- generic [ref=e15]:
- img [ref=e17]
- generic [ref=e19]:
- heading "Authentication Required" [level=3] [ref=e20]
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
- generic [ref=e22]:
- button "Sign In" [ref=e23] [cursor=pointer]
- button "Create Account" [ref=e24] [cursor=pointer]
@@ -1,197 +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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:26 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
@@ -1,197 +0,0 @@
- generic [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
- generic [ref=e124]:
- generic [ref=e125]: Version 1.0.0
- button "Update Failed" [ref=e126] [cursor=pointer]:
- generic [ref=e127]:
- img [ref=e128]
- generic [ref=e130]: Update Failed
- navigation [ref=e132]:
- link "Removed stuff" [ref=e133] [cursor=pointer]:
- /url: /app/removed-stuff
- generic [ref=e134]:
- img [ref=e135]
- generic [ref=e138]: Removed stuff
- link "Settings" [ref=e140] [cursor=pointer]:
- /url: /app/settings
- generic [ref=e141]:
- img [ref=e142]
- generic [ref=e145]: Settings
- button "Logout" [ref=e147] [cursor=pointer]:
- generic [ref=e148]:
- img [ref=e149]
- generic [ref=e153]: Logout
- generic [ref=e155]:
- generic [ref=e156]:
- generic [ref=e157]:
- button [ref=e158] [cursor=pointer]:
- img [ref=e159]
- button "Quick search" [ref=e160] [cursor=pointer]:
- img [ref=e161]
- text: Quick search
- generic [ref=e164]:
- button "Import a document" [ref=e165] [cursor=pointer]:
- img [ref=e166]
- text: Import a document
- button [ref=e170] [cursor=pointer]:
- img [ref=e171]
- img [ref=e176]
- button "AU" [ref=e180] [cursor=pointer]:
- generic [ref=e181]: AU
- img [ref=e182]
- main [ref=e184]:
- generic [ref=e186]:
- generic [ref=e187]:
- heading "Files" [level=1] [ref=e188]
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
- img [ref=e190]
- text: Upload File
- generic [ref=e194]:
- textbox "Search files..." [ref=e195]
- combobox [ref=e196]:
- option "All Tags" [selected]
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
- button "AI Assistant" [ref=e199] [cursor=pointer]:
- img [ref=e200]
- generic [ref=e207]:
- generic [ref=e208]:
- generic [ref=e209]:
- img [ref=e211]
- generic [ref=e218]:
- heading "AI Assistant" [level=3] [ref=e219]
- paragraph [ref=e220]: Always here to help
- button [ref=e222] [cursor=pointer]:
- img [ref=e223]
- generic [ref=e227]:
- img [ref=e229]
- generic [ref=e236]:
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
- paragraph [ref=e238]: 04:26 PM
- generic [ref=e239]:
- generic [ref=e240]:
- textbox "Type your message..." [ref=e241]
- button [disabled]:
- img
- generic [ref=e243]:
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
- img "longcat icon" [ref=e246]
- generic [ref=e247]: LongCat
- img [ref=e248]
- generic [ref=e250]:
- generic [ref=e251]: longcat
- link "AI settings" [ref=e252] [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]
-89
View File
@@ -1,89 +0,0 @@
# Trackeep Deployment Guide
## Flexible Deployment Options
Trackeep is designed to work in various deployment scenarios:
### 1. Local Development (localhost)
```bash
# Start with default settings
docker compose up -d
# Frontend will be available via nginx on port 80
# Backend API on port 8080
# Frontend automatically detects API URL: http://localhost:8080/api/v1
```
### 2. Home Network Deployment
```bash
# Set your HOST environment variable
export HOST=192.168.1.100:8080
# Or modify .env
echo "HOST=192.168.1.100:8080" >> .env
docker compose up -d
# Access from any device on your network
# Frontend: http://192.168.1.100
# API: http://192.168.1.100:8080/api/v1
```
### 3. Domain with Cloudflare/Reverse Proxy
```bash
# Set HOST to your domain
export HOST=yourdomain.com
# Configure CORS for your domain
export CORS_ALLOWED_ORIGINS=https://yourdomain.com
docker compose up -d
# Configure Cloudflare to proxy:
# - yourdomain.com → backend:8080
# - app.yourdomain.com → frontend:80
```
### 4. Production HTTPS
```bash
# Set production mode
export GIN_MODE=release
export HOST=yourdomain.com
export CORS_ALLOWED_ORIGINS=https://yourdomain.com
# Use SSL certificates (via Traefik, Nginx, etc.)
docker compose up -d
```
## Environment Variables
### Core Configuration
- `PORT=8080` - Backend port only
- `GIN_MODE=debug|release` - Application mode
- `HOST=` - Auto-detection fallback (optional)
- `CORS_ALLOWED_ORIGINS=*` - Flexible CORS (restrict in production)
### Removed Variables
-`FRONTEND_PORT` - No longer needed
-`OAUTH_PORT` - Moved to oauth-service/.env
-`VITE_API_URL` - Auto-detected via /api/v1/config
### OAuth Service (Separate)
See `oauth-service/.env.example` for OAuth-specific configuration.
## API Detection
The frontend automatically detects the API URL by:
1. Calling `/api/v1/config` endpoint
2. Using the current request's scheme and host
3. Falling back to `HOST` environment variable
4. Final fallback to `localhost:8080`
## Port Management
- **Backend**: Fixed port 8080 (required for API)
- **Frontend**: No port mapping (uses nginx:80 internally)
- **OAuth**: Separate service on port 9090
- **Database**: Port 5432 (internal to Docker network)
This flexibility allows Trackeep to adapt to any deployment scenario while maintaining a consistent configuration approach.
-33
View File
@@ -1,33 +0,0 @@
/* global chrome */
// Create context menu when extension is installed
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'save-to-trackeep',
title: 'Save to Trackeep',
contexts: ['page', 'link', 'selection', 'image', 'video']
});
});
// Handle context menu click
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== 'save-to-trackeep') return;
// Open popup with pre-filled data based on context
const url = info.linkUrl || info.srcUrl || tab?.url || '';
const title = tab?.title || '';
const selection = info.selectionText || '';
// Store temporary data for popup to read
chrome.storage.local.set({
contextMenuData: {
url,
title,
selection,
timestamp: Date.now()
}
}, () => {
// Open the popup (or focus it if already open)
chrome.action.openPopup();
});
});
-531
View File
@@ -1,531 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-kb-theme="dark">
<head>
<meta charset="UTF-8" />
<title>Trackeep Saver Options</title>
<style>
/* Modern Inter Font */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
/* Modern CSS Variables - Proton Pass Inspired */
:root {
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-tertiary: #262626;
--bg-hover: #2a2a2a;
--bg-active: #333333;
--border-primary: #2a2a2a;
--border-secondary: #333333;
--text-primary: #ffffff;
--text-secondary: #a3a3a3;
--text-tertiary: #737373;
--accent-primary: #3b82f6;
--accent-hover: #2563eb;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
--gradient-secondary: linear-gradient(135deg, #1a1a1a 0%, #262626 100%);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
font-size: 14px;
color-scheme: dark;
}
/* Header */
.header {
background: var(--gradient-secondary);
padding: 32px 20px 20px;
border-bottom: 1px solid var(--border-primary);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--gradient-primary);
}
.header-content {
max-width: 640px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 16px;
}
.logo-container {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 20px;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.logo::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
}
.title-section {
flex: 1;
}
.title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 4px 0;
}
.subtitle {
font-size: 14px;
color: var(--text-secondary);
margin: 0;
}
/* Main Content */
.container {
max-width: 640px;
margin: 0 auto;
padding: 32px 20px;
}
/* Sections */
.section {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: 24px;
border: 1px solid var(--border-primary);
margin-bottom: 24px;
transition: all 0.2s ease;
}
.section:hover {
border-color: var(--border-secondary);
box-shadow: var(--shadow-md);
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.section-icon {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
/* Form Elements */
.form-group {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
input[type="text"],
input[type="url"],
input[type="password"] {
width: 100%;
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 14px;
font-family: 'Inter', sans-serif;
font-weight: 400;
transition: all 0.2s ease;
outline: none;
}
input[type="text"]:focus,
input[type="url"]:focus,
input[type="password"]:focus {
border-color: var(--accent-primary);
background: var(--bg-hover);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Instructions */
.instructions {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: 16px;
border: 1px solid var(--border-primary);
margin-top: 16px;
}
.instructions-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 6px;
}
.instructions-list {
font-size: 13px;
color: var(--text-secondary);
margin: 0;
padding-left: 16px;
line-height: 1.6;
}
.instructions-list li {
margin-bottom: 4px;
}
.instructions-list li:last-child {
margin-bottom: 0;
}
/* Buttons */
.btn {
padding: 14px 24px;
border-radius: var(--radius-md);
border: none;
font-size: 14px;
font-weight: 500;
font-family: 'Inter', sans-serif;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
outline: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
background: var(--gradient-primary);
color: white;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.btn-primary:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
/* Status Messages */
.status-message {
padding: 16px 20px;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 500;
margin-top: 20px;
display: flex;
align-items: center;
gap: 10px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.status-message.success {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.status-message.error {
background: rgba(239, 68, 68, 0.1);
color: var(--error);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.status-message.info {
background: rgba(59, 130, 246, 0.1);
color: var(--accent-primary);
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* Code styling */
code {
background: var(--bg-tertiary);
padding: 3px 8px;
border-radius: var(--radius-sm);
font-size: 12px;
color: var(--text-primary);
border: 1px solid var(--border-primary);
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
}
/* Icon System */
.icon {
width: 16px;
height: 16px;
display: inline-block;
vertical-align: middle;
transition: all 0.2s ease;
}
.icon-sm {
width: 12px;
height: 12px;
}
.icon-lg {
width: 20px;
height: 20px;
}
.icon-xl {
width: 24px;
height: 24px;
}
/* Icon animations */
.icon-spin {
animation: spin 1s linear infinite;
}
.icon-pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
/* Enhanced button icons */
.btn .icon {
transition: transform 0.2s ease;
}
.btn:hover .icon {
transform: scale(1.1);
}
.btn:active .icon {
transform: scale(0.95);
}
/* Section icon enhancements */
.section-icon {
transition: all 0.3s ease;
}
.section:hover .section-icon {
transform: scale(1.05) rotate(5deg);
box-shadow: var(--shadow-md);
}
/* Responsive */
@media (max-width: 640px) {
.container {
padding: 20px 16px;
}
.section {
padding: 20px;
}
.header {
padding: 24px 16px 16px;
}
.title {
font-size: 24px;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-content">
<div class="logo-container">
<div class="logo">T</div>
<div class="title-section">
<h1 class="title">Trackeep Saver</h1>
<p class="subtitle">Configure your extension settings</p>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container">
<div class="section">
<div class="section-header">
<div class="section-icon">
<svg class="icon-xl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v6m0 6v6m4.22-13.22l4.24 4.24M1.54 1.54l4.24 4.24M1 12h6m6 0h6"/>
</svg>
</div>
<h2 class="section-title">API Configuration</h2>
</div>
<div class="form-group">
<label for="apiBaseUrl">Trackeep API Base URL</label>
<input
id="apiBaseUrl"
type="url"
placeholder="https://your-domain.example.com/api/v1 or http://localhost:8080/api/v1"
/>
</div>
<div class="form-group">
<label for="authToken">Authentication Token (JWT)</label>
<input
id="authToken"
type="password"
placeholder="Paste your Trackeep authentication token here"
/>
</div>
<div class="instructions">
<div class="instructions-title">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10,9 9,9 8,9"/>
</svg>
<span>How to get your authentication token:</span>
</div>
<ol class="instructions-list">
<li>Log into your Trackeep account in your browser</li>
<li>Open Developer Tools (F12) → Application → Local Storage</li>
<li>Find key <code>trackeep_token</code> and copy its value</li>
<li>Paste token in field above</li>
<li><strong>Never share this token publicly</strong> - it provides full access to your account</li>
</ol>
</div>
<button class="btn btn-primary" id="saveBtn" style="margin-top: 24px;">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17,21 17,13 7,13 7,21"/>
<polyline points="7,3 7,8 15,8"/>
</svg>
<span>Save Settings</span>
</button>
<div id="statusMessage" class="status-message" style="display: none;"></div>
</div>
</main>
<script src="options.js"></script>
</body>
</html>
-136
View File
@@ -1,136 +0,0 @@
/* global chrome */
const apiBaseUrlInput = document.getElementById('apiBaseUrl');
const authTokenInput = document.getElementById('authToken');
const saveBtn = document.getElementById('saveBtn');
const statusMessageEl = document.getElementById('statusMessage');
function showMessage(message, type = 'info', duration = 5000) {
statusMessageEl.textContent = message;
statusMessageEl.className = `status-message ${type}`;
statusMessageEl.style.display = 'flex';
if (duration > 0) {
setTimeout(() => {
statusMessageEl.style.display = 'none';
}, duration);
}
}
function hideMessage() {
statusMessageEl.style.display = 'none';
}
function setButtonLoading(button, loading = true) {
if (loading) {
button.disabled = true;
const originalContent = button.innerHTML;
button.dataset.originalContent = originalContent;
button.innerHTML = `
<svg class="icon icon-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<span>Saving...</span>
`;
} else {
button.disabled = false;
if (button.dataset.originalContent) {
button.innerHTML = button.dataset.originalContent;
delete button.dataset.originalContent;
}
}
}
function detectAndPrefillApiBaseUrl(callback) {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0];
if (!tab || !tab.url) {
if (callback) callback();
return;
}
try {
const url = new URL(tab.url);
const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost';
if (isTrackeepDomain && (url.protocol === 'https:' || url.protocol === 'http:')) {
const candidate = `${url.origin}/api/v1`;
chrome.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = candidate;
}
if (callback) callback();
});
} else {
// Fallback to localhost if nothing set
chrome.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = 'http://localhost:8080/api/v1';
}
if (callback) callback();
});
}
} catch (e) {
if (callback) callback();
}
});
}
function loadSettings() {
chrome.storage.sync.get(['trackeepApiBaseUrl', 'trackeepAuthToken'], (items) => {
if (items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = items.trackeepApiBaseUrl;
}
if (items.trackeepAuthToken) {
authTokenInput.value = items.trackeepAuthToken;
}
});
}
function saveSettings() {
const apiBaseUrl = apiBaseUrlInput.value.trim();
const authToken = authTokenInput.value.trim();
if (!apiBaseUrl) {
showMessage('API base URL is required.', 'error');
return;
}
if (!authToken) {
showMessage('Authentication token is required.', 'error');
return;
}
setButtonLoading(saveBtn, true);
hideMessage();
chrome.storage.sync.set(
{
trackeepApiBaseUrl: apiBaseUrl,
trackeepAuthToken: authToken
},
() => {
setButtonLoading(saveBtn, false);
if (chrome.runtime.lastError) {
showMessage(`Failed to save: ${chrome.runtime.lastError.message}`, 'error');
} else {
showMessage(`
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"/>
</svg>
Settings saved successfully! You can now use the extension to save bookmarks and files.
`, 'success');
}
}
);
}
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
detectAndPrefillApiBaseUrl(() => {
loadSettings();
saveBtn.addEventListener('click', (e) => {
e.preventDefault();
saveSettings();
});
});
});
+217 -139
View File
@@ -12,6 +12,8 @@
<p align="center"> <p align="center">
<a href="#quick-start">Quick Start</a> <a href="#quick-start">Quick Start</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#screenshots">Screenshots</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#features">Features</a> <a href="#features">Features</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#releases">Releases</a> <a href="#releases">Releases</a>
@@ -27,6 +29,211 @@
<img src="./scorecard.png" alt="Code Quality Score" width="100%"> <img src="./scorecard.png" alt="Code Quality Score" width="100%">
</p> </p>
## 🚀 Quick Start
### Production Deployment with Docker Compose
```bash
git clone https://github.com/dvorinka/trackeep.git
cd trackeep
cp .env.example .env
# Edit .env file with your configuration
docker-compose up -d
```
The `docker-compose.prod.yml` file uses environment variables with sensible defaults:
```yaml
version: '3.8'
services:
trackeep-frontend:
image: 'ghcr.io/dvorinka/trackeep/frontend:latest'
ports:
- '80:80'
- '443:443'
environment:
- NODE_ENV=production
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
depends_on:
- trackeep-backend
restart: unless-stopped
networks:
- trackeep-network
trackeep-backend:
image: 'ghcr.io/dvorinka/trackeep/backend:latest'
ports:
- '8080:8080'
environment:
- PORT=${PORT:-8080}
- GIN_MODE=${GIN_MODE:-release}
- READ_TIMEOUT=${READ_TIMEOUT:-15s}
- WRITE_TIMEOUT=${WRITE_TIMEOUT:-15s}
- IDLE_TIMEOUT=${IDLE_TIMEOUT:-60s}
- SHUTDOWN_TIMEOUT=${SHUTDOWN_TIMEOUT:-30s}
- DB_TYPE=${DB_TYPE:-postgres}
- DB_HOST=${DB_HOST:-postgres}
- 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}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- 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}
- SEARCH_API_PROVIDER=${SEARCH_API_PROVIDER:-demo}
- SEARCH_RESULTS_LIMIT=${SEARCH_RESULTS_LIMIT:-10}
- SEARCH_CACHE_TTL=${SEARCH_CACHE_TTL:-300}
- SEARCH_RATE_LIMIT=${SEARCH_RATE_LIMIT:-100}
- 'OAUTH_SERVICE_URL=${OAUTH_SERVICE_URL:-http://localhost:9090}'
- AUTO_UPDATE_CHECK=${AUTO_UPDATE_CHECK:-false}
- UPDATE_CHECK_INTERVAL=${UPDATE_CHECK_INTERVAL:-24h}
- PRERELEASE_UPDATES=${PRERELEASE_UPDATES:-false}
volumes:
- './data:/data'
- './uploads:/app/uploads'
- './logs:/app/logs'
- '/var/run/docker.sock:/var/run/docker.sock'
restart: unless-stopped
networks:
- trackeep-network
healthcheck:
test:
- CMD
- wget
- '--no-verbose'
- '--tries=1'
- '--spider'
- 'http://localhost:8080/health'
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
postgres:
image: 'postgres:15-alpine'
environment:
POSTGRES_DB: ${POSTGRES_DB:-trackeep}
POSTGRES_USER: ${POSTGRES_USER:-trackeep}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- 'postgres_data:/var/lib/postgresql/data'
restart: unless-stopped
networks:
- trackeep-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-trackeep} -d ${POSTGRES_DB:-trackeep}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data: null
networks:
trackeep-network:
driver: bridge
```
### Service Architecture
Trackeep production deployment consists of **3 essential services**:
#### **🎯 Frontend Service**
- **Image**: `ghcr.io/dvorinka/trackeep/frontend:latest`
- **Ports**: `80:80`, `443:443`
- **Purpose**: Web interface and user experience
- **Health**: Depends on backend service
#### **🔧 Backend Service**
- **Image**: `ghcr.io/dvorinka/trackeep/backend:latest`
- **Ports**: `8080:8080`
- **Purpose**: API server and business logic
- **Health**: Built-in health check endpoint
#### **🗄️ Database Service**
- **Image**: `postgres:15-alpine`
- **Purpose**: Data persistence and storage
- **Health**: PostgreSQL readiness check
- **Storage**: Persistent volume for data
### Required Environment Variables
Create a `.env` file from the provided `.env.example` and configure these required variables:
```env
# Database Configuration
DB_PASSWORD=your_secure_password
POSTGRES_PASSWORD=your_secure_password
# Security Configuration
JWT_SECRET=your_jwt_secret_key
ENCRYPTION_KEY=your_32_character_encryption_key
```
### AI Services Configuration
AI services are now configured **only within the Trackeep application**. No environment variables are needed for AI configuration. Simply:
1. Start Trackeep with the basic configuration above
2. Navigate to Settings → AI Services in the application
3. Add your API tokens and configure AI providers in the app interface
4. Enable/disable AI services as needed through the app settings
### Version Management
Trackeep uses GitHub Docker images with the `:latest` tag. The application version is automatically managed through the Docker image tags and update checking is handled through the OAuth service. No manual version configuration is needed in the environment variables.
### Version Detection
The system automatically detects the running version through multiple methods:
- **Docker Detection**: Identifies container image tags
- **Environment Variables**: Uses `TRACKEEP_VERSION` if set
- **Version Files**: Reads from `/app/VERSION` or similar
- **Git Tags**: Detects version when running from source
Access version information via the API:
```bash
curl http://localhost:8080/api/version
```
All other variables have sensible defaults and can be configured as needed.
## Screenshots
### Dashboard
<!-- TODO: Add dashboard screenshot -->
<p align="center">
<img src="./screenshots/dashboard.png" alt="Dashboard Screenshot" width="800">
</p>
### Bookmarks & Links
<!-- TODO: Add bookmarks screenshot -->
<p align="center">
<img src="./screenshots/bookmarks.png" alt="Bookmarks Screenshot" width="800">
</p>
### Task Management
<!-- TODO: Add tasks screenshot -->
<p align="center">
<img src="./screenshots/tasks.png" alt="Tasks Screenshot" width="800">
</p>
### File Storage & Media
<!-- TODO: Add file storage screenshot -->
<p align="center">
<img src="./screenshots/files.png" alt="Files Screenshot" width="800">
</p>
### Mobile App
<!-- TODO: Add mobile screenshot -->
<p align="center">
<img src="./screenshots/mobile.png" alt="Mobile App Screenshot" width="400">
</p>
## Introduction ## Introduction
@@ -189,6 +396,7 @@ DISABLE_CHINESE_AI=true
### Prerequisites ### Prerequisites
- Docker and Docker Compose - Docker and Docker Compose
- Git - Git
- GitHub CLI (optional, for creating releases): `sudo apt install gh` or `sudo snap install gh`
### Installation with Docker (Recommended) ### Installation with Docker (Recommended)
@@ -218,40 +426,6 @@ DISABLE_CHINESE_AI=true
- Backend API: http://localhost:8080 - Backend API: http://localhost:8080
- Health Check: http://localhost:8080/health - Health Check: http://localhost:8080/health
### Docker Updates (Easy Way)
Trackeep now supports automatic Docker updates! Instead of rebuilding from source, you can pull pre-built images:
#### **Method 1: Quick Update Script**
```bash
./update.sh
```
#### **Method 2: Using Published Images**
```bash
docker compose -f docker-compose.published.yml pull
docker compose -f docker-compose.published.yml up -d
```
#### **Method 3: Manual Pull**
```bash
docker pull ghcr.io/Dvorinka/trackeep/backend:latest
docker pull ghcr.io/Dvorinka/trackeep/frontend:latest
docker compose up -d
```
### Available Docker Images
Pre-built images are automatically published to GitHub Container Registry:
- `ghcr.io/Dvorinka/trackeep/backend:latest`
- `ghcr.io/Dvorinka/trackeep/frontend:latest`
**Benefits:**
- 🚀 **Faster updates** - No need to build from source
- 🔄 **Automatic builds** - Images published on every push to main
- 📦 **Version control** - Images tagged with commit SHAs and branches
- 🛡️ **Stable releases** - Tested images ready for production
### Demo Login ### Demo Login
- Email: `demo@trackeep.com` - Email: `demo@trackeep.com`
- Password: `password` - Password: `password`
@@ -306,6 +480,7 @@ Comprehensive documentation is available in the `/docs` directory:
- **[User Guide](./docs/USER_GUIDE.md)** Complete user documentation - **[User Guide](./docs/USER_GUIDE.md)** Complete user documentation
- **[API Documentation](./docs/API.md)** REST API reference - **[API Documentation](./docs/API.md)** REST API reference
- **[AI Assistant Features](./docs/AI_ASSISTANT.md)** AI-powered features guide - **[AI Assistant Features](./docs/AI_ASSISTANT.md)** AI-powered features guide
- **[Release Guide](./docs/RELEASE_GUIDE.md)** Creating releases and version management
Additional documentation files: Additional documentation files:
- **[Development Guide](./docs/DEVELOPMENT.md)** Development setup and guidelines - **[Development Guide](./docs/DEVELOPMENT.md)** Development setup and guidelines
@@ -395,14 +570,6 @@ GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret GITHUB_CLIENT_SECRET=your_github_client_secret
``` ```
**AI Configuration Notes:**
- All AI services are optional Trackeep works perfectly without any AI
- Mix and match services based on your budget and privacy preferences
- Chinese AI services (DeepSeek, LongCat) offer great pricing but consider your privacy needs
- European option (Mistral) for GDPR-compliant AI processing
- Local AI (Ollama) for complete offline privacy
- Custom endpoints supported for maximum flexibility
## 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.
@@ -453,106 +620,17 @@ This project is built with amazing open-source technologies:
- **Frontend**: SolidJS, UnoCSS, Kobalte, TanStack Query - **Frontend**: SolidJS, UnoCSS, Kobalte, TanStack Query
- **Backend**: Go, Gin, GORM, PostgreSQL - **Backend**: Go, Gin, GORM, PostgreSQL
- **Mobile**: React Native, React Navigation - **Mobile**: React Native, React Navigation
- **DevOps**: Docker, GitHub Actions - **DevOps**: Docker
### Creating Releases
## 🚀 Releases & Updates For detailed release creation instructions, see **[Release Guide](./docs/RELEASE_GUIDE.md)**.
Trackeep uses automated semantic versioning and Docker-based updates. No manual setup required! The guide covers:
- GitHub CLI workflow (recommended)
### 📋 How Updates Work - Manual release scripts
- Semantic versioning
Users get updates automatically through the built-in update system: - Release notes templates
- ✅ **Auto-checks** every 24 hours for new versions
- ✅ **UI notifications** appear in left navigation when updates available
- ✅ **One-click install** pulls latest Docker images and restarts services
- ✅ **Zero setup** - just run `docker compose up` and it works
### 🏷️ Version Management
Versions are managed automatically through semantic versioning (MAJOR.MINOR.PATCH):
- **Frontend**: Version from `frontend/package.json`
- **Backend**: Version from `backend/go.mod`
- **Detection**: Automatic from source code (no env vars needed)
### 🚀 Creating Releases
#### Method 1: Automated (Recommended)
For new features or bug fixes:
```bash
# 1. Commit your changes
git commit -m "feat: add new amazing feature"
# 2. Create version tag and push (triggers automated release)
git tag v1.2.7
git push origin main v1.2.7
```
**What happens automatically:**
1. GitHub Actions detects the version tag
2. Updates all version files (`package.json`, `go.mod`, docker-compose files)
3. Builds Docker images with proper semantic tags
4. Pushes to GitHub Container Registry (`latest` + versioned tags)
5. Creates GitHub release with changelog
6. Updates `latest` tags to point to new version
#### Method 2: Manual
For precise control:
```bash
# Use version update script
./scripts/update-version.sh 1.2.7
# Commit and push
git add . && git commit -m "chore: bump version to 1.2.7"
git push origin main
```
### 🐳 Docker Images
Images are automatically built and pushed to GitHub Container Registry:
- **Registry**: `ghcr.io/dvorinka/trackeep`
- **Latest tags**: `backend:latest`, `frontend:latest` (for updates)
- **Versioned tags**: `backend:1.2.6`, `frontend:1.2.6` (for rollback)
- **Automatic builds**: Triggered by Git tags
### 📖 Semantic Versioning
Follow industry standard (MAJOR.MINOR.PATCH):
```
1.2.6 → 1.3.0 (MINOR: new features)
1.2.6 → 1.2.7 (PATCH: bug fixes)
1.2.6 → 2.0.0 (MAJOR: breaking changes)
```
### 🔧 Development Setup
```bash
# Clone and run
git clone https://github.com/Dvorinka/Trackeep.git
cd Trackeep
# Start with automatic updates
docker compose up
# System automatically:
# - Detects version from source code
# - Checks for updates every 24h
# - Shows update notifications in UI
# - Installs updates with one click
```
### 📚 Documentation
- **Version workflow**: See [docs/SIMPLIFIED_VERSION_SYSTEM.md](docs/SIMPLIFIED_VERSION_SYSTEM.md)
- **API documentation**: See [docs/API.md](docs/API.md)
- **Update system**: See [docs/AUTO_UPDATE_GUIDE.md](docs/AUTO_UPDATE_GUIDE.md)
## A Personal Note ## A Personal Note
-172
View File
@@ -1,172 +0,0 @@
# 🎉 Trackeep v1.2.5 Release Complete!
## ✅ What We Accomplished
### **🏷️ Proper Semantic Versioning**
- ✅ Created version `v1.2.5` following MAJOR.MINOR.PATCH format
- ✅ Git tag created: `v1.2.5`
- ✅ Version pushed to origin
- ✅ Ready for GitHub Actions automated builds
### **🔄 Complete Update System**
-**No OAuth required** - removed authentication dependency
-**Docker-based** - uses container registry pulls
-**Latest tags** - always gets newest versions
-**Integrated UI** - update notifications in left navigation
-**Auto-checking** - every 24 hours in background
-**One-click updates** - users can update directly from UI
### **🐳 Docker Configuration**
-**docker-compose.yml** - local builds with version variables
-**docker-compose.prod.yml** - production with latest images
-**Version environment** - `APP_VERSION` passed to containers
-**Docker socket** - mounted for in-container updates
### **🚀 Automated Release Workflow**
-**GitHub Actions** - `.github/workflows/release.yml`
-**Semantic version extraction** - from Git tags
-**Multi-arch builds** - backend and frontend matrix
-**Docker registry push** - automatic with version tags
-**GitHub releases** - automated creation
-**SBOM generation** - security and compliance
### **📋 Documentation Created**
-**VERSION_WORKFLOW.md** - complete versioning guide
-**Release script** - manual release automation
-**Update guides** - user documentation
## 🎯 How Users Get Updates
### **Current Experience:**
```bash
# User just runs:
docker compose up
# System automatically:
# 1. Sets APP_VERSION from environment
# 2. Checks for updates every 24h
# 3. Shows update button in left nav
# 4. Pulls latest images when clicked
# 5. Restarts services automatically
```
### **Version Detection:**
- **Backend**: Reads `APP_VERSION` environment variable
- **Frontend**: Reads `VITE_APP_VERSION` from build
- **Comparison**: Current vs `latest` tag in registry
### **Update Flow:**
1. **Background check** → API call to `/api/updates/check`
2. **Version compare** → Semantic version comparison
3. **UI notification** → Update button appears in left sidebar
4. **User action** → Click to install update
5. **Docker pull** → Backend pulls `latest` images
6. **Service restart** → Automatic with new images
## 📦 Release Strategy
### **Tag Management:**
```
ghcr.io/dvorinka/trackeep/backend:latest ← Always newest
ghcr.io/dvorinka/trackeep/backend:1.2.5 ← This release
ghcr.io/dvorinka/trackeep/backend:1.2.4 ← Previous release
ghcr.io/dvorinka/trackeep/frontend:latest ← Always newest
ghcr.io/dvorinka/trackeep/frontend:1.2.5 ← This release
ghcr.io/dvorinka/trackeep/frontend:1.2.4 ← Previous release
```
### **Semantic Version Rules:**
```
1.2.5 → 1.3.0 (MINOR: new features)
1.2.5 → 1.2.6 (PATCH: bug fixes)
1.2.5 → 2.0.0 (MAJOR: breaking changes)
```
## 🔄 Future Release Process
### **Automated (Recommended):**
```bash
# 1. Make changes
git commit -m "feat: add new feature"
# 2. Bump version (semantic-release will handle)
# 3. Push to trigger GitHub Actions
git push origin main
# 4. GitHub Actions automatically:
# - Builds Docker images
# - Pushes to registry
# - Creates GitHub release
# - Updates documentation
```
### **Manual:**
```bash
# 1. Use release script
./scripts/release.sh 1.2.6
# 2. Or manual process
export APP_VERSION=1.2.6
git tag v1.2.6
git push origin v1.2.6
docker build & push
```
## ✨ Industry Best Practices Implemented
### **Version Management:**
- ✅ Semantic versioning (MAJOR.MINOR.PATCH)
- ✅ Environment variable configuration
- ✅ Git tagging with proper format
- ✅ Automated changelog generation
### **Docker Strategy:**
- ✅ Multi-stage builds
- ✅ Layer caching
- ✅ Security scanning (SBOM)
- ✅ Proper tagging (latest + versioned)
### **Release Automation:**
- ✅ GitHub Actions CI/CD
- ✅ Automated testing
- ✅ Artifact management
- ✅ Rollback capability
### **User Experience:**
- ✅ Zero-friction updates
- ✅ Background checking
- ✅ UI notifications
- ✅ One-click installation
- ✅ No authentication required
## 🎊 Next Steps
### **For v1.3.0:**
1. **New features** → Add to backlog
2. **Bug fixes** → Document in commits
3. **Version bump**`1.3.0` (MINOR version)
### **Monitoring:**
1. **Update analytics** → Track update adoption
2. **Error tracking** → Monitor update failures
3. **User feedback** → Collect update experience
---
## 🎉 Release Status: COMPLETE
**Trackeep v1.2.5 is now ready with:**
- ✅ Proper semantic versioning
- ✅ Automated release workflow
- ✅ Docker-based update system
- ✅ Complete user documentation
- ✅ Industry best practices
**Users can now:**
- 🚀 `docker compose up` and get automatic updates
- 🔄 See update notifications in left navigation
- ⚡ Install updates with one click
- 📦 Always get the latest versions
**The update system is production-ready!** 🚀
+1 -1
View File
@@ -41,7 +41,7 @@ type AppConfig struct {
func Load() *Config { func Load() *Config {
return &Config{ return &Config{
Server: ServerConfig{ Server: ServerConfig{
Port: getEnvWithDefault("PORT", "8080"), Port: getEnvWithDefault("BACKEND_PORT", getEnvWithDefault("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),
+245
View File
@@ -2,10 +2,15 @@ package handlers
import ( import (
"crypto/rand" "crypto/rand"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http"
"net/smtp" "net/smtp"
"os" "os"
"os/exec"
"runtime"
"strings" "strings"
"time" "time"
@@ -771,3 +776,243 @@ func formatTimeAgo(t time.Time) string {
return t.Format("Jan 2, 2006") return t.Format("Jan 2, 2006")
} }
} }
// GitHubRelease represents a GitHub release
type GitHubRelease struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
PublishedAt string `json:"published_at"`
Body string `json:"body"`
}
// GetLatestVersion fetches the latest version from GitHub releases
func GetLatestVersion() (string, error) {
// GitHub API endpoint for releases
url := "https://api.github.com/repos/dvorinka/trackeep/releases"
// Create HTTP request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", "Trackeep-Backend")
// Make request
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to fetch releases: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read response: %w", err)
}
// Parse JSON
var releases []GitHubRelease
if err := json.Unmarshal(body, &releases); err != nil {
return "", fmt.Errorf("failed to parse JSON: %w", err)
}
// Find latest non-draft release
for _, release := range releases {
if !release.Draft && !release.Prerelease {
return release.TagName, nil
}
}
// If no stable release found, return the latest release (including prerelease)
if len(releases) > 0 {
return releases[0].TagName, nil
}
return "", errors.New("no releases found")
}
// GetCurrentVersion detects the current running version
func GetCurrentVersion() (string, error) {
// Method 1: Check if running in Docker and get image info
if isRunningInDocker() {
if version, err := getDockerImageVersion(); err == nil && version != "" {
return version, nil
}
}
// Method 2: Check for version file or environment variable
if version := os.Getenv("TRACKEEP_VERSION"); version != "" {
return version, nil
}
// Method 3: Try to read from version file
if version, err := readVersionFile(); err == nil && version != "" {
return version, nil
}
// Method 4: Check git tag if running from source
if version, err := getGitVersion(); err == nil && version != "" {
return version, nil
}
// Fallback: Return build time or unknown
if buildTime := os.Getenv("BUILD_TIME"); buildTime != "" {
return fmt.Sprintf("build-%s", buildTime), nil
}
return "unknown", nil
}
// isRunningInDocker checks if the application is running in a Docker container
func isRunningInDocker() bool {
// Check for .dockerenv file
if _, err := os.Stat("/.dockerenv"); err == nil {
return true
}
// Check for Docker in cgroup
data, err := os.ReadFile("/proc/1/cgroup")
if err != nil {
return false
}
return strings.Contains(string(data), "docker")
}
// getDockerImageVersion gets the Docker image tag
func getDockerImageVersion() (string, error) {
// Try to get container ID from cgroup
containerID, err := getContainerID()
if err != nil {
return "", err
}
// Try to inspect the container to get image info
cmd := exec.Command("docker", "inspect", "--format='{{.Config.Image}}'", containerID)
output, err := cmd.Output()
if err != nil {
return "", err
}
imageName := strings.TrimSpace(string(output))
if strings.Contains(imageName, ":") {
parts := strings.Split(imageName, ":")
if len(parts) > 1 {
tag := parts[len(parts)-1]
// Remove quotes if present
tag = strings.Trim(tag, "'")
return tag, nil
}
}
return "latest", nil
}
// getContainerID attempts to get the current container ID
func getContainerID() (string, error) {
// Method 1: Read from /proc/self/cgroup
data, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
return "", err
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.Contains(line, "docker") {
parts := strings.Split(line, "/")
if len(parts) > 0 {
containerID := parts[len(parts)-1]
// Remove any non-hex characters
containerID = strings.Trim(containerID, " \t\r\n")
if len(containerID) >= 12 {
return containerID[:12], nil
}
}
}
}
// Method 2: Try to get from hostname
hostname, err := os.Hostname()
if err == nil && len(hostname) >= 12 {
return hostname[:12], nil
}
return "", errors.New("could not determine container ID")
}
// readVersionFile tries to read version from a file
func readVersionFile() (string, error) {
// Try multiple possible version file locations
versionFiles := []string{
"/app/VERSION",
"/app/version.txt",
"./VERSION",
"./version.txt",
}
for _, file := range versionFiles {
if data, err := os.ReadFile(file); err == nil {
return strings.TrimSpace(string(data)), nil
}
}
return "", errors.New("no version file found")
}
// getGitVersion gets version from git tag
func getGitVersion() (string, error) {
if runtime.GOOS == "windows" {
return "", errors.New("git version detection not supported on Windows")
}
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
output, err := cmd.Output()
if err != nil {
return "", err
}
version := strings.TrimSpace(string(output))
return strings.TrimPrefix(version, "v"), nil
}
// GetVersionHandler returns the current and latest version
func GetVersionHandler(c *gin.Context) {
latestVersion, err := GetLatestVersion()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to fetch latest version",
"details": err.Error(),
})
return
}
// Get current running version
currentVersion, err := GetCurrentVersion()
if err != nil {
currentVersion = "unknown"
}
// Clean the version tag (remove 'v' prefix if present)
cleanLatestVersion := strings.TrimPrefix(latestVersion, "v")
response := gin.H{
"current_version": currentVersion,
"latest_version": cleanLatestVersion,
"latest_tag": latestVersion, // Keep the original tag for reference
"is_latest": currentVersion == cleanLatestVersion || currentVersion == "latest",
"update_available": currentVersion != cleanLatestVersion && currentVersion != "latest",
"running_in_docker": isRunningInDocker(),
}
c.JSON(http.StatusOK, response)
}
+437
View File
@@ -0,0 +1,437 @@
package handlers
import (
"archive/zip"
"crypto/rand"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/config"
"github.com/trackeep/backend/models"
)
// CreateAPIKeyRequest represents a request to create an API key
type CreateAPIKeyRequest struct {
Name string `json:"name" binding:"required,min=1,max=100"`
Permissions []string `json:"permissions" binding:"required"`
ExpiresIn *int `json:"expires_in,omitempty"` // Days until expiration
}
// APIKeyResponse represents API key response
type APIKeyResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Key string `json:"key"`
Permissions []string `json:"permissions"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// BrowserExtensionAuth represents browser extension authentication
type BrowserExtensionAuth struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null"`
ExtensionID string `json:"extension_id" gorm:"not null"`
Name string `json:"name" gorm:"not null"`
IsActive bool `json:"is_active" gorm:"default:true"`
LastSeen *time.Time `json:"last_seen,omitempty" gorm:"not null"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
// GenerateAPIKey creates a new API key for browser extension
func GenerateAPIKey(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "User not authenticated"})
return
}
currentUser := user.(models.User)
var req CreateAPIKeyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Validate permissions
validPermissions := map[string]bool{
"bookmarks:read": true,
"bookmarks:write": true,
"files:read": true,
"files:write": true,
"notes:read": true,
"notes:write": true,
"tasks:read": true,
"tasks:write": true,
}
for _, perm := range req.Permissions {
if !validPermissions[perm] {
c.JSON(400, gin.H{"error": fmt.Sprintf("Invalid permission: %s", perm)})
return
}
}
// Generate API key
key := generateAPIKey()
// Set expiration if provided
var expiresAt *time.Time
if req.ExpiresIn != nil && *req.ExpiresIn > 0 {
expiration := time.Now().AddDate(0, 0, *req.ExpiresIn)
expiresAt = &expiration
}
// Create API key record
apiKey := models.APIKey{
Name: req.Name,
Key: key,
UserID: currentUser.ID,
Permissions: req.Permissions,
IsActive: true,
ExpiresAt: expiresAt,
}
db := config.GetDB()
if err := db.Create(&apiKey).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to create API key"})
return
}
response := APIKeyResponse{
ID: apiKey.ID,
Name: apiKey.Name,
Key: apiKey.Key,
Permissions: apiKey.Permissions,
ExpiresAt: apiKey.ExpiresAt,
CreatedAt: apiKey.CreatedAt,
}
c.JSON(201, response)
}
// GetAPIKeys retrieves user's API keys
func GetAPIKeys(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "User not authenticated"})
return
}
currentUser := user.(models.User)
var apiKeys []models.APIKey
db := config.GetDB()
if err := db.Where("user_id = ? AND is_active = ?", currentUser.ID, true).Order("created_at desc").Find(&apiKeys).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to retrieve API keys"})
return
}
// Don't return the actual keys in list view
var response []map[string]interface{}
for _, key := range apiKeys {
response = append(response, map[string]interface{}{
"id": key.ID,
"name": key.Name,
"permissions": key.Permissions,
"is_active": key.IsActive,
"last_used": key.LastUsed,
"expires_at": key.ExpiresAt,
"created_at": key.CreatedAt,
"updated_at": key.UpdatedAt,
})
}
c.JSON(200, response)
}
// RevokeAPIKey revokes an API key
func RevokeAPIKey(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "User not authenticated"})
return
}
currentUser := user.(models.User)
keyID := c.Param("id")
db := config.GetDB()
var apiKey models.APIKey
if err := db.Where("id = ? AND user_id = ?", keyID, currentUser.ID).First(&apiKey).Error; err != nil {
c.JSON(404, gin.H{"error": "API key not found"})
return
}
// Deactivate the key
if err := db.Model(&apiKey).Update("is_active", false).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to revoke API key"})
return
}
c.JSON(200, gin.H{"message": "API key revoked successfully"})
}
// ValidateAPIKey validates an API key from browser extension
func ValidateAPIKey(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(401, gin.H{"error": "Authorization header required"})
return
}
// Extract Bearer token
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(401, gin.H{"error": "Invalid authorization format"})
return
}
apiKey := parts[1]
db := config.GetDB()
var keyRecord models.APIKey
if err := db.Where("key = ? AND is_active = ?", apiKey, true).Preload("User").First(&keyRecord).Error; err != nil {
c.JSON(401, gin.H{"error": "Invalid API key"})
return
}
// Check expiration
if keyRecord.ExpiresAt != nil && keyRecord.ExpiresAt.Before(time.Now()) {
c.JSON(401, gin.H{"error": "API key expired"})
return
}
// Update last used timestamp
now := time.Now()
keyRecord.LastUsed = &now
db.Model(&keyRecord).Update("last_used", now)
// Return user info for extension
c.JSON(200, gin.H{
"valid": true,
"user_id": keyRecord.UserID,
"permissions": keyRecord.Permissions,
})
}
// generateAPIKey generates a secure API key
func generateAPIKey() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
keyLength := 32
bytes := make([]byte, keyLength)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = charset[b%byte(len(charset))]
}
return "tk_" + string(bytes)
}
// RegisterBrowserExtension registers a browser extension instance
func RegisterBrowserExtension(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "User not authenticated"})
return
}
currentUser := user.(models.User)
var req struct {
ExtensionID string `json:"extension_id" binding:"required"`
Name string `json:"name" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Check if extension already registered
db := config.GetDB()
var existingAuth BrowserExtensionAuth
if err := db.Where("user_id = ? AND extension_id = ?", currentUser.ID, req.ExtensionID).First(&existingAuth).Error; err == nil {
c.JSON(409, gin.H{"error": "Extension already registered"})
return
}
// Create new extension registration
extAuth := BrowserExtensionAuth{
UserID: currentUser.ID,
ExtensionID: req.ExtensionID,
Name: req.Name,
IsActive: true,
LastSeen: &time.Time{},
}
if err := db.Create(&extAuth).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to register extension"})
return
}
c.JSON(201, gin.H{
"message": "Extension registered successfully",
"extension_id": extAuth.ExtensionID,
})
}
// GetBrowserExtensions retrieves user's registered browser extensions
func GetBrowserExtensions(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "User not authenticated"})
return
}
currentUser := user.(models.User)
var extensions []BrowserExtensionAuth
db := config.GetDB()
if err := db.Where("user_id = ?", currentUser.ID).Order("created_at desc").Find(&extensions).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to retrieve extensions"})
return
}
c.JSON(200, extensions)
}
// RevokeBrowserExtension revokes a browser extension
func RevokeBrowserExtension(c *gin.Context) {
user, exists := c.Get("user")
if !exists {
c.JSON(401, gin.H{"error": "User not authenticated"})
return
}
currentUser := user.(models.User)
extensionID := c.Param("id")
db := config.GetDB()
var extAuth BrowserExtensionAuth
if err := db.Where("extension_id = ? AND user_id = ?", extensionID, currentUser.ID).First(&extAuth).Error; err != nil {
c.JSON(404, gin.H{"error": "Extension not found"})
return
}
// Deactivate the extension
if err := db.Model(&extAuth).Update("is_active", false).Error; err != nil {
c.JSON(500, gin.H{"error": "Failed to revoke extension"})
return
}
c.JSON(200, gin.H{"message": "Extension revoked successfully"})
}
// DownloadBrowserExtension serves the browser extension as a downloadable zip file
func DownloadBrowserExtension(c *gin.Context) {
// Path to the browser extension directory
extDir := "../browser-extension"
// Create a temporary zip file
zipPath := "/tmp/browser-extension.zip"
// Create zip file
err := createZip(extDir, zipPath)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to create zip file"})
return
}
// Open the zip file
zipFile, err := os.Open(zipPath)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to open zip file"})
return
}
defer zipFile.Close()
// Get file info
fileInfo, err := zipFile.Stat()
if err != nil {
c.JSON(500, gin.H{"error": "Failed to get file info"})
return
}
// Set headers for download
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", "attachment; filename=browser-extension.zip")
c.Header("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
// Copy file to response
io.Copy(c.Writer, zipFile)
// Clean up temporary file
os.Remove(zipPath)
}
// createZip creates a zip file from a directory
func createZip(source, target string) error {
zipfile, err := os.Create(target)
if err != nil {
return err
}
defer zipfile.Close()
archive := zip.NewWriter(zipfile)
defer archive.Close()
info, err := os.Stat(source)
if err != nil {
return nil
}
var baseDir string
if info.IsDir() {
baseDir = filepath.Base(source)
}
return filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
if baseDir != "" {
header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
}
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
return err
})
}
+74 -2
View File
@@ -49,10 +49,17 @@ type AttachmentInput struct {
Title string `json:"title"` Title string `json:"title"`
} }
type ReferenceInput struct {
EntityType string `json:"entity_type"`
EntityID uint `json:"entity_id"`
DeepLink string `json:"deep_link"`
}
type CreateMessageRequest struct { type CreateMessageRequest struct {
Body string `json:"body"` Body string `json:"body"`
Attachments []AttachmentInput `json:"attachments"` Attachments []AttachmentInput `json:"attachments"`
Metadata map[string]interface{} `json:"metadata"` Metadata map[string]interface{} `json:"metadata"`
References []ReferenceInput `json:"references"`
} }
type UpdateMessageRequest struct { type UpdateMessageRequest struct {
@@ -641,8 +648,8 @@ func CreateConversationMessage(c *gin.Context) {
} }
trimmedBody := strings.TrimSpace(req.Body) trimmedBody := strings.TrimSpace(req.Body)
if trimmedBody == "" && len(req.Attachments) == 0 { if trimmedBody == "" && len(req.Attachments) == 0 && len(req.References) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Message body or attachments are required"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Message body, attachments, or references are required"})
return return
} }
@@ -656,6 +663,37 @@ func CreateConversationMessage(c *gin.Context) {
}) })
} }
referenceRows := make([]models.MessageReference, 0, len(req.References))
for _, ref := range req.References {
entityType := normalizeReferenceType(ref.EntityType)
if entityType == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reference entity_type"})
return
}
if ref.EntityID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reference entity_id"})
return
}
deepLink := strings.TrimSpace(ref.DeepLink)
if deepLink == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reference deep_link"})
return
}
if !isReferenceDeepLinkAllowed(deepLink) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported reference deep_link"})
return
}
if !canReferenceEntity(models.DB, userID, entityType, ref.EntityID) {
c.JSON(http.StatusForbidden, gin.H{"error": "Reference target is not accessible"})
return
}
referenceRows = append(referenceRows, models.MessageReference{
EntityType: entityType,
EntityID: ref.EntityID,
DeepLink: deepLink,
})
}
suggestions, inferredAttachments, isSensitive := services.DetectMessageContent(trimmedBody) suggestions, inferredAttachments, isSensitive := services.DetectMessageContent(trimmedBody)
for _, inferred := range inferredAttachments { for _, inferred := range inferredAttachments {
if hasAttachment(attachmentRows, inferred.Kind, inferred.URL) { if hasAttachment(attachmentRows, inferred.Kind, inferred.URL) {
@@ -719,6 +757,13 @@ func CreateConversationMessage(c *gin.Context) {
models.DB.Create(&attachmentRows) models.DB.Create(&attachmentRows)
} }
for i := range referenceRows {
referenceRows[i].MessageID = message.ID
}
if len(referenceRows) > 0 {
models.DB.Create(&referenceRows)
}
if len(suggestions) > 0 { if len(suggestions) > 0 {
suggestionRows := make([]models.MessageSuggestion, 0, len(suggestions)) suggestionRows := make([]models.MessageSuggestion, 0, len(suggestions))
for _, s := range suggestions { for _, s := range suggestions {
@@ -2159,6 +2204,33 @@ func normalizeAttachmentKind(kind string) string {
} }
} }
func normalizeReferenceType(entityType string) string {
t := strings.ToLower(strings.TrimSpace(entityType))
switch t {
case "task", "bookmark", "calendar_event", "youtube_video", "learning_path", "saved_search", "github", "password_vault_item", "ai_chat_session", "ai_chat_message":
return t
default:
return ""
}
}
func isReferenceDeepLinkAllowed(deepLink string) bool {
return strings.HasPrefix(deepLink, "/") || strings.HasPrefix(deepLink, "http://") || strings.HasPrefix(deepLink, "https://")
}
func canReferenceEntity(db *gorm.DB, userID uint, entityType string, entityID uint) bool {
switch entityType {
case "ai_chat_session":
var session models.ChatSession
return db.Where("id = ? AND user_id = ?", entityID, userID).First(&session).Error == nil
case "ai_chat_message":
var message models.ChatMessage
return db.Where("id = ? AND user_id = ?", entityID, userID).First(&message).Error == nil
default:
return true
}
}
func compactMessageTitle(text string, limit int) string { func compactMessageTitle(text string, limit int) string {
trimmed := strings.TrimSpace(text) trimmed := strings.TrimSpace(text)
if len(trimmed) <= limit { if len(trimmed) <= limit {
+351 -136
View File
@@ -7,7 +7,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -63,78 +62,176 @@ func SearchWeb(c *gin.Context) {
req.Count = 10 req.Count = 10
} }
apiKey := os.Getenv("BRAVE_API_KEY") // Get user ID from context (authentication is required)
if apiKey == "" { userID, exists := c.Get("user_id")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"}) if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required for search functionality"})
return return
} }
// Build Brave Search API request // Get user's search settings from database
baseURL := "https://api.search.brave.com/res/v1/web/search" searchSettings, err := GetSearchSettingsForAPI(userID.(int))
q := url.Values{}
q.Set("q", req.Query)
q.Set("count", fmt.Sprint(req.Count))
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get search settings"})
return
}
reqHTTP.Header.Set("Accept", "application/json")
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
resp, err := http.DefaultClient.Do(reqHTTP)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave Search API"})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave API error: %d", resp.StatusCode)})
return return
} }
var braveResp BraveSearchResponse // Check if user has search API key configured
if err := json.NewDecoder(resp.Body).Decode(&braveResp); err != nil { if searchSettings.SearchAPIProvider == "brave" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave response"}) apiKey := searchSettings.BraveAPIKey
return if apiKey == "" {
} c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
return
}
// Prefer web.results, fall back to mixed.results // Build Brave Search API request
resultsRaw := braveResp.Web.Results baseURL := "https://api.search.brave.com/res/v1/web/search"
if len(resultsRaw) == 0 { q := url.Values{}
resultsRaw = braveResp.Mixed.Results q.Set("q", req.Query)
} q.Set("count", fmt.Sprint(req.Count))
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
results := make([]BraveSearchResult, 0, len(resultsRaw)) reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
for _, r := range resultsRaw { if err != nil {
title, _ := r["title"].(string) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
urlStr, _ := r["url"].(string) return
desc, _ := r["description"].(string) }
lang, _ := r["language"].(string) reqHTTP.Header.Set("Accept", "application/json")
pageAge, _ := r["page_age"].(string) reqHTTP.Header.Set("X-Subscription-Token", apiKey)
results = append(results, BraveSearchResult{ resp, err := http.DefaultClient.Do(reqHTTP)
Title: title, if err != nil {
URL: urlStr, c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave Search API"})
Description: desc, return
PublishedDate: pageAge, }
Language: lang, defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave API error: %d", resp.StatusCode)})
return
}
var braveResp BraveSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&braveResp); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave response"})
return
}
// Prefer web.results, fall back to mixed.results
resultsRaw := braveResp.Web.Results
if len(resultsRaw) == 0 {
resultsRaw = braveResp.Mixed.Results
}
results := make([]BraveSearchResult, 0, len(resultsRaw))
for _, r := range resultsRaw {
title, _ := r["title"].(string)
urlStr, _ := r["url"].(string)
desc, _ := r["description"].(string)
lang, _ := r["language"].(string)
pageAge, _ := r["page_age"].(string)
results = append(results, BraveSearchResult{
Title: title,
URL: urlStr,
Description: desc,
PublishedDate: pageAge,
Language: lang,
})
}
c.JSON(http.StatusOK, gin.H{
"results": results,
"query": gin.H{
"original": braveResp.Query.Original,
"display": braveResp.Query.Display,
},
"count": len(results),
}) })
return
} }
c.JSON(http.StatusOK, gin.H{ // Use the configured provider
"results": results, if searchSettings.SearchAPIProvider == "brave" {
"query": gin.H{ apiKey := searchSettings.BraveAPIKey
"original": braveResp.Query.Original, if apiKey == "" {
"display": braveResp.Query.Display, c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
}, return
"count": len(results), }
})
// Build Brave Search API request
baseURL := searchSettings.BraveSearchBaseURL
q := url.Values{}
q.Set("q", req.Query)
q.Set("count", fmt.Sprint(req.Count))
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
return
}
reqHTTP.Header.Set("Accept", "application/json")
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
resp, err := http.DefaultClient.Do(reqHTTP)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave Search API"})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave API error: %d", resp.StatusCode)})
return
}
var braveResp BraveSearchResponse
if err := json.NewDecoder(resp.Body).Decode(&braveResp); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave response"})
return
}
// Prefer web.results, fall back to mixed.results
resultsRaw := braveResp.Web.Results
if len(resultsRaw) == 0 {
resultsRaw = braveResp.Mixed.Results
}
results := make([]BraveSearchResult, 0, len(resultsRaw))
for _, r := range resultsRaw {
title, _ := r["title"].(string)
urlStr, _ := r["url"].(string)
desc, _ := r["description"].(string)
lang, _ := r["language"].(string)
pageAge, _ := r["page_age"].(string)
results = append(results, BraveSearchResult{
Title: title,
URL: urlStr,
Description: desc,
PublishedDate: pageAge,
Language: lang,
})
}
c.JSON(http.StatusOK, gin.H{
"results": results,
"query": gin.H{
"original": braveResp.Query.Original,
"display": braveResp.Query.Display,
},
"count": len(results),
})
} else if searchSettings.SearchAPIProvider == "serper" {
// TODO: Implement Serper API integration
c.JSON(http.StatusNotImplemented, gin.H{"error": "Serper API integration not yet implemented"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid search API provider configured. Please configure a search API provider in settings."})
}
} }
// SearchNews handles POST /api/v1/search/news
func SearchNews(c *gin.Context) { func SearchNews(c *gin.Context) {
fmt.Printf("DEBUG: SearchNews function called\n") fmt.Printf("DEBUG: SearchNews function called\n")
var req struct { var req struct {
@@ -151,97 +248,215 @@ func SearchNews(c *gin.Context) {
req.Count = 10 req.Count = 10
} }
apiKey := os.Getenv("BRAVE_API_KEY") // Get user ID from context (authentication is required)
if apiKey == "" { userID, exists := c.Get("user_id")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"}) if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required for search functionality"})
return return
} }
baseURL := "https://api.search.brave.com/res/v1/news/search" // Get user's search settings from database
q := url.Values{} searchSettings, err := GetSearchSettingsForAPI(userID.(int))
q.Set("q", req.Query)
q.Set("count", fmt.Sprint(req.Count))
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get search settings"})
return
}
reqHTTP.Header.Set("Accept", "application/json")
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
resp, err := http.DefaultClient.Do(reqHTTP)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave News API"})
return return
} }
if resp.StatusCode != http.StatusOK { // Check if user has search API key configured
resp.Body.Close() if searchSettings.SearchAPIProvider == "brave" {
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave News API error: %d", resp.StatusCode)}) apiKey := searchSettings.BraveAPIKey
return if apiKey == "" {
} c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
return
// Read the response body for debugging
bodyBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"})
return
}
fmt.Printf("DEBUG: Raw Brave News API response: %s\n", string(bodyBytes))
var braveResp BraveNewsResponse
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&braveResp); err != nil {
fmt.Printf("DEBUG: JSON decode error: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave news response"})
return
}
// Debug logging
fmt.Printf("DEBUG: Parsed BraveNewsResponse: %+v\n", braveResp)
fmt.Printf("DEBUG: Number of results: %d\n", len(braveResp.Results))
resultsRaw := braveResp.Results
results := make([]BraveSearchResult, 0, len(resultsRaw))
for _, r := range resultsRaw {
title, _ := r["title"].(string)
urlStr, _ := r["url"].(string)
desc, _ := r["description"].(string)
lang, _ := r["language"].(string)
pubDate, _ := r["published_date"].(string)
if pubDate == "" {
pubDate, _ = r["page_age"].(string)
} }
results = append(results, BraveSearchResult{ baseURL := "https://api.search.brave.com/res/v1/news/search"
Title: title, q := url.Values{}
URL: urlStr, q.Set("q", req.Query)
Description: desc, q.Set("count", fmt.Sprint(req.Count))
PublishedDate: pubDate, endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
Language: lang,
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
return
}
reqHTTP.Header.Set("Accept", "application/json")
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
resp, err := http.DefaultClient.Do(reqHTTP)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave News API"})
return
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave News API error: %d", resp.StatusCode)})
return
}
// Read the response body for debugging
bodyBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"})
return
}
fmt.Printf("DEBUG: Raw Brave News API response: %s\n", string(bodyBytes))
var braveResp BraveNewsResponse
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&braveResp); err != nil {
fmt.Printf("DEBUG: JSON decode error: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave news response"})
return
}
// Debug logging
fmt.Printf("DEBUG: Parsed BraveNewsResponse: %+v\n", braveResp)
fmt.Printf("DEBUG: Number of results: %d\n", len(braveResp.Results))
resultsRaw := braveResp.Results
results := make([]BraveSearchResult, 0, len(resultsRaw))
for _, r := range resultsRaw {
title, _ := r["title"].(string)
urlStr, _ := r["url"].(string)
desc, _ := r["description"].(string)
lang, _ := r["language"].(string)
pubDate, _ := r["published_date"].(string)
if pubDate == "" {
pubDate, _ = r["page_age"].(string)
}
results = append(results, BraveSearchResult{
Title: title,
URL: urlStr,
Description: desc,
PublishedDate: pubDate,
Language: lang,
})
}
original := braveResp.Query.Original
display := braveResp.Query.Display
if original == "" {
original = req.Query
}
if display == "" {
display = req.Query
}
c.JSON(http.StatusOK, gin.H{
"results": results,
"query": gin.H{
"original": original,
"display": display,
},
"count": len(results),
}) })
return
} }
original := braveResp.Query.Original // Use the configured provider
display := braveResp.Query.Display if searchSettings.SearchAPIProvider == "brave" {
if original == "" { apiKey := searchSettings.BraveAPIKey
original = req.Query if apiKey == "" {
} c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"})
if display == "" { return
display = req.Query }
}
c.JSON(http.StatusOK, gin.H{ baseURL := "https://api.search.brave.com/res/v1/news/search"
"results": results, q := url.Values{}
"query": gin.H{ q.Set("q", req.Query)
"original": original, q.Set("count", fmt.Sprint(req.Count))
"display": display, endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
},
"count": len(results), reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
}) if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
return
}
reqHTTP.Header.Set("Accept", "application/json")
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
resp, err := http.DefaultClient.Do(reqHTTP)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave News API"})
return
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave News API error: %d", resp.StatusCode)})
return
}
// Read the response body for debugging
bodyBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"})
return
}
fmt.Printf("DEBUG: Raw Brave News API response: %s\n", string(bodyBytes))
var braveResp BraveNewsResponse
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&braveResp); err != nil {
fmt.Printf("DEBUG: JSON decode error: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave news response"})
return
}
// Debug logging
fmt.Printf("DEBUG: Parsed BraveNewsResponse: %+v\n", braveResp)
fmt.Printf("DEBUG: Number of results: %d\n", len(braveResp.Results))
resultsRaw := braveResp.Results
results := make([]BraveSearchResult, 0, len(resultsRaw))
for _, r := range resultsRaw {
title, _ := r["title"].(string)
urlStr, _ := r["url"].(string)
desc, _ := r["description"].(string)
lang, _ := r["language"].(string)
pubDate, _ := r["published_date"].(string)
if pubDate == "" {
pubDate, _ = r["page_age"].(string)
}
results = append(results, BraveSearchResult{
Title: title,
URL: urlStr,
Description: desc,
PublishedDate: pubDate,
Language: lang,
})
}
original := braveResp.Query.Original
display := braveResp.Query.Display
if original == "" {
original = req.Query
}
if display == "" {
display = req.Query
}
c.JSON(http.StatusOK, gin.H{
"results": results,
"query": gin.H{
"original": original,
"display": display,
},
"count": len(results),
})
} else if searchSettings.SearchAPIProvider == "serper" {
// TODO: Implement Serper API integration for news
c.JSON(http.StatusNotImplemented, gin.H{"error": "Serper API integration not yet implemented"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid search API provider configured. Please configure a search API provider in settings."})
}
} }
// GetSearchSuggestions handles GET /api/v1/search/suggestions // GetSearchSuggestions handles GET /api/v1/search/suggestions
+184
View File
@@ -0,0 +1,184 @@
package handlers
import (
"net/http"
"os"
"strconv"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/models"
)
// SearchSettings represents search API configuration
type SearchSettings struct {
BraveAPIKey string `json:"brave_api_key"`
BraveSearchBaseURL string `json:"brave_search_base_url"`
SerperAPIKey string `json:"serper_api_key"`
SerperBaseURL string `json:"serper_base_url"`
SearchAPIProvider string `json:"search_api_provider"`
SearchResultsLimit int `json:"search_results_limit"`
SearchCacheTTL int `json:"search_cache_ttl"`
SearchRateLimit int `json:"search_rate_limit"`
}
// GetSearchSettings handles GET /api/v1/auth/search/settings
func GetSearchSettings(c *gin.Context) {
userID := c.GetInt("user_id")
// Get settings from database
settings, err := models.GetUserSearchSettings(uint(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"})
return
}
// Convert to response format
response := SearchSettings{
BraveSearchBaseURL: settings.BraveSearchBaseURL,
SerperBaseURL: settings.SerperBaseURL,
SearchAPIProvider: settings.SearchAPIProvider,
SearchResultsLimit: settings.SearchResultsLimit,
SearchCacheTTL: settings.SearchCacheTTL,
SearchRateLimit: settings.SearchRateLimit,
}
// Mask API keys for security
if settings.BraveAPIKey != "" && len(settings.BraveAPIKey) > 8 {
response.BraveAPIKey = settings.BraveAPIKey[:4] + "********" + settings.BraveAPIKey[len(settings.BraveAPIKey)-4:]
}
if settings.SerperAPIKey != "" && len(settings.SerperAPIKey) > 8 {
response.SerperAPIKey = settings.SerperAPIKey[:4] + "********" + settings.SerperAPIKey[len(settings.SerperAPIKey)-4:]
}
c.JSON(http.StatusOK, response)
}
// UpdateSearchSettings handles PUT /api/v1/auth/search/settings
func UpdateSearchSettings(c *gin.Context) {
userID := c.GetInt("user_id")
var newSettings SearchSettings
if err := c.ShouldBindJSON(&newSettings); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get existing settings to preserve API keys if they're masked
existingSettings, err := models.GetUserSearchSettings(uint(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get existing settings"})
return
}
// Check if API keys are masked and preserve existing values
if len(newSettings.BraveAPIKey) > 8 && newSettings.BraveAPIKey[4:12] == "********" {
newSettings.BraveAPIKey = existingSettings.BraveAPIKey
}
if len(newSettings.SerperAPIKey) > 8 && newSettings.SerperAPIKey[4:12] == "********" {
newSettings.SerperAPIKey = existingSettings.SerperAPIKey
}
// Update model
updatedSettings := &models.UserSearchSettings{
BraveAPIKey: newSettings.BraveAPIKey,
BraveSearchBaseURL: newSettings.BraveSearchBaseURL,
SerperAPIKey: newSettings.SerperAPIKey,
SerperBaseURL: newSettings.SerperBaseURL,
SearchAPIProvider: newSettings.SearchAPIProvider,
SearchResultsLimit: newSettings.SearchResultsLimit,
SearchCacheTTL: newSettings.SearchCacheTTL,
SearchRateLimit: newSettings.SearchRateLimit,
}
// Save to database
err = models.SaveUserSearchSettings(uint(userID), updatedSettings)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"})
return
}
// Return masked settings for consistency
GetSearchSettings(c)
}
// GetTestSearchSettings handles GET /api/v1/test-search-settings (for demo mode)
func GetTestSearchSettings(c *gin.Context) {
settings := getDefaultSearchSettings()
// Mask API keys for security
if settings.BraveAPIKey != "" && len(settings.BraveAPIKey) > 8 {
settings.BraveAPIKey = settings.BraveAPIKey[:4] + "********" + settings.BraveAPIKey[len(settings.BraveAPIKey)-4:]
}
if settings.SerperAPIKey != "" && len(settings.SerperAPIKey) > 8 {
settings.SerperAPIKey = settings.SerperAPIKey[:4] + "********" + settings.SerperAPIKey[len(settings.SerperAPIKey)-4:]
}
c.JSON(http.StatusOK, settings)
}
// GetSearchSettingsForAPI returns unmasked search settings for internal API use
func GetSearchSettingsForAPI(userID int) (SearchSettings, error) {
settings, err := models.GetUserSearchSettings(uint(userID))
if err != nil {
// Return default settings if error
defaultSettings := getDefaultSearchSettings()
return defaultSettings, nil
}
return SearchSettings{
BraveAPIKey: settings.BraveAPIKey,
BraveSearchBaseURL: settings.BraveSearchBaseURL,
SerperAPIKey: settings.SerperAPIKey,
SerperBaseURL: settings.SerperBaseURL,
SearchAPIProvider: settings.SearchAPIProvider,
SearchResultsLimit: settings.SearchResultsLimit,
SearchCacheTTL: settings.SearchCacheTTL,
SearchRateLimit: settings.SearchRateLimit,
}, nil
}
func getDefaultSearchSettings() SearchSettings {
return SearchSettings{
BraveAPIKey: getEnvWithDefault("BRAVE_API_KEY", "BSAw0HNI1v3rKmXlSTr0C_UfZDjw7fT"),
BraveSearchBaseURL: getEnvWithDefault("BRAVE_SEARCH_BASE_URL", "https://api.search.brave.com/res/v1/web/search"),
SerperAPIKey: getEnvWithDefault("SERPER_API_KEY", "6f1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"),
SerperBaseURL: getEnvWithDefault("SERPER_BASE_URL", "https://google.serper.dev/search"),
SearchAPIProvider: getEnvWithDefault("SEARCH_API_PROVIDER", "brave"),
SearchResultsLimit: getIntEnvWithDefault("SEARCH_RESULTS_LIMIT", 10),
SearchCacheTTL: getIntEnvWithDefault("SEARCH_CACHE_TTL", 300),
SearchRateLimit: getIntEnvWithDefault("SEARCH_RATE_LIMIT", 100),
}
}
func getEnvWithDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getIntEnvWithDefault(key string, defaultValue int) int {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
return defaultValue
}
func getBoolEnvWithDefault(key string, defaultValue bool) bool {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
if value == "true" || value == "1" {
return true
}
return false
}
+13 -4
View File
@@ -6,6 +6,7 @@ import (
"crypto/cipher" "crypto/cipher"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"image/png" "image/png"
@@ -60,9 +61,13 @@ type TOTPLoginRequest struct {
// encrypt encrypts text using AES-GCM // encrypt encrypts text using AES-GCM
func encrypt(plaintext string) (string, error) { func encrypt(plaintext string) (string, error) {
key := []byte(os.Getenv("ENCRYPTION_KEY")) keyHex := strings.TrimSpace(os.Getenv("JWT_SECRET"))
key, err := hex.DecodeString(keyHex)
if err != nil {
return "", fmt.Errorf("failed to decode JWT secret for encryption: %v", err)
}
if len(key) != 32 { if len(key) != 32 {
return "", fmt.Errorf("encryption key must be 32 bytes") return "", fmt.Errorf("JWT secret must be 32 bytes when decoded, got %d", len(key))
} }
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
@@ -86,9 +91,13 @@ func encrypt(plaintext string) (string, error) {
// decrypt decrypts text using AES-GCM // decrypt decrypts text using AES-GCM
func decrypt(ciphertext string) (string, error) { func decrypt(ciphertext string) (string, error) {
key := []byte(os.Getenv("ENCRYPTION_KEY")) keyHex := strings.TrimSpace(os.Getenv("JWT_SECRET"))
key, err := hex.DecodeString(keyHex)
if err != nil {
return "", fmt.Errorf("failed to decode JWT secret for encryption: %v", err)
}
if len(key) != 32 { if len(key) != 32 {
return "", fmt.Errorf("encryption key must be 32 bytes") return "", fmt.Errorf("JWT secret must be 32 bytes when decoded, got %d", len(key))
} }
block, err := aes.NewCipher(key) block, err := aes.NewCipher(key)
+99
View File
@@ -0,0 +1,99 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/trackeep/backend/models"
)
// UpdateSettings represents update and OAuth configuration
type UpdateSettings struct {
OAuthServiceURL string `json:"oauth_service_url"`
AutoUpdateCheck bool `json:"auto_update_check"`
UpdateCheckInterval string `json:"update_check_interval"`
PrereleaseUpdates bool `json:"prerelease_updates"`
}
// GetUpdateSettings handles GET /api/v1/auth/update/settings
func GetUpdateSettings(c *gin.Context) {
userID := c.GetInt("user_id")
// Get settings from database
settings, err := models.GetUserUpdateSettings(uint(userID))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get settings"})
return
}
// Convert to response format
response := UpdateSettings{
OAuthServiceURL: settings.OAuthServiceURL,
AutoUpdateCheck: settings.AutoUpdateCheck,
UpdateCheckInterval: settings.UpdateCheckInterval,
PrereleaseUpdates: settings.PrereleaseUpdates,
}
c.JSON(http.StatusOK, response)
}
// UpdateUpdateSettings handles PUT /api/v1/auth/update/settings
func UpdateUpdateSettings(c *gin.Context) {
userID := c.GetInt("user_id")
var newSettings UpdateSettings
if err := c.ShouldBindJSON(&newSettings); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update model
updatedSettings := &models.UserUpdateSettings{
OAuthServiceURL: newSettings.OAuthServiceURL,
AutoUpdateCheck: newSettings.AutoUpdateCheck,
UpdateCheckInterval: newSettings.UpdateCheckInterval,
PrereleaseUpdates: newSettings.PrereleaseUpdates,
}
// Save to database
err := models.SaveUserUpdateSettings(uint(userID), updatedSettings)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save settings"})
return
}
// Return updated settings
GetUpdateSettings(c)
}
// GetTestUpdateSettings handles GET /api/v1/test-update-settings (for demo mode)
func GetTestUpdateSettings(c *gin.Context) {
settings := getDefaultUpdateSettings()
c.JSON(http.StatusOK, settings)
}
// GetUpdateSettingsForAPI returns update settings for internal API use
func GetUpdateSettingsForAPI(userID int) (UpdateSettings, error) {
settings, err := models.GetUserUpdateSettings(uint(userID))
if err != nil {
// Return default settings if error
defaultSettings := getDefaultUpdateSettings()
return defaultSettings, nil
}
return UpdateSettings{
OAuthServiceURL: settings.OAuthServiceURL,
AutoUpdateCheck: settings.AutoUpdateCheck,
UpdateCheckInterval: settings.UpdateCheckInterval,
PrereleaseUpdates: settings.PrereleaseUpdates,
}, nil
}
func getDefaultUpdateSettings() UpdateSettings {
return UpdateSettings{
OAuthServiceURL: getEnvWithDefault("OAUTH_SERVICE_URL", "https://oauth.tdvorak.dev"),
AutoUpdateCheck: getBoolEnvWithDefault("AUTO_UPDATE_CHECK", false),
UpdateCheckInterval: getEnvWithDefault("UPDATE_CHECK_INTERVAL", "24h"),
PrereleaseUpdates: getBoolEnvWithDefault("PRERELEASE_UPDATES", false),
}
}
+181 -25
View File
@@ -4,6 +4,7 @@ import (
"archive/zip" "archive/zip"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -66,33 +67,48 @@ func init() {
} }
} }
// getCurrentVersion reads the current version from frontend/package.json
func getCurrentVersion() string {
// Try to read from frontend/package.json first
packageJsonPath := "frontend/package.json"
if content, err := os.ReadFile(packageJsonPath); err == nil {
var packageJson struct {
Version string `json:"version"`
}
if err := json.Unmarshal(content, &packageJson); err == nil && packageJson.Version != "" {
log.Printf("Found version in frontend/package.json: %s", packageJson.Version)
return packageJson.Version
}
}
// Fallback to backend/go.mod
goModPath := "go.mod"
if content, err := os.ReadFile(goModPath); err == nil {
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.Contains(line, "module ") {
// Extract version from module path or use a default
// For now, return a default version
log.Printf("Using fallback version from go.mod")
return "1.2.5"
}
}
}
// Final fallback
log.Printf("Using default version - could not detect from source files")
return "1.2.5"
}
// CheckForUpdates checks if a new version is available using Docker registry // CheckForUpdates checks if a new version is available using Docker registry
func CheckForUpdates(c *gin.Context) { func CheckForUpdates(c *gin.Context) {
updateMutex.Lock() updateMutex.Lock()
defer updateMutex.Unlock() defer updateMutex.Unlock()
// Get current version from go.mod // Get current version from frontend/package.json
currentVersion := "1.2.5" currentVersion := getCurrentVersion()
// Try to read from go.mod if running in development log.Printf("Checking for updates using GitHub releases (current version: %s)", currentVersion)
if _, err := os.Stat("go.mod"); err == nil {
if content, err := os.ReadFile("go.mod"); err == nil {
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.Contains(line, "go ") && strings.Contains(line, "1.2.5") {
// Extract version from go.mod
parts := strings.Fields(line)
if len(parts) >= 2 {
currentVersion = strings.TrimSpace(parts[1])
log.Printf("Found version in go.mod: %s", currentVersion)
break
}
}
}
}
}
log.Printf("Checking for updates using Docker registry (current version: %s)", currentVersion)
// Check for updates using Docker registry // Check for updates using Docker registry
updateInfo, updateAvailable, err := checkForUpdatesWithDocker(currentVersion) updateInfo, updateAvailable, err := checkForUpdatesWithDocker(currentVersion)
@@ -109,14 +125,20 @@ func CheckForUpdates(c *gin.Context) {
currentUpdate = updateInfo currentUpdate = updateInfo
updateProgress.Available = true updateProgress.Available = true
} else { } else {
currentUpdate = nil // Still preserve updateInfo for displaying latest version, but mark as no update available
currentUpdate = updateInfo
updateProgress.Available = false updateProgress.Available = false
} }
latestVersion := ""
if updateInfo != nil {
latestVersion = updateInfo.Version
}
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"updateAvailable": updateAvailable, "updateAvailable": updateAvailable,
"currentVersion": currentVersion, "currentVersion": currentVersion,
"latestVersion": updateInfo.Version, "latestVersion": latestVersion,
"updateInfo": currentUpdate, "updateInfo": currentUpdate,
}) })
} }
@@ -167,8 +189,142 @@ func UpdateProgressWebSocket(c *gin.Context) {
}) })
} }
// checkForUpdatesWithDocker checks for updates using Docker registry // checkForUpdatesWithDocker checks for updates using GitHub releases
func checkForUpdatesWithDocker(currentVersion string) (*UpdateInfo, bool, error) { func checkForUpdatesWithDocker(currentVersion string) (*UpdateInfo, bool, error) {
log.Printf("Checking for updates (current version: %s)", currentVersion)
// Get latest release from GitHub
latestRelease, err := getLatestGitHubRelease()
if err != nil {
log.Printf("Failed to get latest release from GitHub: %v", err)
// Fallback to Docker registry check
return checkForUpdatesWithDockerRegistry(currentVersion)
}
log.Printf("Latest release from GitHub: %s", latestRelease.Version)
// Compare versions
if isNewerVersion(latestRelease.Version, currentVersion) {
log.Printf("Update available: %s -> %s", currentVersion, latestRelease.Version)
return latestRelease, true, nil
}
log.Printf("No updates available - current version %s is latest", currentVersion)
return latestRelease, false, nil
}
// getLatestGitHubRelease fetches the latest release from GitHub API
func getLatestGitHubRelease() (*UpdateInfo, error) {
client := &http.Client{Timeout: 10 * time.Second}
url := "https://api.github.com/repos/Dvorinka/Trackeep/releases/latest"
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch release: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}
// Parse JSON response
var release struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
PublishedAt string `json:"published_at"`
Prerelease bool `json:"prerelease"`
Draft bool `json:"draft"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return nil, fmt.Errorf("failed to decode release JSON: %w", err)
}
// Skip drafts and prereleases unless specifically allowed
if release.Draft {
return nil, fmt.Errorf("latest release is a draft")
}
// Check if prereleases are allowed
allowPrerelease := os.Getenv("PRERELEASE_UPDATES") == "true"
if release.Prerelease && !allowPrerelease {
// Try to get latest non-prerelease
return getLatestStableRelease()
}
// Clean version (remove 'v' prefix if present)
version := strings.TrimPrefix(release.TagName, "v")
updateInfo := &UpdateInfo{
Version: version,
ReleaseNotes: release.Body,
DownloadURL: "", // Docker images don't need download URL
Mandatory: false,
Size: "Docker images",
Checksum: "",
PublishedAt: release.PublishedAt,
Prerelease: release.Prerelease,
}
return updateInfo, nil
}
// getLatestStableRelease gets the latest stable (non-prerelease) release
func getLatestStableRelease() (*UpdateInfo, error) {
client := &http.Client{Timeout: 10 * time.Second}
url := "https://api.github.com/repos/Dvorinka/Trackeep/releases"
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("failed to fetch releases: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}
// Parse JSON response
var releases []struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
PublishedAt string `json:"published_at"`
Prerelease bool `json:"prerelease"`
Draft bool `json:"draft"`
}
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
return nil, fmt.Errorf("failed to decode releases JSON: %w", err)
}
// Find first stable (non-prerelease, non-draft) release
for _, release := range releases {
if !release.Draft && !release.Prerelease {
version := strings.TrimPrefix(release.TagName, "v")
updateInfo := &UpdateInfo{
Version: version,
ReleaseNotes: release.Body,
DownloadURL: "",
Mandatory: false,
Size: "Docker images",
Checksum: "",
PublishedAt: release.PublishedAt,
Prerelease: false,
}
return updateInfo, nil
}
}
return nil, fmt.Errorf("no stable releases found")
}
// checkForUpdatesWithDockerRegistry fallback method using Docker registry
func checkForUpdatesWithDockerRegistry(currentVersion string) (*UpdateInfo, bool, error) {
// Define images to check (using latest) // Define images to check (using latest)
backendImage := "ghcr.io/dvorinka/trackeep/backend:latest" backendImage := "ghcr.io/dvorinka/trackeep/backend:latest"
frontendImage := "ghcr.io/dvorinka/trackeep/frontend:latest" frontendImage := "ghcr.io/dvorinka/trackeep/frontend:latest"
@@ -348,7 +504,7 @@ func updateWithDockerCompose() error {
// Check if production docker-compose file exists // Check if production docker-compose file exists
composeFile := "docker-compose.prod.yml" composeFile := "docker-compose.prod.yml"
if _, err := os.Stat(composeFile); err != nil { if _, err := os.Stat(composeFile); err != nil {
return fmt.Errorf("production docker-compose.yml not found") return fmt.Errorf("production docker-compose file not found")
} }
// Use docker compose command directly (assuming Docker is available on host) // Use docker compose command directly (assuming Docker is available on host)
+116 -13
View File
@@ -7,8 +7,10 @@ import (
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/trackeep/backend/config" "github.com/trackeep/backend/config"
"github.com/trackeep/backend/handlers" "github.com/trackeep/backend/handlers"
@@ -22,23 +24,67 @@ func IsDemoMode() bool {
} }
func initializeSecuritySecrets() error { func initializeSecuritySecrets() error {
jwtSecret, err := utils.GetOrCreateJWTSecret() // Only set JWT_SECRET if not already provided in environment
if err != nil { if os.Getenv("JWT_SECRET") == "" {
return err jwtSecret, err := utils.GetOrCreateJWTSecret()
if err != nil {
return err
}
os.Setenv("JWT_SECRET", jwtSecret)
log.Println("JWT secret initialized from file")
} else {
log.Println("JWT secret found in environment variable")
} }
os.Setenv("JWT_SECRET", jwtSecret)
log.Println("JWT secret initialized successfully")
encryptionKey, err := utils.GetOrCreateEncryptionKey() // Only set ENCRYPTION_KEY if not already provided in environment
if err != nil { if os.Getenv("ENCRYPTION_KEY") == "" {
return err encryptionKey, err := utils.GetOrCreateEncryptionKey()
if err != nil {
return err
}
os.Setenv("ENCRYPTION_KEY", encryptionKey)
log.Println("Encryption key initialized from file")
} else {
log.Println("Encryption key found in environment variable")
} }
os.Setenv("ENCRYPTION_KEY", encryptionKey)
log.Println("Encryption key initialized successfully")
return nil return nil
} }
// initializeDragonflyDB initializes DragonflyDB (Redis-compatible) connection
func initializeDragonflyDB() *redis.Client {
dragonflyAddr := os.Getenv("DRAGONFLY_ADDR")
dragonflyPassword := os.Getenv("DRAGONFLY_PASSWORD")
if dragonflyAddr == "" {
log.Println("DRAGONFLY_ADDR not set, using default: localhost:6379")
dragonflyAddr = "localhost:6379"
}
rdb := redis.NewClient(&redis.Options{
Addr: dragonflyAddr,
Password: dragonflyPassword,
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
PoolSize: 20,
})
// Test connection
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := rdb.Ping(ctx).Result()
if err != nil {
log.Printf("Warning: Failed to connect to DragonflyDB at %s: %v", dragonflyAddr, err)
log.Println("Falling back to in-memory cache and sessions")
return nil
}
log.Printf("Successfully connected to DragonflyDB at %s", dragonflyAddr)
return rdb
}
func main() { func main() {
os.Setenv("APP_VERSION", "1.0.0") os.Setenv("APP_VERSION", "1.0.0")
@@ -75,10 +121,28 @@ func main() {
log.Fatal("Failed to initialize security secrets:", err) log.Fatal("Failed to initialize security secrets:", err)
} }
// Initialize session store // Initialize DragonflyDB
middleware.InitSessionStore() dragonflyClient := initializeDragonflyDB()
// Initialize session store with DragonflyDB
middleware.InitSessionStore(dragonflyClient)
log.Println("Session store initialized successfully") log.Println("Session store initialized successfully")
// Initialize cache middleware with DragonflyDB
var cacheConfig middleware.CacheConfig
if dragonflyClient != nil {
cacheConfig = middleware.CacheConfig{
Duration: 5 * time.Minute,
KeyPrefix: "trackeep:",
Enabled: true,
RedisClient: dragonflyClient,
}
log.Println("DragonflyDB cache middleware initialized")
} else {
cacheConfig = middleware.DefaultCacheConfig()
log.Println("Using in-memory cache fallback")
}
// Seed demo data in background // Seed demo data in background
// go func() { // go func() {
// SeedData() // SeedData()
@@ -95,7 +159,9 @@ func main() {
// Middleware // Middleware
r.Use(gin.Logger()) r.Use(gin.Logger())
r.Use(gin.Recovery()) r.Use(gin.Recovery())
r.Use(middleware.SessionMiddleware()) // Add session middleware r.Use(middleware.CacheMiddleware(cacheConfig)) // Add DragonflyDB cache middleware
r.Use(middleware.CacheInvalidationMiddleware(dragonflyClient)) // Add cache invalidation
r.Use(middleware.SessionMiddleware()) // Add session middleware
r.Use(middleware.AuditMiddleware()) r.Use(middleware.AuditMiddleware())
r.Use(middleware.InputValidationMiddleware()) r.Use(middleware.InputValidationMiddleware())
@@ -125,10 +191,17 @@ func main() {
// Serve static files (frontend) // Serve static files (frontend)
r.Static("/assets", "../frontend/dist/assets") r.Static("/assets", "../frontend/dist/assets")
r.StaticFile("/", "../frontend/dist/index.html") r.StaticFile("/", "../frontend/dist/index.html")
// Serve browser extension download
r.GET("/browser-extension", handlers.DownloadBrowserExtension)
r.NoRoute(func(c *gin.Context) { r.NoRoute(func(c *gin.Context) {
c.File("../frontend/dist/index.html") c.File("../frontend/dist/index.html")
}) })
// Version endpoint
r.GET("/api/version", handlers.GetVersionHandler)
// Initialize handlers // Initialize handlers
memberHandler := handlers.NewMemberHandler(config.GetDB()) memberHandler := handlers.NewMemberHandler(config.GetDB())
timeEntryHandler := handlers.NewTimeEntryHandler(config.GetDB()) timeEntryHandler := handlers.NewTimeEntryHandler(config.GetDB())
@@ -203,11 +276,23 @@ func main() {
authProtected.GET("/ai/settings", handlers.GetAISettings) authProtected.GET("/ai/settings", handlers.GetAISettings)
authProtected.PUT("/ai/settings", handlers.UpdateAISettings) authProtected.PUT("/ai/settings", handlers.UpdateAISettings)
authProtected.POST("/ai/test-connection", handlers.TestAIConnection) authProtected.POST("/ai/test-connection", handlers.TestAIConnection)
// Search Settings routes
authProtected.GET("/search/settings", handlers.GetSearchSettings)
authProtected.PUT("/search/settings", handlers.UpdateSearchSettings)
// Update Settings routes
authProtected.GET("/update/settings", handlers.GetUpdateSettings)
authProtected.PUT("/update/settings", handlers.UpdateUpdateSettings)
} }
// Test AI settings without auth // Test AI settings without auth
v1.GET("/test-ai-settings", handlers.GetAISettings) v1.GET("/test-ai-settings", handlers.GetAISettings)
// Test search and update settings without auth (for demo mode)
v1.GET("/test-search-settings", handlers.GetTestSearchSettings)
v1.GET("/test-update-settings", handlers.GetTestUpdateSettings)
// Dashboard routes (protected) // Dashboard routes (protected)
dashboard := v1.Group("/dashboard") dashboard := v1.Group("/dashboard")
dashboard.Use(handlers.AuthMiddleware()) dashboard.Use(handlers.AuthMiddleware())
@@ -721,6 +806,24 @@ func main() {
performance.POST("/optimize", performanceHandler.OptimizeDatabase) performance.POST("/optimize", performanceHandler.OptimizeDatabase)
performance.POST("/cleanup-audit-logs", performanceHandler.CleanupOldAuditLogs) performance.POST("/cleanup-audit-logs", performanceHandler.CleanupOldAuditLogs)
} }
// Browser Extension API routes
browserExt := v1.Group("/browser-extension")
browserExt.Use(handlers.AuthMiddleware())
{
// API Key management
browserExt.POST("/api-keys/generate", handlers.GenerateAPIKey)
browserExt.GET("/api-keys", handlers.GetAPIKeys)
browserExt.DELETE("/api-keys/:id", handlers.RevokeAPIKey)
// Extension registration and validation
browserExt.POST("/register", handlers.RegisterBrowserExtension)
browserExt.GET("/extensions", handlers.GetBrowserExtensions)
browserExt.DELETE("/extensions/:id", handlers.RevokeBrowserExtension)
// Public endpoints (for extension validation)
browserExt.GET("/validate", handlers.ValidateAPIKey)
}
} }
srv := &http.Server{ srv := &http.Server{
+88 -6
View File
@@ -1,12 +1,15 @@
package middleware package middleware
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/trackeep/backend/models" "github.com/trackeep/backend/models"
) )
@@ -34,13 +37,15 @@ type SessionStore interface {
// RedisSessionStore implements SessionStore using Redis (or fallback to memory) // RedisSessionStore implements SessionStore using Redis (or fallback to memory)
type RedisSessionStore struct { type RedisSessionStore struct {
sessions map[string]*SessionData // Fallback in-memory store redisClient *redis.Client
sessions map[string]*SessionData // Fallback in-memory store
} }
// NewSessionStore creates a new session store // NewSessionStore creates a new session store
func NewSessionStore() SessionStore { func NewSessionStore(redisClient *redis.Client) SessionStore {
return &RedisSessionStore{ return &RedisSessionStore{
sessions: make(map[string]*SessionData), redisClient: redisClient,
sessions: make(map[string]*SessionData),
} }
} }
@@ -48,32 +53,109 @@ func NewSessionStore() SessionStore {
func (r *RedisSessionStore) CreateSession(sessionData *SessionData) error { func (r *RedisSessionStore) CreateSession(sessionData *SessionData) error {
sessionData.CreatedAt = time.Now() sessionData.CreatedAt = time.Now()
sessionData.LastActive = time.Now() sessionData.LastActive = time.Now()
// Try Redis first
if r.redisClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
sessionJSON, err := json.Marshal(sessionData)
if err != nil {
return fmt.Errorf("failed to marshal session data: %w", err)
}
// Store in Redis with 24 hour expiration
err = r.redisClient.Set(ctx, "session:"+sessionData.SessionID, sessionJSON, 24*time.Hour).Err()
if err == nil {
return nil
}
// Fall back to memory if Redis fails
}
// Fallback to in-memory storage
r.sessions[sessionData.SessionID] = sessionData r.sessions[sessionData.SessionID] = sessionData
return nil return nil
} }
// GetSession retrieves a session by ID // GetSession retrieves a session by ID
func (r *RedisSessionStore) GetSession(sessionID string) (*SessionData, error) { func (r *RedisSessionStore) GetSession(sessionID string) (*SessionData, error) {
// Try Redis first
if r.redisClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
sessionJSON, err := r.redisClient.Get(ctx, "session:"+sessionID).Result()
if err == nil {
var sessionData SessionData
if err := json.Unmarshal([]byte(sessionJSON), &sessionData); err == nil {
// Update last active time
sessionData.LastActive = time.Now()
// Update in Redis
updatedJSON, _ := json.Marshal(sessionData)
r.redisClient.Set(ctx, "session:"+sessionID, updatedJSON, 24*time.Hour)
return &sessionData, nil
}
}
// Fall back to memory if Redis fails
}
// Fallback to in-memory storage
if session, exists := r.sessions[sessionID]; exists { if session, exists := r.sessions[sessionID]; exists {
// Update last active time // Update last active time
session.LastActive = time.Now() session.LastActive = time.Now()
return session, nil return session, nil
} }
return nil, fmt.Errorf("session not found") return nil, fmt.Errorf("session not found")
} }
// UpdateSession updates an existing session // UpdateSession updates an existing session
func (r *RedisSessionStore) UpdateSession(sessionID string, sessionData *SessionData) error { func (r *RedisSessionStore) UpdateSession(sessionID string, sessionData *SessionData) error {
sessionData.LastActive = time.Now()
// Try Redis first
if r.redisClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
sessionJSON, err := json.Marshal(sessionData)
if err != nil {
return fmt.Errorf("failed to marshal session data: %w", err)
}
err = r.redisClient.Set(ctx, "session:"+sessionID, sessionJSON, 24*time.Hour).Err()
if err == nil {
return nil
}
// Fall back to memory if Redis fails
}
// Fallback to in-memory storage
if _, exists := r.sessions[sessionID]; exists { if _, exists := r.sessions[sessionID]; exists {
sessionData.LastActive = time.Now()
r.sessions[sessionID] = sessionData r.sessions[sessionID] = sessionData
return nil return nil
} }
return fmt.Errorf("session not found") return fmt.Errorf("session not found")
} }
// DeleteSession removes a session // DeleteSession removes a session
func (r *RedisSessionStore) DeleteSession(sessionID string) error { func (r *RedisSessionStore) DeleteSession(sessionID string) error {
// Try Redis first
if r.redisClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := r.redisClient.Del(ctx, "session:"+sessionID).Err()
if err == nil {
// Also remove from memory fallback
delete(r.sessions, sessionID)
return nil
}
// Fall back to memory if Redis fails
}
// Fallback to in-memory storage
delete(r.sessions, sessionID) delete(r.sessions, sessionID)
return nil return nil
} }
@@ -93,8 +175,8 @@ func (r *RedisSessionStore) CleanupExpiredSessions() error {
var sessionStore SessionStore var sessionStore SessionStore
// InitSessionStore initializes the session store // InitSessionStore initializes the session store
func InitSessionStore() { func InitSessionStore(redisClient *redis.Client) {
sessionStore = NewSessionStore() sessionStore = NewSessionStore(redisClient)
// Start cleanup goroutine // Start cleanup goroutine
go func() { go func() {
+33
View File
@@ -0,0 +1,33 @@
package models
import (
"time"
)
// APIKey represents an API key for browser extension
type APIKey struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"not null"`
Key string `json:"key" gorm:"not null;uniqueIndex"`
UserID uint `json:"user_id" gorm:"not null"`
Permissions []string `json:"permissions" gorm:"serializer:json"`
IsActive bool `json:"is_active" gorm:"default:true"`
LastUsed *time.Time `json:"last_used,omitempty" gorm:"not null"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
ExpiresAt *time.Time `json:"expires_at,omitempty" gorm:"not null"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BrowserExtension represents a browser extension registration
type BrowserExtension struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null"`
ExtensionID string `json:"extension_id" gorm:"not null"`
Name string `json:"name" gorm:"not null"`
IsActive bool `json:"is_active" gorm:"default:true"`
LastSeen *time.Time `json:"last_seen,omitempty" gorm:"not null"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
+2
View File
@@ -49,6 +49,8 @@ func AutoMigrate() {
&AISummary{}, &AISummary{},
&AITaskSuggestion{}, &AITaskSuggestion{},
&UserAISettings{}, &UserAISettings{},
&UserSearchSettings{},
&UserUpdateSettings{},
&AITagSuggestion{}, &AITagSuggestion{},
&AIContentGeneration{}, &AIContentGeneration{},
&AICodeReview{}, &AICodeReview{},
+65
View File
@@ -0,0 +1,65 @@
package models
import (
"time"
"gorm.io/gorm"
)
// UserSearchSettings stores user-specific search API configurations
type UserSearchSettings struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
UserID uint `json:"user_id" gorm:"not null;uniqueIndex"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
// Brave Search Settings
BraveAPIKey string `json:"-" gorm:"column:brave_api_key"` // Encrypted
BraveSearchBaseURL string `json:"brave_search_base_url" gorm:"default:https://api.search.brave.com/res/v1/web/search"`
// Serper (Google) Search Settings
SerperAPIKey string `json:"-" gorm:"column:serper_api_key"` // Encrypted
SerperBaseURL string `json:"serper_base_url" gorm:"default:https://google.serper.dev/search"`
// Search Configuration
SearchAPIProvider string `json:"search_api_provider" gorm:"default:brave"` // brave, serper
SearchResultsLimit int `json:"search_results_limit" gorm:"default:10"`
SearchCacheTTL int `json:"search_cache_ttl" gorm:"default:300"` // seconds
SearchRateLimit int `json:"search_rate_limit" gorm:"default:100"` // requests per minute
}
// GetUserSearchSettings retrieves search settings for a user
func GetUserSearchSettings(userID uint) (*UserSearchSettings, error) {
var settings UserSearchSettings
err := DB.Where("user_id = ?", userID).First(&settings).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// Create default settings
settings = UserSearchSettings{
UserID: userID,
BraveSearchBaseURL: "https://api.search.brave.com/res/v1/web/search",
SerperBaseURL: "https://google.serper.dev/search",
SearchAPIProvider: "brave",
SearchResultsLimit: 10,
SearchCacheTTL: 300,
SearchRateLimit: 100,
}
// Save defaults
if err := DB.Create(&settings).Error; err != nil {
return nil, err
}
return &settings, nil
}
return nil, err
}
return &settings, nil
}
// SaveUserSearchSettings saves search settings for a user
func SaveUserSearchSettings(userID uint, settings *UserSearchSettings) error {
settings.UserID = userID
return DB.Where("user_id = ?", userID).Assign(settings).FirstOrCreate(settings).Error
}
+57
View File
@@ -0,0 +1,57 @@
package models
import (
"time"
"gorm.io/gorm"
)
// UserUpdateSettings stores user-specific update and OAuth configurations
type UserUpdateSettings struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
UserID uint `json:"user_id" gorm:"not null;uniqueIndex"`
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
// OAuth Service Configuration
OAuthServiceURL string `json:"oauth_service_url" gorm:"default:https://oauth.trackeep.org"`
// Update Configuration
AutoUpdateCheck bool `json:"auto_update_check" gorm:"default:false"`
UpdateCheckInterval string `json:"update_check_interval" gorm:"default:24h"` // 1h, 6h, 12h, 24h, 168h
PrereleaseUpdates bool `json:"prerelease_updates" gorm:"default:false"`
}
// GetUserUpdateSettings retrieves update settings for a user
func GetUserUpdateSettings(userID uint) (*UserUpdateSettings, error) {
var settings UserUpdateSettings
err := DB.Where("user_id = ?", userID).First(&settings).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// Create default settings
settings = UserUpdateSettings{
UserID: userID,
OAuthServiceURL: "https://oauth.trackeep.org",
AutoUpdateCheck: false,
UpdateCheckInterval: "24h",
PrereleaseUpdates: false,
}
// Save defaults
if err := DB.Create(&settings).Error; err != nil {
return nil, err
}
return &settings, nil
}
return nil, err
}
return &settings, nil
}
// SaveUserUpdateSettings saves update settings for a user
func SaveUserUpdateSettings(userID uint, settings *UserUpdateSettings) error {
settings.UserID = userID
return DB.Where("user_id = ?", userID).Assign(settings).FirstOrCreate(settings).Error
}
+8 -4
View File
@@ -48,10 +48,14 @@ type User struct {
LockedUntil *time.Time `json:"locked_until"` LockedUntil *time.Time `json:"locked_until"`
// Privacy Settings // Privacy Settings
ProfileVisibility string `json:"profile_visibility" gorm:"default:public"` // public, private, friends ProfileVisibility string `json:"profile_visibility" gorm:"default:private"` // public, private, friends
ShowEmail bool `json:"show_email" gorm:"default:false"` EmailNotifications bool `json:"email_notifications" gorm:"default:true"`
ShowActivity bool `json:"show_activity" gorm:"default:true"` PushNotifications bool `json:"push_notifications" gorm:"default:true"`
AllowMessages bool `json:"allow_messages" gorm:"default:true"`
// Social Features
ShowEmail bool `json:"show_email" gorm:"default:false"`
ShowActivity bool `json:"show_activity" gorm:"default:true"`
AllowMessages bool `json:"allow_messages" gorm:"default:true"`
// Social Stats // Social Stats
FollowersCount int `json:"followers_count" gorm:"default:0"` FollowersCount int `json:"followers_count" gorm:"default:0"`
-77
View File
@@ -1,77 +0,0 @@
# YouTube Scraper Service
A standalone microservice for scraping YouTube video data. This service runs independently from the main Trackeep application.
## Features
- **Mock YouTube Data**: Provides mock YouTube video data for development and testing
- **Channel Videos**: Fetch videos from specific YouTube channels
- **Search**: Search through YouTube video metadata
- **REST API**: Simple REST endpoints for integration
## API Endpoints
### Health Check
```
GET /
```
Returns service status and information.
### Get Channel Videos
```
GET /channel_videos?channel={channel_name}
```
Fetches videos for a specific YouTube channel.
**Parameters:**
- `channel`: YouTube channel name (e.g., "@Fireship", "@NetworkChuck")
### Search Videos
```
GET /search?q={query}
```
Searches through video titles, descriptions, and channel names.
**Parameters:**
- `q`: Search query
## Running the Service
### Development
```bash
cd youtube-scraper
go run .
```
### Production
```bash
cd youtube-scraper
go build -o youtube-scraper .
./youtube-scraper
```
### Docker
```bash
docker build -f ../Dockerfile.youtube-scraper -t youtube-scraper ..
docker run -p 7857:7857 youtube-scraper
```
## Environment Variables
- `PORT`: Service port (default: 7857)
## Mock Data
The service includes mock data for popular tech YouTube channels:
- @Fireship
- @NetworkChuck
- @beyondfireship
- @LinusTechTips
- @Mrwhosetheboss
- @JerryRigEverything
- @JeffGeerling
- @mkbhd
## Integration
This service is designed to be called by the main Trackeep application via HTTP requests. The main app can be configured to use this service for YouTube-related features.
-32
View File
@@ -1,32 +0,0 @@
module youtube-scraper
go 1.21
require github.com/gin-gonic/gin v1.9.1
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-86
View File
@@ -1,86 +0,0 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
-539
View File
@@ -1,539 +0,0 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
)
type VideoResponse struct {
VideoID string `json:"video_id"`
ChannelName string `json:"channel_name"`
}
var ctx = context.Background()
// ChannelVideosResponse represents the response for channel videos scraping
type ChannelVideosResponse struct {
Channel string `json:"channel"`
ChannelURL string `json:"channel_url"`
SubscribersText string `json:"subscribers_text"`
Subscribers int64 `json:"subscribers"`
Videos []VideoItem `json:"videos"`
}
// VideoItem holds per-video metadata extracted from the /videos page
type VideoItem struct {
VideoID string `json:"video_id"`
Title string `json:"title,omitempty"`
Length string `json:"length,omitempty"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
ViewsText string `json:"views_text,omitempty"`
Views int64 `json:"views"`
PublishedText string `json:"published_text,omitempty"`
PublishedDate string `json:"published_date,omitempty"` // ISO 8601 date
}
// normalizeChannelInput accepts a handle like "@FCBizoniUH" or "FCBizoniUH" or a full URL
// and returns the canonical handle (with leading @) and the corresponding /videos URL.
func normalizeChannelInput(input string) (handle string, url string) {
in := strings.TrimSpace(input)
lower := strings.ToLower(in)
isURL := strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") || strings.HasPrefix(lower, "www.") || strings.HasPrefix(lower, "youtube.com/")
if isURL {
// Ensure scheme
if strings.HasPrefix(lower, "www.") || strings.HasPrefix(lower, "youtube.com/") {
in = "https://" + strings.TrimPrefix(in, "www.")
if !strings.HasPrefix(strings.ToLower(in), "https://youtube.com/") && !strings.HasPrefix(strings.ToLower(in), "https://www.youtube.com/") {
in = "https://www." + strings.TrimPrefix(in, "https://")
}
}
// Normalize m.youtube.com -> www.youtube.com
in = strings.ReplaceAll(in, "m.youtube.com", "www.youtube.com")
// Extract handle if present
reHandle := regexp.MustCompile(`https?://(www\.)?youtube\.com/(@[^/]+)`) // group with @
if m := reHandle.FindStringSubmatch(in); len(m) >= 3 {
handle = m[2]
} else {
// Try path segment after domain
rePath := regexp.MustCompile(`https?://(www\.)?youtube\.com/([^/?#]+)`) // capture after domain
if m2 := rePath.FindStringSubmatch(in); len(m2) >= 3 {
seg := m2[2]
if strings.HasPrefix(seg, "@") {
handle = seg
} else {
handle = "@" + seg
}
}
}
// Respect provided tab if present: /videos, /shorts, /streams; default to /videos
if strings.Contains(strings.ToLower(in), "/videos") || strings.Contains(strings.ToLower(in), "/shorts") || strings.Contains(strings.ToLower(in), "/streams") {
url = in
} else {
// Build a /videos URL from detected handle
if handle == "" {
// If we couldn't find a handle, just use the original URL
url = in
} else {
url = fmt.Sprintf("https://www.youtube.com/%s/videos", handle)
}
}
} else {
// Not a URL; treat as handle or bare identifier
if strings.HasPrefix(in, "@") {
handle = in
} else {
handle = "@" + in
}
url = fmt.Sprintf("https://www.youtube.com/%s/videos", handle)
}
if handle == "" {
// As a final fallback from given input
handle = in
if !strings.HasPrefix(handle, "@") {
handle = "@" + handle
}
}
return
}
// fetchChannelVideos scrapes the channel's /videos page and extracts video IDs present
func fetchChannelVideos(channelInput string) (ChannelVideosResponse, error) {
handle, channelURL := normalizeChannelInput(channelInput)
log.Printf("Fetching channel videos: handle=%s url=%s", handle, channelURL)
// Craft request with a desktop UA to improve likelihood of getting full HTML payload
req, err := http.NewRequest("GET", channelURL, nil)
if err != nil {
return ChannelVideosResponse{}, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return ChannelVideosResponse{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ChannelVideosResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ChannelVideosResponse{}, err
}
html := string(body)
// Regex to capture all 11-char YouTube video IDs from initial data payload
// Standard videos
vidRe := regexp.MustCompile(`"videoRenderer":\{[^}]*?"videoId":"([a-zA-Z0-9_-]{11})"`)
matches := vidRe.FindAllStringSubmatchIndex(html, -1)
seen := make(map[string]struct{})
var videos []VideoItem
for _, idx := range matches {
if len(idx) < 4 { // need at least match start/end and group start/end
continue
}
// Extract ID
id := html[idx[2]:idx[3]]
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
// Build a local window around the match to parse related fields
start := idx[0]
if start-2000 > 0 {
start = start - 2000
}
end := idx[1] + 8000
if end > len(html) {
end = len(html)
}
snippet := html[start:end]
vi := VideoItem{VideoID: id}
// Prefer deterministic thumbnail URL derived from video ID
vi.ThumbnailURL = fmt.Sprintf("https://img.youtube.com/vi/%s/maxresdefault.jpg", id)
// Title (may appear as simpleText or runs)
if m := regexp.MustCompile(`"title":\{"runs":\[\{"text":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
vi.Title = unescapeYT(m[1])
} else if m := regexp.MustCompile(`"title":\{"simpleText":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
vi.Title = unescapeYT(m[1])
}
// Length
if m := regexp.MustCompile(`"lengthText":\{[^}]*"simpleText":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
// Generic lengthText.simpleText (with or without accessibility block)
vi.Length = m[1]
} else if m := regexp.MustCompile(`"lengthText":\{[^}]*"runs":\[\{"text":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
// lengthText.runs[0].text
vi.Length = m[1]
} else if m := regexp.MustCompile(`"thumbnailOverlays":\[[^\]]*?"thumbnailOverlayTimeStatusRenderer":\{"text":\{"simpleText":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
// Overlay badge duration
vi.Length = m[1]
} else if m := regexp.MustCompile(`yt-badge-shape__text">([^<]+)<`).FindStringSubmatch(snippet); len(m) >= 2 {
// Fallback: raw HTML badge text seen in thumbnails
vi.Length = strings.TrimSpace(m[1])
}
// Extra fallback: search the global HTML near the video anchor for DOM-based duration
if vi.Length == "" {
anchorRe := regexp.MustCompile(fmt.Sprintf(`<a[^>]+href="/watch\?v=%s[^\"]*"`, regexp.QuoteMeta(id)))
if loc := anchorRe.FindStringIndex(html); loc != nil {
// Search a forward window after the anchor for duration elements
start2 := loc[1]
end2 := start2 + 4000
if end2 > len(html) {
end2 = len(html)
}
chunk := html[start2:end2]
// Try yt-formatted-string id="length" inner text like 5:59
if m := regexp.MustCompile(`yt-formatted-string[^>]*id="length"[^>]*>([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)<`).FindStringSubmatch(chunk); len(m) >= 2 {
vi.Length = strings.TrimSpace(m[1])
} else if m := regexp.MustCompile(`yt-formatted-string[^>]*id="length"[^>]*aria-label="([^"]+)"`).FindStringSubmatch(chunk); len(m) >= 2 {
if parsed := parseLocalizedDuration(unescapeYT(m[1])); parsed != "" {
vi.Length = parsed
}
} else if m := regexp.MustCompile(`yt-badge-shape__text">([^<]+)<`).FindStringSubmatch(chunk); len(m) >= 2 {
vi.Length = strings.TrimSpace(m[1])
}
}
}
// Thumbnail URL (first in thumbnails array) as a fallback only if not set
if vi.ThumbnailURL == "" {
if m := regexp.MustCompile(`"thumbnail":\{"thumbnails":\[\{"url":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
vi.ThumbnailURL = normalizeThumbURL(unescapeYT(m[1]))
}
}
// Published time text (e.g., "3 days ago")
if m := regexp.MustCompile(`"publishedTimeText":\{"simpleText":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
vi.PublishedText = m[1]
vi.PublishedDate = parseRelativeToISO(m[1])
}
// Views
if m := regexp.MustCompile(`"viewCountText":\{"simpleText":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
vi.ViewsText = m[1]
vi.Views = parseCountText(m[1])
} else if m := regexp.MustCompile(`"viewCountText":\{"runs":\[\{"text":"([^"]+)"`).FindStringSubmatch(snippet); len(m) >= 2 {
vi.ViewsText = m[1] + " views"
vi.Views = parseCountText(m[1])
}
videos = append(videos, vi)
}
// Attempt to derive a displayable channel handle/name
channelDisplay := handle
// Try to extract canonicalBaseUrl if present
canRe := regexp.MustCompile(`"canonicalBaseUrl":"\\/(@[^\"]+)"`)
if m := canRe.FindStringSubmatch(html); len(m) >= 2 {
channelDisplay = m[1]
}
// Extract subscribers (header section)
subText := ""
// Try simpleText first
if m := regexp.MustCompile(`"subscriberCountText":\{"simpleText":"([^"]+)"`).FindStringSubmatch(html); len(m) >= 2 {
subText = m[1]
} else {
// Try runs: join all text segments inside subscriberCountText.runs
if loc := regexp.MustCompile(`"subscriberCountText":\{"runs":\[`).FindStringIndex(html); loc != nil {
// Take a slice starting at runs and limited length
slice := html[loc[1]:]
// Find the closing ]
if endIdx := strings.Index(slice, "]}"); endIdx != -1 {
runsChunk := slice[:endIdx]
// Collect all text fields inside runs
texts := regexp.MustCompile(`"text":"([^"]+)"`).FindAllStringSubmatch(runsChunk, -1)
var parts []string
for _, t := range texts {
if len(t) >= 2 {
parts = append(parts, unescapeYT(t[1]))
}
}
subText = strings.Join(parts, "")
}
}
}
// Fallbacks: approximateSubscriberCount or localized patterns like "131 odběratelů"
if subText == "" {
if m := regexp.MustCompile(`"approximateSubscriberCount":"([^"]+)"`).FindStringSubmatch(html); len(m) >= 2 {
subText = m[1]
}
}
if subText == "" {
// Case-insensitive; match digits with optional spaces/commas/dots before localized label
if m := regexp.MustCompile(`(?i)([0-9][0-9\s\.,]*)\s*(odběratel(?:é|ů)?|subscribers?)`).FindStringSubmatch(html); len(m) >= 2 {
subText = strings.TrimSpace(m[0])
}
}
subs := parseCountText(subText)
res := ChannelVideosResponse{
Channel: channelDisplay,
ChannelURL: channelURL,
SubscribersText: subText,
Subscribers: subs,
Videos: videos,
}
return res, nil
}
// unescapeYT fixes escaped sequences in YouTube HTML JSON strings
func unescapeYT(s string) string {
s = strings.ReplaceAll(s, `\/`, `/`)
s = strings.ReplaceAll(s, `\u0026`, `&`)
return s
}
// normalizeThumbURL ensures thumbnails use https and removes query artifacts if needed
func normalizeThumbURL(u string) string {
u = unescapeYT(u)
if strings.HasPrefix(u, "//") {
u = "https:" + u
}
return u
}
// parseRelativeToISO converts strings like "3 days ago", "2 weeks ago", "1 year ago" to ISO date (yyyy-mm-dd)
func parseRelativeToISO(rel string) string {
now := time.Now()
lower := strings.ToLower(rel)
re := regexp.MustCompile(`(\d+)[\s-]*(second|minute|hour|day|week|month|year)s?\s+ago`)
if m := re.FindStringSubmatch(lower); len(m) >= 3 {
n, _ := strconv.Atoi(m[1])
unit := m[2]
dur := time.Duration(0)
switch unit {
case "second":
dur = time.Duration(n) * time.Second
return now.Add(-dur).Format("2006-01-02")
case "minute":
dur = time.Duration(n) * time.Minute
return now.Add(-dur).Format("2006-01-02")
case "hour":
dur = time.Duration(n) * time.Hour
return now.Add(-dur).Format("2006-01-02")
case "day":
return now.AddDate(0, 0, -n).Format("2006-01-02")
case "week":
return now.AddDate(0, 0, -7*n).Format("2006-01-02")
case "month":
return now.AddDate(0, -n, 0).Format("2006-01-02")
case "year":
return now.AddDate(-n, 0, 0).Format("2006-01-02")
}
}
// Sometimes YouTube uses "Streamed X days ago" or "Premiered ..."
re2 := regexp.MustCompile(`(streamed|premiered|started|live)\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago`)
if m := re2.FindStringSubmatch(lower); len(m) >= 4 {
n, _ := strconv.Atoi(m[2])
unit := m[3]
switch unit {
case "second":
return now.Add(-time.Duration(n) * time.Second).Format("2006-01-02")
case "minute":
return now.Add(-time.Duration(n) * time.Minute).Format("2006-01-02")
case "hour":
return now.Add(-time.Duration(n) * time.Hour).Format("2006-01-02")
case "day":
return now.AddDate(0, 0, -n).Format("2006-01-02")
case "week":
return now.AddDate(0, 0, -7*n).Format("2006-01-02")
case "month":
return now.AddDate(0, -n, 0).Format("2006-01-02")
case "year":
return now.AddDate(-n, 0, 0).Format("2006-01-02")
}
}
return ""
}
// parseLocalizedDuration converts localized duration phrases (e.g., "5 minut a 59 sekund")
// into a mm:ss or hh:mm:ss string. Supports English and basic Czech variants.
func parseLocalizedDuration(s string) string {
t := strings.ToLower(strings.TrimSpace(s))
// Replace HTML entities and non-breaking spaces
t = strings.ReplaceAll(t, "&nbsp;", " ")
t = strings.ReplaceAll(t, "\u00a0", " ")
t = strings.TrimSpace(t)
// If already in 00:00 or 0:00:00 form, return as-is trimmed
if m := regexp.MustCompile(`^\d{1,2}:\d{2}(?::\d{2})?$`).FindString(t); m != "" {
return m
}
// Patterns like: 1 hour 2 minutes 3 seconds (EN)
// or Czech: 1 hodina/hodiny/hodin, 2 minuty/minut, 3 sekundy/sekund
// We'll extract numbers for h/m/s separately.
var h, m, sec int
// English capture
if mm := regexp.MustCompile(`(\d+)\s*hour`).FindStringSubmatch(t); len(mm) >= 2 {
h, _ = strconv.Atoi(mm[1])
}
if mm := regexp.MustCompile(`(\d+)\s*minute`).FindStringSubmatch(t); len(mm) >= 2 {
m, _ = strconv.Atoi(mm[1])
}
if mm := regexp.MustCompile(`(\d+)\s*second`).FindStringSubmatch(t); len(mm) >= 2 {
sec, _ = strconv.Atoi(mm[1])
}
// Czech capture
if mm := regexp.MustCompile(`(\d+)\s*hodin(?:a|y)?`).FindStringSubmatch(t); len(mm) >= 2 {
if h == 0 {
h, _ = strconv.Atoi(mm[1])
}
}
if mm := regexp.MustCompile(`(\d+)\s*minut(?:a|y)?`).FindStringSubmatch(t); len(mm) >= 2 {
if m == 0 {
m, _ = strconv.Atoi(mm[1])
}
}
if mm := regexp.MustCompile(`(\d+)\s*sekund(?:a|y)?`).FindStringSubmatch(t); len(mm) >= 2 {
if sec == 0 {
sec, _ = strconv.Atoi(mm[1])
}
}
// If we still didn't parse anything but string contains a plain number like "5 minutes",
// ensure we at least capture minutes.
if h == 0 && m == 0 && sec == 0 {
if mm := regexp.MustCompile(`^(\d+)$`).FindStringSubmatch(t); len(mm) >= 2 {
m, _ = strconv.Atoi(mm[1])
}
}
// Build the time string
if h > 0 {
return fmt.Sprintf("%d:%02d:%02d", h, m, sec)
}
if m > 0 || sec > 0 {
return fmt.Sprintf("%d:%02d", m, sec)
}
return ""
}
// parseCountText handles strings like "1,234 views", "12K subscribers", "3.4M"
func parseCountText(s string) int64 {
t := strings.ToLower(strings.TrimSpace(s))
// keep only the first number token
re := regexp.MustCompile(`([0-9]+(?:\.[0-9]+)?)([kmb])?`)
if m := re.FindStringSubmatch(t); len(m) >= 2 {
numStr := m[1]
suf := ""
if len(m) >= 3 {
suf = m[2]
}
f, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0
}
switch suf {
case "k":
f *= 1_000
case "m":
f *= 1_000_000
case "b":
f *= 1_000_000_000
}
return int64(f)
}
// Fallback: strip non-digits and parse
digits := regexp.MustCompile(`[^0-9]`).ReplaceAllString(t, "")
if digits == "" {
return 0
}
v, _ := strconv.ParseInt(digits, 10, 64)
return v
}
func channelVideosHandler(w http.ResponseWriter, r *http.Request) {
channel := r.URL.Query().Get("channel")
if channel == "" {
log.Println("Missing channel parameter")
http.Error(w, "Missing channel parameter. Provide a handle like @FCBizoniUH, FCBBizoniUH, or a full channel URL.", http.StatusBadRequest)
return
}
res, err := fetchChannelVideos(channel)
if err != nil {
log.Printf("Failed to fetch channel videos for %s: %v", channel, err)
http.Error(w, "Failed to fetch channel videos", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
// CORS Middleware
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// Handle preflight requests
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
response := map[string]interface{}{
"status": "ok",
"service": "YouTube Scraper",
"version": "1.0.0",
"endpoints": map[string]string{
"channel_videos": "/channel_videos?channel={handle_or_url}",
},
}
json.NewEncoder(w).Encode(response)
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "7857"
}
mux := http.NewServeMux()
// Create a new mux with CORS middleware
handlerWithCORS := corsMiddleware(mux)
// Register routes on the original mux
mux.HandleFunc("/", rootHandler)
mux.HandleFunc("/channel_videos", channelVideosHandler)
log.Printf("YouTube Scraper starting on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, handlerWithCORS))
}
+217
View File
@@ -0,0 +1,217 @@
/* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
// Handle keyboard commands
browser.commands.onCommand.addListener((command) => {
if (command === 'quick-save') {
// Get current tab and trigger quick save
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0];
if (tab) {
browser.storage.local.set({
contextMenuData: {
url: tab.url,
title: tab.title,
selection: '',
timestamp: Date.now(),
isQuickSave: true
}
}, () => {
browser.action.openPopup();
});
}
});
}
});
// Handle first-time install
browser.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
// Set up first-time install flag
browser.storage.sync.set({
isFirstInstall: true,
installDate: new Date().toISOString()
}, () => {
// Open options page for first-time setup
browser.runtime.openOptionsPage();
});
}
// Create context menus
browser.contextMenus.create({
id: 'save-to-trackeep',
title: 'Save to Trackeep',
contexts: ['page', 'link', 'selection', 'image', 'video']
});
// Quick save menu
browser.contextMenus.create({
id: 'quick-save-to-trackeep',
title: 'Quick Save to Trackeep',
contexts: ['page']
});
});
// Handle context menu click
browser.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== 'save-to-trackeep' && info.menuItemId !== 'quick-save-to-trackeep') return;
// Detect content type and get smart data
const smartData = await detectContentType(info, tab);
// Open popup with pre-filled data based on context
const url = info.linkUrl || info.srcUrl || tab?.url || '';
const title = tab?.title || '';
const selection = info.selectionText || '';
// Store temporary data for popup to read
browser.storage.local.set({
contextMenuData: {
url,
title,
selection,
timestamp: Date.now(),
isQuickSave: info.menuItemId === 'quick-save-to-trackeep',
smartData
}
}, () => {
// Open popup (or focus it if already open)
browser.action.openPopup();
});
});
// Smart content detection
async function detectContentType(info, tab) {
const url = info.linkUrl || info.srcUrl || tab?.url || '';
const title = tab?.title || '';
try {
const urlObj = new URL(url);
const domain = urlObj.hostname.toLowerCase();
// Video detection
if (url.includes('youtube.com/watch') || url.includes('youtu.be/')) {
return {
type: 'video',
platform: 'youtube',
suggestedTags: ['video', 'youtube', 'educational'],
autoTitle: extractYouTubeTitle(url) || title
};
}
if (url.includes('vimeo.com') || url.includes('dailymotion.com')) {
return {
type: 'video',
platform: domain.replace('.com', ''),
suggestedTags: ['video', domain.replace('.com', '')]
};
}
// Social media detection
if (domain.includes('twitter.com') || domain.includes('x.com')) {
return {
type: 'social',
platform: 'twitter',
suggestedTags: ['social', 'twitter', 'tweet']
};
}
if (domain.includes('linkedin.com')) {
return {
type: 'social',
platform: 'linkedin',
suggestedTags: ['social', 'linkedin', 'professional']
};
}
if (domain.includes('reddit.com')) {
return {
type: 'social',
platform: 'reddit',
suggestedTags: ['social', 'reddit', 'discussion']
};
}
// Development platforms
if (domain.includes('github.com')) {
return {
type: 'code',
platform: 'github',
suggestedTags: ['code', 'github', 'development', 'repository']
};
}
if (domain.includes('stackoverflow.com')) {
return {
type: 'code',
platform: 'stackoverflow',
suggestedTags: ['code', 'stackoverflow', 'programming', 'qa']
};
}
if (domain.includes('medium.com')) {
return {
type: 'article',
platform: 'medium',
suggestedTags: ['article', 'blog', 'medium']
};
}
// Documentation
if (domain.includes('docs.') || domain.includes('documentation')) {
return {
type: 'documentation',
suggestedTags: ['documentation', 'docs', 'reference']
};
}
// News sites
if (domain.includes('news.') || domain.includes('cnn.com') || domain.includes('bbc.com') ||
domain.includes('reuters.com') || domain.includes('washingtonpost.com')) {
return {
type: 'news',
suggestedTags: ['news', 'article', 'current-events']
};
}
// E-commerce
if (domain.includes('amazon.com') || domain.includes('ebay.com') ||
domain.includes('shopify.com') || domain.includes('etsy.com')) {
return {
type: 'shopping',
suggestedTags: ['shopping', 'product', 'ecommerce']
};
}
// Default detection
return {
type: 'general',
suggestedTags: ['bookmark', 'webpage']
};
} catch (e) {
return {
type: 'general',
suggestedTags: ['bookmark', 'webpage']
};
}
}
// Extract YouTube video title
function extractYouTubeTitle(url) {
try {
const urlObj = new URL(url);
const videoId = urlObj.searchParams.get('v');
if (videoId) {
// In a real implementation, you might fetch YouTube API
// For now, return null and let the page title be used
return null;
}
} catch (e) {
return null;
}
}
+11
View File
@@ -0,0 +1,11 @@
/* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
// Export the browser object for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = browser;
}

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 769 B

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

Before

Width:  |  Height:  |  Size: 275 B

After

Width:  |  Height:  |  Size: 275 B

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 346 B

@@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "Trackeep Saver", "name": "Trackeep Saver",
"version": "0.1.0", "version": "0.2.0",
"description": "Save the current page or a file to your Trackeep account as a bookmark or upload.", "description": "Smart content detection and quick saving for Trackeep with auto-tagging and recommendations.",
"action": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_title": "Save to Trackeep" "default_title": "Save to Trackeep"
@@ -15,11 +15,21 @@
"storage", "storage",
"tabs", "tabs",
"activeTab", "activeTab",
"contextMenus" "contextMenus",
"scripting"
], ],
"host_permissions": [ "host_permissions": [
"<all_urls>" "<all_urls>"
], ],
"commands": {
"quick-save": {
"suggested_key": {
"default": "Ctrl+Shift+S",
"mac": "Command+Shift+S"
},
"description": "Quick save current page to Trackeep"
}
},
"icons": { "icons": {
"16": "icons/icon16.png", "16": "icons/icon16.png",
"32": "icons/icon32.png", "32": "icons/icon32.png",
+903
View File
@@ -0,0 +1,903 @@
<!DOCTYPE html>
<html lang="en" data-kb-theme="dark">
<head>
<meta charset="UTF-8" />
<title>Trackeep Saver Options</title>
<style>
/* Modern Inter Font */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
/* Modern CSS Variables - Proton Pass Inspired */
:root {
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-tertiary: #262626;
--bg-hover: #2a2a2a;
--bg-active: #333333;
--border-primary: #2a2a2a;
--border-secondary: #333333;
--text-primary: #ffffff;
--text-secondary: #a3a3a3;
--text-tertiary: #737373;
--accent-primary: #3b82f6;
--accent-hover: #2563eb;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
--gradient-secondary: linear-gradient(135deg, #1a1a1a 0%, #262626 100%);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
font-size: 14px;
color-scheme: dark;
}
/* Container */
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px;
background: var(--bg-primary);
min-height: 100vh;
}
/* Header */
.header {
background: var(--gradient-secondary);
padding: 32px 20px 20px;
border-bottom: 1px solid var(--border-primary);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--gradient-primary);
}
.header-content {
max-width: 800px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 16px;
}
.logo-container {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 20px;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.logo::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
}
.title-section {
flex: 1;
}
.title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 4px 0;
}
.subtitle {
font-size: 14px;
color: var(--text-secondary);
margin: 0;
}
/* Main Content */
.main-content {
max-width: 800px;
margin: 0 auto;
}
/* Section */
.section {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: 32px;
margin-bottom: 24px;
border: 1px solid var(--border-primary);
box-shadow: var(--shadow-lg);
}
.section-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-primary);
}
.section-icon {
width: 48px;
height: 48px;
background: var(--gradient-primary);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.section-description {
font-size: 14px;
color: var(--text-secondary);
margin: 4px 0 0 0;
}
/* Form Elements */
.form {
max-width: 100%;
}
.form-group {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Install Welcome Styles */
.install-welcome {
padding: 40px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.install-card {
background: var(--bg-secondary);
border-radius: var(--radius-xl);
border: 1px solid var(--border-primary);
box-shadow: var(--shadow-lg);
max-width: 600px;
width: 100%;
overflow: hidden;
}
.install-header {
background: var(--gradient-primary);
padding: 32px;
text-align: center;
color: white;
}
.install-icon {
font-size: 48px;
margin-bottom: 16px;
}
.install-header h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 700;
}
.install-header p {
margin: 0;
opacity: 0.9;
font-size: 16px;
}
.setup-steps {
padding: 32px;
}
.step {
display: flex;
gap: 20px;
margin-bottom: 32px;
align-items: flex-start;
}
.step-number {
background: var(--accent-primary);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
flex-shrink: 0;
}
.step-content h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.step-content p {
margin: 0 0 12px 0;
color: var(--text-secondary);
line-height: 1.5;
}
.security-note {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: var(--radius-md);
padding: 12px;
margin-top: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.security-note .icon {
color: var(--success);
flex-shrink: 0;
}
.security-note strong {
color: var(--success);
}
.main-options {
display: none;
}
/* Setup Form Styles */
.setup-form {
margin-top: 16px;
padding: 16px;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
}
.setup-form .form-group {
margin-bottom: 16px;
}
.setup-form .form-group:last-child {
margin-bottom: 0;
}
.setup-form label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 6px;
}
.setup-form .form-input {
width: 100%;
padding: 12px 14px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-primary);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 14px;
font-family: 'Inter', sans-serif;
transition: all 0.2s ease;
outline: none;
}
.setup-form .form-input:focus {
border-color: var(--accent-primary);
background: var(--bg-hover);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.setup-form .input-help {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
.setup-actions {
display: flex;
gap: 12px;
margin-top: 24px;
width: 100%;
}
.setup-actions .btn {
flex: 1;
}
/* Button Groups */
.btn-group {
display: flex;
gap: 12px;
margin-top: 20px;
width: 100%;
}
.btn-group .btn {
flex: 1;
}
/* Full Width Elements */
.btn-block {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Instructions */
.instructions {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: 16px;
border: 1px solid var(--border-primary);
margin-top: 16px;
width: 100%;
}
input[type="url"],
input[type="password"] {
width: 100%;
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 14px;
font-family: 'Inter', sans-serif;
font-weight: 400;
transition: all 0.2s ease;
outline: none;
}
input[type="text"]:focus,
input[type="url"]:focus,
input[type="password"]:focus {
border-color: var(--accent-primary);
background: var(--bg-hover);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Instructions */
.instructions {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: 16px;
border: 1px solid var(--border-primary);
margin-top: 16px;
}
.instructions-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 6px;
}
.instructions-list {
font-size: 13px;
color: var(--text-secondary);
margin: 0;
padding-left: 16px;
line-height: 1.6;
}
.instructions-list li {
margin-bottom: 4px;
}
.instructions-list li:last-child {
margin-bottom: 0;
}
/* Buttons */
.btn {
padding: 14px 24px;
border-radius: var(--radius-md);
border: none;
font-size: 14px;
font-weight: 500;
font-family: 'Inter', sans-serif;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
outline: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
background: var(--gradient-primary);
color: white;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.btn-primary:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
/* Status Messages */
.status-message {
padding: 16px 20px;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 500;
margin-top: 20px;
display: flex;
align-items: center;
gap: 10px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.status-message.success {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.security-badge {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: var(--radius-sm);
padding: 6px 10px;
font-size: 11px;
font-weight: 600;
color: var(--success);
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.security-badge .icon {
flex-shrink: 0;
}
.connection-status {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: 16px;
margin-top: 20px;
border: 1px solid var(--border-primary);
}
.connection-status.connected {
border-color: var(--success);
background: rgba(16, 185, 129, 0.05);
}
.connection-status.error {
border-color: var(--error);
background: rgba(239, 68, 68, 0.05);
}
.status-message.info {
background: rgba(59, 130, 246, 0.1);
color: var(--accent-primary);
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* Code styling */
code {
background: var(--bg-tertiary);
padding: 3px 8px;
border-radius: var(--radius-sm);
font-size: 12px;
color: var(--text-primary);
border: 1px solid var(--border-primary);
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
}
/* Icon System */
.icon {
width: 16px;
height: 16px;
display: inline-block;
vertical-align: middle;
transition: all 0.2s ease;
fill: white;
stroke: white;
}
.icon-sm {
width: 12px;
height: 12px;
fill: white;
stroke: white;
}
.icon-lg {
width: 20px;
height: 20px;
fill: white;
stroke: white;
}
.icon-xl {
width: 24px;
height: 24px;
fill: white;
stroke: white;
}
/* External SVG Icons */
img.icon {
filter: brightness(0) invert(1);
}
img.icon-sm {
filter: brightness(0) invert(1);
}
img.icon-lg {
filter: brightness(0) invert(1);
}
img.icon-xl {
filter: brightness(0) invert(1);
}
/* Icon animations */
.icon-spin {
animation: spin 1s linear infinite;
}
.icon-pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
/* Enhanced button icons */
.btn .icon {
transition: transform 0.2s ease;
}
.btn:hover .icon {
transform: scale(1.1);
}
.btn:active .icon {
transform: scale(0.95);
}
/* Section icon enhancements */
.section-icon {
transition: all 0.3s ease;
}
.section:hover .section-icon {
transform: scale(1.05) rotate(5deg);
box-shadow: var(--shadow-md);
}
/* Responsive */
@media (max-width: 640px) {
.container {
padding: 20px 16px;
}
.section {
padding: 20px;
}
.header {
padding: 24px 16px 16px;
}
.title {
font-size: 24px;
}
}
</style>
</head>
<body>
<!-- First-time Install Welcome -->
<div id="installWelcome" class="install-welcome" style="display: none;">
<div class="install-card">
<div class="install-header">
<div class="install-icon">🎉</div>
<h2>Welcome to Trackeep Saver!</h2>
<p>Let's set up your connection to get started.</p>
</div>
<div class="setup-steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h3>Get Your API Key</h3>
<p>Log into your Trackeep account and generate an API key in Settings → Security.</p>
<div class="security-note">
<img src="https://www.svgrepo.com/show/381193/secure-shield-password-protect-safe.svg" alt="Security" class="icon" style="width: 16px; height: 16px;" />
<strong>Secure:</strong> API keys are safer than JWT tokens and can be revoked anytime.
</div>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h3>Configure Connection</h3>
<p>Enter your Trackeep URL and API key below.</p>
<div class="setup-form">
<div class="form-group">
<label for="setupApiUrl">Trackeep URL</label>
<input type="url" id="setupApiUrl" placeholder="https://your-trackeep.com/api/v1" class="form-input" />
<div class="input-help">Your Trackeep instance API URL</div>
</div>
<div class="form-group">
<label for="setupApiKey">API Key</label>
<input type="password" id="setupApiKey" placeholder="tk_..." class="form-input" />
<div class="input-help">
<div class="security-badge">
<img src="https://www.svgrepo.com/show/381193/secure-shield-password-protect-safe.svg" alt="Security" class="icon" style="width: 16px; height: 16px;" />
<span>Secure API Key</span>
</div>
More secure than JWT tokens, revocable anytime
</div>
</div>
</div>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h3>
<img src="https://www.svgrepo.com/show/448375/connection-gateway.svg" alt="Test" class="icon" style="width: 20px; height: 20px; margin-right: 8px;" />
Test Connection
</h3>
<p>Verify your connection works before saving bookmarks.</p>
<div id="setupConnectionStatus" class="connection-status" style="display: none;">
<div class="status-content">
<img src="https://www.svgrepo.com/show/448375/connection-gateway.svg" alt="Status" class="icon" style="width: 16px; height: 16px;" />
<div>
<strong id="setupStatusTitle">Testing Connection...</strong>
<p id="setupStatusMessage">Please wait</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="setup-actions">
<button id="testSetupConnectionBtn" class="btn btn-primary">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 8l-3-3-6-6z"></path>
<path d="M21 12v.01M5 12c0-4.41 3.58-8 8s8 3.58 8 8-3.58 8-8-8z"></path>
</svg>
Test Connection
</button>
<button id="completeSetupBtn" class="btn btn-secondary">
<img src="https://www.svgrepo.com/show/521819/save.svg" alt="Complete" class="icon" style="width: 16px; height: 16px;" />
Complete Setup
</button>
</div>
</div>
</div>
</div>
<!-- Main Options -->
<div id="mainOptions" class="container">
<header class="header">
<div class="header-content">
<div class="logo-container">
<div class="logo">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/trackeep.svg" alt="Trackeep" class="icon-xl" style="width: 24px; height: 24px; fill: white;" />
</div>
<div>
<h1 class="title">Trackeep Saver</h1>
<p class="subtitle">Browser Extension Settings</p>
</div>
</div>
</div>
</header>
<main class="main-content">
<section class="section">
<div class="section-header">
<div class="section-icon">
<img src="https://www.svgrepo.com/show/505495/settings.svg" alt="Settings" class="icon-xl" style="width: 24px; height: 24px;" />
</div>
<h2 class="section-title">Connection Settings</h2>
<p class="section-description">Configure your Trackeep connection and API key</p>
</div>
<form id="optionsForm" class="form">
<div class="form-group">
<label for="trackeepApiUrl">Trackeep URL</label>
<input type="url" id="trackeepApiUrl" placeholder="https://your-trackeep.com/api/v1" class="form-input" />
<div class="input-help">Your Trackeep instance API URL</div>
</div>
<div class="form-group">
<label for="trackeepApiKey">API Key</label>
<input type="password" id="trackeepApiKey" placeholder="tk_..." class="form-input" />
<div class="input-help">
<div class="security-badge">
<img src="https://www.svgrepo.com/show/381193/secure-shield-password-protect-safe.svg" alt="Security" class="icon" style="width: 16px; height: 16px;" />
<span>Secure API Key</span>
</div>
More secure than JWT tokens, revocable anytime
</div>
</div>
<div id="connectionStatus" class="connection-status" style="display: none;">
<div class="status-content">
<img src="https://www.svgrepo.com/show/448375/connection-gateway.svg" alt="Status" class="icon" style="width: 16px; height: 16px;" />
<div>
<strong id="statusTitle">Testing Connection...</strong>
<p id="statusMessage">Please wait</p>
</div>
</div>
</div>
</form>
<div class="btn-group">
<button id="testConnectionBtn" class="btn btn-primary">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 8l-3-3-6-6z"></path>
<path d="M21 12v.01M5 12c0-4.41 3.58-8 8s8 3.58 8 8-3.58 8-8-8z"></path>
</svg>
Test Connection
</button>
<button id="generateKeyBtn" class="btn btn-secondary">
<img src="https://www.svgrepo.com/show/532805/file-shredder.svg" alt="Generate" class="icon" style="width: 16px; height: 16px;" />
Generate API Key
</button>
</div>
<div class="instructions">
<div class="instructions-title">
<img src="https://www.svgrepo.com/show/447845/website-click.svg" alt="Instructions" class="icon" style="width: 16px; height: 16px;" />
<span>How to get your API key:</span>
</div>
<ol class="instructions-list">
<li>Log into your Trackeep account</li>
<li>Go to Settings → Security</li>
<li>Click "Generate New API Key"</li>
<li>Copy the generated key (starts with <code>tk_</code>)</li>
<li>Paste the key in the field above</li>
<li><strong>API keys are more secure than JWT tokens</strong> - they can be revoked anytime and have limited permissions</li>
</ol>
</div>
<button class="btn btn-primary" id="saveBtn" style="margin-top: 24px;">
<img src="https://www.svgrepo.com/show/521819/save.svg" alt="Save" class="icon" style="width: 16px; height: 16px;" />
<span>Save Settings</span>
</button>
<div id="statusMessage" class="status-message" style="display: none;"></div>
</section>
</main>
</div>
<script src="options.js"></script>
</body>
</html>
+374
View File
@@ -0,0 +1,374 @@
/* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
const apiBaseUrlInput = document.getElementById('trackeepApiUrl');
const apiKeyInput = document.getElementById('trackeepApiKey');
const testConnectionBtn = document.getElementById('testConnectionBtn');
const generateKeyBtn = document.getElementById('generateKeyBtn');
const saveBtn = document.getElementById('saveBtn');
const statusMessageEl = document.getElementById('statusMessage');
const connectionStatusEl = document.getElementById('connectionStatus');
const statusTitleEl = document.getElementById('statusTitle');
const statusTextEl = document.getElementById('statusMessage');
const installWelcomeEl = document.getElementById('installWelcome');
const mainOptionsEl = document.getElementById('mainOptions');
function showMessage(message, type = 'info', duration = 5000) {
statusMessageEl.textContent = message;
statusMessageEl.className = `status-message ${type}`;
statusMessageEl.style.display = 'flex';
if (duration > 0) {
setTimeout(() => {
statusMessageEl.style.display = 'none';
}, duration);
}
}
function hideMessage() {
statusMessageEl.style.display = 'none';
}
function showConnectionStatus(title, message, type = 'info') {
connectionStatusEl.style.display = 'block';
statusTitleEl.textContent = title;
statusTextEl.textContent = message;
connectionStatusEl.className = `connection-status ${type}`;
}
function hideConnectionStatus() {
connectionStatusEl.style.display = 'none';
}
function setButtonLoading(button, loading = true) {
if (loading) {
button.disabled = true;
const originalContent = button.innerHTML;
button.dataset.originalContent = originalContent;
button.innerHTML = `
<svg class="icon icon-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<span>Saving...</span>
`;
} else {
button.disabled = false;
if (button.dataset.originalContent) {
button.innerHTML = button.dataset.originalContent;
delete button.dataset.originalContent;
}
}
}
function detectAndPrefillApiBaseUrl(callback) {
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0];
if (!tab || !tab.url) {
if (callback) callback();
return;
}
try {
const url = new URL(tab.url);
const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost';
if (isTrackeepDomain && (url.protocol === 'https:' || url.protocol === 'http:')) {
const candidate = `${url.origin}/api/v1`;
browser.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = candidate;
}
if (callback) callback();
});
} else {
// Fallback to localhost if nothing set
browser.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = 'http://localhost:8080/api/v1';
}
if (callback) callback();
});
}
} catch (e) {
if (callback) callback();
}
});
}
function loadSettings() {
browser.storage.sync.get(['trackeepApiBaseUrl', 'trackeepApiKey', 'isFirstInstall'], (items) => {
// Handle first-time install
if (items.isFirstInstall) {
installWelcomeEl.style.display = 'flex';
mainOptionsEl.style.display = 'none';
} else {
installWelcomeEl.style.display = 'none';
mainOptionsEl.style.display = 'block';
}
// Load saved settings
if (items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = items.trackeepApiBaseUrl;
}
if (items.trackeepApiKey) {
apiKeyInput.value = items.trackeepApiKey;
}
// Auto-detect API URL if empty
if (!items.trackeepApiBaseUrl) {
detectAndPrefillApiBaseUrl();
}
});
}
function saveSettings() {
const apiBaseUrl = apiBaseUrlInput.value.trim();
const apiKey = apiKeyInput.value.trim();
if (!apiBaseUrl) {
showMessage('API base URL is required.', 'error');
return;
}
if (!apiKey) {
showMessage('API key is required.', 'error');
return;
}
if (!apiKey.startsWith('tk_')) {
showMessage('API key should start with "tk_"', 'error');
return;
}
setButtonLoading(saveBtn, true);
showMessage('Saving settings...', 'info', 0);
browser.storage.sync.set({
trackeepApiBaseUrl: apiBaseUrl,
trackeepApiKey: apiKey,
isFirstInstall: false
}, () => {
setButtonLoading(saveBtn, false);
showMessage('Settings saved successfully!', 'success');
});
}
async function testConnection() {
const apiBaseUrl = apiBaseUrlInput.value.trim();
const apiKey = apiKeyInput.value.trim();
if (!apiBaseUrl || !apiKey) {
showConnectionStatus('Connection Failed', 'Please enter both URL and API key', 'error');
return;
}
showConnectionStatus('Testing Connection', 'Connecting to your Trackeep instance...', 'info');
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/auth/me`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
showConnectionStatus('Connection Successful', `Connected as ${data.username || 'user'}. API key is valid!`, 'success');
// Hide success message after 3 seconds
setTimeout(() => {
hideConnectionStatus();
}, 3000);
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
showConnectionStatus('Connection Failed', `Error: ${error.message}`, 'error');
}
}
async function generateApiKey() {
const apiBaseUrl = apiBaseUrlInput.value.trim();
if (!apiBaseUrl) {
showMessage('Please enter API URL first', 'error');
return;
}
showConnectionStatus('Generating API Key', 'Opening Trackeep to generate new API key...', 'info');
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/auth/generate-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.api_key) {
apiKeyInput.value = data.api_key;
showConnectionStatus('API Key Generated', 'New API key generated and copied to clipboard!', 'success');
// Copy to clipboard
navigator.clipboard.writeText(data.api_key);
// Hide success message after 3 seconds
setTimeout(() => {
hideConnectionStatus();
}, 3000);
} else {
throw new Error('No API key in response');
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
showConnectionStatus('Generation Failed', `Error: ${error.message}`, 'error');
}
}
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
detectAndPrefillApiBaseUrl(() => {
loadSettings();
});
// Event listeners for main options
saveBtn.addEventListener('click', (e) => {
e.preventDefault();
saveSettings();
});
testConnectionBtn.addEventListener('click', (e) => {
e.preventDefault();
testConnection();
});
generateKeyBtn.addEventListener('click', (e) => {
e.preventDefault();
generateApiKey();
});
// Event listeners for setup form
const testSetupConnectionBtn = document.getElementById('testSetupConnectionBtn');
const completeSetupBtn = document.getElementById('completeSetupBtn');
const getStartedBtn = document.getElementById('getStartedBtn');
if (testSetupConnectionBtn) {
testSetupConnectionBtn.addEventListener('click', (e) => {
e.preventDefault();
testSetupConnection();
});
}
if (completeSetupBtn) {
completeSetupBtn.addEventListener('click', (e) => {
e.preventDefault();
completeSetup();
});
}
if (getStartedBtn) {
getStartedBtn.addEventListener('click', (e) => {
e.preventDefault();
// Hide welcome and show main options with setup form
document.getElementById('installWelcome').style.display = 'none';
document.getElementById('mainOptions').style.display = 'block';
});
}
});
// Test connection from setup form
async function testSetupConnection() {
const apiBaseUrl = document.getElementById('setupApiUrl').value.trim();
const apiKey = document.getElementById('setupApiKey').value.trim();
if (!apiBaseUrl || !apiKey) {
showSetupConnectionStatus('Connection Failed', 'Please enter both URL and API key', 'error');
return;
}
showSetupConnectionStatus('Testing Connection', 'Connecting to your Trackeep instance...', 'info');
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/auth/me`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
showSetupConnectionStatus('Connection Successful', `Connected as ${data.username || 'user'}. API key is valid!`, 'success');
// Copy values to main form
document.getElementById('trackeepApiUrl').value = apiBaseUrl;
document.getElementById('trackeepApiKey').value = apiKey;
// Hide success message after 3 seconds
setTimeout(() => {
hideSetupConnectionStatus();
}, 3000);
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
showSetupConnectionStatus('Connection Failed', `Error: ${error.message}`, 'error');
}
}
// Complete setup
function completeSetup() {
const apiBaseUrl = document.getElementById('setupApiUrl').value.trim();
const apiKey = document.getElementById('setupApiKey').value.trim();
if (!apiBaseUrl || !apiKey) {
showMessage('Please fill in both URL and API key', 'error');
return;
}
// Save settings
browser.storage.sync.set({
trackeepApiBaseUrl: apiBaseUrl,
trackeepApiKey: apiKey,
isFirstInstall: false
}, () => {
showMessage('Setup completed successfully!', 'success');
// Switch to main options view
document.getElementById('installWelcome').style.display = 'none';
document.getElementById('mainOptions').style.display = 'block';
// Load settings in main form
document.getElementById('trackeepApiUrl').value = apiBaseUrl;
document.getElementById('trackeepApiKey').value = apiKey;
});
}
// Setup connection status functions
function showSetupConnectionStatus(title, message, type = 'info') {
const statusEl = document.getElementById('setupConnectionStatus');
const titleEl = document.getElementById('setupStatusTitle');
const messageEl = document.getElementById('setupStatusMessage');
statusEl.style.display = 'block';
titleEl.textContent = title;
messageEl.textContent = message;
statusEl.className = `connection-status ${type}`;
}
function hideSetupConnectionStatus() {
document.getElementById('setupConnectionStatus').style.display = 'none';
}
@@ -85,7 +85,44 @@
gap: 12px; gap: 12px;
} }
/* Enhanced logo animation */ /* Icon Definitions */
.icon {
width: 16px;
height: 16px;
fill: white;
stroke: white;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.icon-sm {
width: 14px;
height: 14px;
}
.icon-lg {
width: 20px;
height: 20px;
}
.icon-xl {
width: 24px;
height: 24px;
}
/* External SVG Icons */
img.icon {
filter: brightness(0) invert(1);
}
img.icon-sm {
filter: brightness(0) invert(1);
}
img.icon-xl {
filter: brightness(0) invert(1);
}
.logo { .logo {
width: 32px; width: 32px;
height: 32px; height: 32px;
@@ -496,10 +533,85 @@
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.btn-secondary { /* Enhanced Header Styles */
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.quick-save-btn {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
border: none;
padding: 8px 12px;
border-radius: var(--radius-md);
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
box-shadow: var(--shadow-sm);
}
.quick-save-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-md);
background: linear-gradient(135deg, #059669 0%, #047857 100%);
}
.content-type-indicator {
padding: 6px 12px;
border-radius: var(--radius-md);
font-size: 12px;
font-weight: 600;
margin: 8px 0;
background: var(--bg-tertiary); background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-primary); border: 1px solid var(--border-primary);
display: inline-block;
}
/* Tags Suggestions */
.tags-container {
position: relative;
}
.suggested-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
padding: 8px;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
}
.suggested-tag {
background: var(--accent-primary);
color: white;
padding: 4px 8px;
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.suggested-tag:hover {
background: var(--accent-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.tags-help {
font-size: 11px;
color: var(--text-tertiary);
margin-top: 4px;
font-style: italic;
} }
.btn-secondary:hover { .btn-secondary:hover {
@@ -741,30 +853,38 @@
</style> </style>
</head> </head>
<body> <body>
<!-- Header --> <!-- Enhanced Header with Smart Detection -->
<header class="header"> <div class="header">
<div class="header-content"> <div class="header-content">
<div class="logo-container"> <div class="logo-container">
<div class="logo">T</div> <div class="logo">
<h1 class="title">Trackeep</h1> <img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/trackeep.svg" alt="Trackeep" class="icon-xl" style="width: 24px; height: 24px; fill: white;" />
</div>
<div>
<div class="title">Trackeep Saver</div>
<div id="statusIndicator" class="status-indicator"></div>
</div>
</div>
<div class="header-actions">
<button id="quickSaveBtn" class="quick-save-btn" title="Quick Save (Ctrl+Shift+S)">
<img src="https://www.svgrepo.com/show/521819/save.svg" alt="Save" class="icon-sm" style="width: 14px; height: 14px;" />
<span>Quick Save</span>
</button>
<button id="openOptions" class="options-btn" title="Options">
<img src="https://www.svgrepo.com/show/505495/settings.svg" alt="Settings" class="icon" style="width: 16px; height: 16px;" />
</button>
</div>
</div> </div>
<button class="options-btn" id="openOptions" title="Settings"> <div id="statusText" class="status-text">Loading...</div>
<svg class="icon icon-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <div id="contentTypeIndicator" class="content-type-indicator" style="display: none;"></div>
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v6m0 6v6m4.22-13.22l4.24 4.24M1.54 1.54l4.24 4.24M1 12h6m6 0h6"/>
</svg>
</button>
</div> </div>
</header>
<!-- Status Bar --> <!-- Status Bar -->
<div class="status-bar"> <div class="status-bar">
<div class="status-indicator" id="statusIndicator"></div> </div>
<div class="status-text" id="statusText">Checking configuration...</div>
</div>
<!-- Main Content --> <!-- Main Content -->
<main class="content"> <main class="content">
<!-- Tabs --> <!-- Tabs -->
<div class="tabs"> <div class="tabs">
<button class="tab active" data-tab="bookmark"> <button class="tab active" data-tab="bookmark">
@@ -776,7 +896,6 @@
<button class="tab" data-tab="file"> <button class="tab" data-tab="file">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14,2 14,8 20,8"/>
</svg> </svg>
<span>File</span> <span>File</span>
</button> </button>
@@ -787,32 +906,35 @@
<div class="form-section"> <div class="form-section">
<div class="section-header"> <div class="section-header">
<div class="section-icon"> <div class="section-icon">
<svg class="icon-xl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <img src="https://www.svgrepo.com/show/447845/website-click.svg" alt="Website" class="icon-xl" style="width: 24px; height: 24px;" />
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
</div> </div>
<h2 class="section-title">Save Current Page</h2> <h2 class="section-title">Save Current Page</h2>
</div> </div>
<!-- Enhanced Bookmark Form with Smart Suggestions -->
<div class="form-group"> <div class="form-group">
<label for="bookmarkTitle">Title</label> <label for="bookmarkTitle">Title</label>
<input id="bookmarkTitle" type="text" placeholder="Page title will be auto-filled" /> <input type="text" id="bookmarkTitle" placeholder="Page title..." class="form-input" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="bookmarkUrl">URL</label> <label for="bookmarkUrl">URL</label>
<input id="bookmarkUrl" type="url" placeholder="https://example.com" /> <input type="url" id="bookmarkUrl" placeholder="https://..." class="form-input" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="bookmarkDescription">Description</label> <label for="bookmarkDescription">Description</label>
<textarea id="bookmarkDescription" placeholder="Why is this page important? Add notes..."></textarea> <textarea id="bookmarkDescription" placeholder="Optional description..." rows="3" class="form-input"></textarea>
</div> </div>
<!-- Smart Tags Suggestions -->
<div class="form-group"> <div class="form-group">
<label for="bookmarkTags">Tags</label> <label for="bookmarkTags">Tags</label>
<input id="bookmarkTags" type="text" placeholder="reading, development, tutorial" /> <div class="tags-container">
<input type="text" id="bookmarkTags" placeholder="comma-separated tags..." class="form-input" />
<div id="suggestedTags" class="suggested-tags" style="display: none;"></div>
</div>
<div class="tags-help">Click suggested tags to add them automatically</div>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -841,11 +963,7 @@
<div class="form-section"> <div class="form-section">
<div class="section-header"> <div class="section-header">
<div class="section-icon"> <div class="section-icon">
<svg class="icon-xl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <img src="https://www.svgrepo.com/show/532805/file-shredder.svg" alt="File" class="icon-xl" style="width: 24px; height: 24px;" />
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17,8 12,3 7,8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
</div> </div>
<h2 class="section-title">Upload File</h2> <h2 class="section-title">Upload File</h2>
</div> </div>
@@ -857,14 +975,16 @@
<div class="form-group"> <div class="form-group">
<label for="fileDescription">Description</label> <label for="fileDescription">Description</label>
<textarea id="fileDescription" placeholder="Describe this file..."></textarea> <textarea id="fileDescription" placeholder="Describe this file..." rows="3"></textarea>
</div> </div>
<button class="btn btn-primary btn-block" id="uploadFileBtn"> <button class="btn btn-primary btn-block" id="uploadFileBtn">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> <path d="M21 15v4a2 2 0 0 1-2h-4a2 2 0 0 0 1 2 2v4"/>
<polyline points="17,8 12,3 7,8"/> <polyline points="17,8 12,8 8,8"/>
<line x1="12" y1="3" x2="12" y2="15"/> <line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10,9 9,9 8,9"/>
</svg> </svg>
<span>Upload File</span> <span>Upload File</span>
</button> </button>
@@ -1,4 +1,9 @@
/* global chrome */ /* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
// DOM Elements // DOM Elements
const statusIndicatorEl = document.getElementById('statusIndicator'); const statusIndicatorEl = document.getElementById('statusIndicator');
@@ -23,11 +28,19 @@ const fileInput = document.getElementById('fileInput');
const fileDescriptionInput = document.getElementById('fileDescription'); const fileDescriptionInput = document.getElementById('fileDescription');
const uploadFileBtn = document.getElementById('uploadFileBtn'); const uploadFileBtn = document.getElementById('uploadFileBtn');
// Smart suggestion elements
const suggestedTagsContainer = document.getElementById('suggestedTags');
const contentTypeIndicator = document.getElementById('contentTypeIndicator');
const quickSaveBtn = document.getElementById('quickSaveBtn');
let trackeepConfig = { let trackeepConfig = {
apiBaseUrl: '', apiBaseUrl: '',
authToken: '' authToken: ''
}; };
let smartData = null;
let isQuickSaveMode = false;
// Tab switching functionality // Tab switching functionality
function initTabs() { function initTabs() {
tabBtns.forEach(btn => { tabBtns.forEach(btn => {
@@ -112,7 +125,7 @@ function disableForms(disabled) {
} }
function loadConfig(callback) { function loadConfig(callback) {
chrome.storage.sync.get(['trackeepApiBaseUrl', 'trackeepAuthToken'], (items) => { browser.storage.sync.get(['trackeepApiBaseUrl', 'trackeepAuthToken'], (items) => {
const apiBaseUrl = (items.trackeepApiBaseUrl || '').trim(); const apiBaseUrl = (items.trackeepApiBaseUrl || '').trim();
const authToken = (items.trackeepAuthToken || '').trim(); const authToken = (items.trackeepAuthToken || '').trim();
@@ -135,7 +148,7 @@ function loadConfig(callback) {
} }
function detectTrackeepDomain(callback) { function detectTrackeepDomain(callback) {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0]; const tab = tabs && tabs[0];
if (!tab || !tab.url) { if (!tab || !tab.url) {
if (callback) callback(); if (callback) callback();
@@ -147,9 +160,9 @@ function detectTrackeepDomain(callback) {
const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost'; const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost';
if (isTrackeepDomain && url.protocol === 'https:') { if (isTrackeepDomain && url.protocol === 'https:') {
const candidate = `${url.origin}/api/v1`; const candidate = `${url.origin}/api/v1`;
chrome.storage.sync.get(['trackeepApiBaseUrl'], (items) => { browser.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) { if (!items.trackeepApiBaseUrl) {
chrome.storage.sync.set({ trackeepApiBaseUrl: candidate }, () => { browser.storage.sync.set({ trackeepApiBaseUrl: candidate }, () => {
console.log('Auto-detected Trackeep API URL:', candidate); console.log('Auto-detected Trackeep API URL:', candidate);
if (callback) callback(); if (callback) callback();
}); });
@@ -167,13 +180,18 @@ function detectTrackeepDomain(callback) {
} }
function initActiveTab() { function initActiveTab() {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0]; const tab = tabs && tabs[0];
if (!tab) return; if (!tab) return;
chrome.storage.local.get(['contextMenuData'], (items) => { browser.storage.local.get(['contextMenuData'], (items) => {
const ctx = items.contextMenuData; const ctx = items.contextMenuData;
if (ctx && ctx.timestamp && Date.now() - ctx.timestamp < 5000) { if (ctx && ctx.timestamp && Date.now() - ctx.timestamp < 5000) {
// Use context menu data
smartData = ctx.smartData || null;
isQuickSaveMode = ctx.isQuickSave || false;
if (ctx.url && !bookmarkUrlInput.value) { if (ctx.url && !bookmarkUrlInput.value) {
bookmarkUrlInput.value = ctx.url; bookmarkUrlInput.value = ctx.url;
} }
@@ -183,8 +201,22 @@ function initActiveTab() {
if (ctx.selection && !bookmarkDescriptionInput.value) { if (ctx.selection && !bookmarkDescriptionInput.value) {
bookmarkDescriptionInput.value = ctx.selection; bookmarkDescriptionInput.value = ctx.selection;
} }
chrome.storage.local.remove(['contextMenuData']);
// Apply smart suggestions
if (smartData) {
applySmartSuggestions(smartData);
}
// Handle quick save mode
if (isQuickSaveMode) {
handleQuickSave();
}
browser.storage.local.remove(['contextMenuData']);
} else { } else {
// Regular tab detection
detectAndApplySmartData(tab);
if (tab.title && !bookmarkTitleInput.value) { if (tab.title && !bookmarkTitleInput.value) {
bookmarkTitleInput.value = tab.title; bookmarkTitleInput.value = tab.title;
} }
@@ -196,6 +228,98 @@ function initActiveTab() {
}); });
} }
// Smart data detection for regular tab
async function detectAndApplySmartData(tab) {
try {
const info = { linkUrl: tab.url, srcUrl: tab.url };
smartData = await detectContentType(info, tab);
if (smartData) {
applySmartSuggestions(smartData);
}
} catch (e) {
console.error('Smart detection failed:', e);
}
}
// Apply smart suggestions to UI
function applySmartSuggestions(data) {
// Show content type indicator
if (contentTypeIndicator) {
const typeColors = {
video: '#ff0000',
social: '#1da1f2',
code: '#0969da',
article: '#ff6900',
documentation: '#6f42c1',
news: '#ff4500',
shopping: '#ff9500',
general: '#6b7280'
};
const typeIcons = {
video: '🎥',
social: '💬',
code: '💻',
article: '📝',
documentation: '📚',
news: '📰',
shopping: '🛒',
general: '🔗'
};
contentTypeIndicator.innerHTML = `
<span style="color: ${typeColors[data.type] || typeColors.general}; font-weight: 600;">
${typeIcons[data.type] || typeIcons.general} ${data.type.charAt(0).toUpperCase() + data.type.slice(1)}
</span>
${data.platform ? `<span style="color: #6b7280; font-size: 0.85em; margin-left: 8px;">• ${data.platform}</span>` : ''}
`;
contentTypeIndicator.style.display = 'inline-block';
}
// Show suggested tags
if (suggestedTagsContainer && data.suggestedTags) {
suggestedTagsContainer.innerHTML = '';
data.suggestedTags.forEach(tag => {
const tagEl = document.createElement('span');
tagEl.className = 'suggested-tag';
tagEl.textContent = tag;
tagEl.onclick = () => addSuggestedTag(tag);
suggestedTagsContainer.appendChild(tagEl);
});
suggestedTagsContainer.style.display = 'flex';
}
}
// Add suggested tag to input
function addSuggestedTag(tag) {
const currentTags = bookmarkTagsInput.value
.split(',')
.map(t => t.trim())
.filter(t => t);
if (!currentTags.includes(tag)) {
currentTags.push(tag);
bookmarkTagsInput.value = currentTags.join(', ');
}
}
// Handle quick save
function handleQuickSave() {
if (isQuickSaveMode && smartData) {
// Auto-fill with smart data and save immediately
if (smartData.suggestedTags && !bookmarkTagsInput.value) {
bookmarkTagsInput.value = smartData.suggestedTags.join(', ');
}
// Auto-save after a short delay
setTimeout(() => {
if (bookmarkUrlInput.value && bookmarkTitleInput.value) {
saveBookmark(new Event('submit'));
}
}, 500);
}
}
async function saveBookmark(event) { async function saveBookmark(event) {
event.preventDefault(); event.preventDefault();
hideMessage(); hideMessage();
@@ -350,10 +474,10 @@ async function uploadFile(event) {
} }
function openOptions() { function openOptions() {
if (chrome.runtime.openOptionsPage) { if (browser.runtime.openOptionsPage) {
chrome.runtime.openOptionsPage(); browser.runtime.openOptionsPage();
} else { } else {
window.open(chrome.runtime.getURL('options.html')); window.open(browser.runtime.getURL('options.html'));
} }
} }
@@ -364,6 +488,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Event listeners // Event listeners
openOptionsBtn.addEventListener('click', openOptions); openOptionsBtn.addEventListener('click', openOptions);
quickSaveBtn.addEventListener('click', handleQuickSave);
saveBookmarkBtn.addEventListener('click', (e) => { saveBookmarkBtn.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
saveBookmark(e); saveBookmark(e);
@@ -373,6 +498,14 @@ document.addEventListener('DOMContentLoaded', () => {
uploadFile(e); uploadFile(e);
}); });
// Keyboard shortcut for quick save
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
handleQuickSave();
}
});
// Initialize configuration and active tab // Initialize configuration and active tab
detectTrackeepDomain(() => { detectTrackeepDomain(() => {
loadConfig(() => { loadConfig(() => {

Some files were not shown because too many files have changed in this diff Show More