14 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
Tomas Dvorak 446bc7acfb fix: remove verbatimModuleSyntax to resolve CI TypeScript build errors
- Remove verbatimModuleSyntax: true from tsconfig.app.json
- This option caused async/await syntax errors in CI/CD environment
- Build now works consistently across local and CI environments
- Fixes TS1308 errors on await expressions in updateService.ts
2026-02-27 19:24:18 +01:00
Tomas Dvorak 90f0b90cc7 fix: remove verbatimModuleSyntax to resolve CI TypeScript build errors
- Remove verbatimModuleSyntax: true from tsconfig.app.json
- This option caused async/await syntax errors in CI/CD environment
- Build now works consistently across local and CI environments
- Fixes TS1308 errors on await expressions in updateService.ts
2026-02-27 19:24:05 +01:00
Tomas Dvorak ecd31f4e3b small fix 2026-02-27 19:11:40 +01:00
Tomas Dvorak 9c17f80d5d chore: complete simplified version system v1.2.5
 Complete Simplified Version System:
- Version detection from source code (package.json/go.mod)
- GitHub Actions workflow for automated releases
- Zero setup required for users
- Industry-standard semantic versioning

🚀 Ready for automated releases!
2026-02-27 19:09:43 +01:00
Tomas Dvorak 3b8e14c6b8 chore: update to v1.2.5 and simplify version management
🎯 Simplified Version System:
- Update frontend/backend to v1.2.5 in package.json and go.mod
- Frontend reads version from package.json directly
- Backend reads version from go.mod directly
- No environment variables needed for versioning

🔄 Automated Release Workflow:
- GitHub Actions automatically updates all version files
- Extracts version from Git tag (v1.2.5)
- Updates package.json, go.mod, docker-compose files
- Builds and pushes Docker images with proper tags
- Creates GitHub release automatically

🚀 User Experience:
- Just: docker compose up
- System auto-detects version from code
- Updates work with no manual setup
- Proper semantic versioning (MAJOR.MINOR.PATCH)

Ready for automated release!
2026-02-27 19:08:24 +01:00
213 changed files with 16339 additions and 51128 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
.gitignore
README.md
.env
.env.local
.env.production
Dockerfile
+12 -82
View File
@@ -1,10 +1,9 @@
# Server Configuration
PORT=8080
FRONTEND_PORT=3000
BACKEND_PORT=8080
DB_PORT=5432
DRAGONFLY_PORT=6379
GIN_MODE=debug
READ_TIMEOUT=15s
WRITE_TIMEOUT=15s
IDLE_TIMEOUT=60s
SHUTDOWN_TIMEOUT=30s
# Database Configuration
DB_TYPE=postgres
@@ -15,20 +14,14 @@ DB_PASSWORD=your_password_here
DB_NAME=trackeep
DB_SSL_MODE=disable
# Docker Compose Database (used by docker-compose.yml)
POSTGRES_DB=trackeep
POSTGRES_USER=trackeep
POSTGRES_PASSWORD=your_secure_password_here
# DragonflyDB Configuration
DRAGONFLY_ADDR=dragonfly:6379
DRAGONFLY_PASSWORD=your_dragonfly_password_here
# JWT Configuration
# JWT_SECRET is auto-generated on startup and stored in jwt_secret.key
# You can override by setting JWT_SECRET environment variable if needed
# JWT Configuration (also used for encryption)
JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
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
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=10485760
@@ -36,77 +29,14 @@ MAX_FILE_SIZE=10485760
# CORS Configuration
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
VITE_DEMO_MODE=false
# Browser Search API Configuration
BRAVE_API_KEY=your_brave_api_key_here
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
# AI Services Configuration
SEARCH_API_PROVIDER=demo
SEARCH_RESULTS_LIMIT=10
SEARCH_CACHE_TTL=300
SEARCH_RATE_LIMIT=100
# 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 Configuration
AUTO_UPDATE_CHECK=false
UPDATE_CHECK_INTERVAL=24h
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
with:
go-version: '1.24'
cache: true
cache-dependency-path: backend/go.sum
- name: Install backend dependencies
run: |
@@ -91,6 +93,8 @@ jobs:
uses: actions/setup-go@v4
with:
go-version: '1.24'
cache: true
cache-dependency-path: backend/go.sum
- name: Run go vet
run: |
+32 -7
View File
@@ -149,17 +149,42 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Update docker-compose.prod.yml with new version
- name: Update version in all files
run: |
# Update the version in docker-compose.prod.yml for next development
sed -i "s/APP_VERSION=.*/APP_VERSION=${{ needs.extract-version.outputs.version }}/" docker-compose.prod.yml
VERSION="${{ needs.extract-version.outputs.version }}"
echo "🏷️ Updating all version files to $VERSION"
echo "📝 Updated docker-compose.prod.yml with version ${{ needs.extract-version.outputs.version }}"
# Update frontend package.json
if [ -f "frontend/package.json" ]; then
echo "📝 Updating frontend/package.json..."
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" frontend/package.json
echo "✅ Frontend updated to $VERSION"
fi
- name: Commit updated docker-compose.prod.yml
# Update backend go.mod
if [ -f "backend/go.mod" ]; then
echo "📝 Updating backend/go.mod..."
sed -i "s/go [^\"]*\"/go $VERSION/" backend/go.mod
echo "✅ Backend updated to $VERSION"
fi
# Update docker-compose files
if [ -f "docker-compose.yml" ]; then
sed -i "s/APP_VERSION=.*/APP_VERSION=$VERSION/" docker-compose.yml
echo "✅ docker-compose.yml updated"
fi
if [ -f "docker-compose.prod.yml" ]; then
sed -i "s/APP_VERSION=.*/APP_VERSION=$VERSION/" docker-compose.prod.yml
echo "✅ docker-compose.prod.yml updated"
fi
echo "🎉 All version files updated to $VERSION"
- name: Commit updated version files
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docker-compose.prod.yml
git commit -m "chore: Update APP_VERSION to ${{ needs.extract-version.outputs.version }}"
git add .
git commit -m "chore: Update version to ${{ needs.extract-version.outputs.version }}"
git push
+3
View File
@@ -26,6 +26,9 @@ pids
*.pid
*.seed
*.pid.lock
.playwright
.playwright-cli
.desloppify
# Coverage directory used by tools like istanbul
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();
});
});
});
+222 -43
View File
@@ -12,8 +12,12 @@
<p align="center">
<a href="#quick-start">Quick Start</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#screenshots">Screenshots</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#features">Features</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#releases">Releases</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#tech-stack">Tech Stack</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="#documentation">Documentation</a>
@@ -25,6 +29,212 @@
<img src="./scorecard.png" alt="Code Quality Score" width="100%">
</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
I built Trackeep because I was tired of juggling a dozen different apps for my digital life. You know how it is bookmarks in one place, tasks in another, random notes scattered everywhere, and that great article you meant to read somewhere in your browser history.
@@ -186,6 +396,7 @@ DISABLE_CHINESE_AI=true
### Prerequisites
- Docker and Docker Compose
- Git
- GitHub CLI (optional, for creating releases): `sudo apt install gh` or `sudo snap install gh`
### Installation with Docker (Recommended)
@@ -215,40 +426,6 @@ DISABLE_CHINESE_AI=true
- Backend API: http://localhost:8080
- 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
- Email: `demo@trackeep.com`
- Password: `password`
@@ -303,6 +480,7 @@ Comprehensive documentation is available in the `/docs` directory:
- **[User Guide](./docs/USER_GUIDE.md)** Complete user documentation
- **[API Documentation](./docs/API.md)** REST API reference
- **[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:
- **[Development Guide](./docs/DEVELOPMENT.md)** Development setup and guidelines
@@ -392,14 +570,6 @@ GITHUB_CLIENT_ID=your_github_client_id
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
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.
@@ -450,8 +620,17 @@ This project is built with amazing open-source technologies:
- **Frontend**: SolidJS, UnoCSS, Kobalte, TanStack Query
- **Backend**: Go, Gin, GORM, PostgreSQL
- **Mobile**: React Native, React Navigation
- **DevOps**: Docker, GitHub Actions
- **DevOps**: Docker
### Creating Releases
For detailed release creation instructions, see **[Release Guide](./docs/RELEASE_GUIDE.md)**.
The guide covers:
- GitHub CLI workflow (recommended)
- Manual release scripts
- Semantic versioning
- Release notes templates
## A Personal Note
+1 -1
View File
@@ -41,7 +41,7 @@ type AppConfig struct {
func Load() *Config {
return &Config{
Server: ServerConfig{
Port: getEnvWithDefault("PORT", "8080"),
Port: getEnvWithDefault("BACKEND_PORT", getEnvWithDefault("PORT", "8080")),
ReadTimeout: getDurationEnv("READ_TIMEOUT", 15*time.Second),
WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 15*time.Second),
IdleTimeout: getDurationEnv("IDLE_TIMEOUT", 60*time.Second),
+2 -2
View File
@@ -25,9 +25,9 @@ require (
github.com/antchfx/xmlquery v1.5.0 // indirect
github.com/antchfx/xpath v1.3.5 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
+4 -3
View File
@@ -11,13 +11,14 @@ github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwq
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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=
+245
View File
@@ -2,10 +2,15 @@ package handlers
import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/smtp"
"os"
"os/exec"
"runtime"
"strings"
"time"
@@ -771,3 +776,243 @@ func formatTimeAgo(t time.Time) string {
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"`
}
type ReferenceInput struct {
EntityType string `json:"entity_type"`
EntityID uint `json:"entity_id"`
DeepLink string `json:"deep_link"`
}
type CreateMessageRequest struct {
Body string `json:"body"`
Attachments []AttachmentInput `json:"attachments"`
Metadata map[string]interface{} `json:"metadata"`
References []ReferenceInput `json:"references"`
}
type UpdateMessageRequest struct {
@@ -641,8 +648,8 @@ func CreateConversationMessage(c *gin.Context) {
}
trimmedBody := strings.TrimSpace(req.Body)
if trimmedBody == "" && len(req.Attachments) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Message body or attachments are required"})
if trimmedBody == "" && len(req.Attachments) == 0 && len(req.References) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Message body, attachments, or references are required"})
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)
for _, inferred := range inferredAttachments {
if hasAttachment(attachmentRows, inferred.Kind, inferred.URL) {
@@ -719,6 +757,13 @@ func CreateConversationMessage(c *gin.Context) {
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 {
suggestionRows := make([]models.MessageSuggestion, 0, len(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 {
trimmed := strings.TrimSpace(text)
if len(trimmed) <= limit {
+351 -136
View File
@@ -7,7 +7,6 @@ import (
"io"
"net/http"
"net/url"
"os"
"github.com/gin-gonic/gin"
)
@@ -63,78 +62,176 @@ func SearchWeb(c *gin.Context) {
req.Count = 10
}
apiKey := os.Getenv("BRAVE_API_KEY")
if apiKey == "" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"})
// Get user ID from context (authentication is required)
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required for search functionality"})
return
}
// Build Brave Search API request
baseURL := "https://api.search.brave.com/res/v1/web/search"
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)
// Get user's search settings from database
searchSettings, err := GetSearchSettingsForAPI(userID.(int))
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)})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get search settings"})
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
}
// Check if user has search API key configured
if searchSettings.SearchAPIProvider == "brave" {
apiKey := searchSettings.BraveAPIKey
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
resultsRaw := braveResp.Web.Results
if len(resultsRaw) == 0 {
resultsRaw = braveResp.Mixed.Results
}
// Build Brave Search API request
baseURL := "https://api.search.brave.com/res/v1/web/search"
q := url.Values{}
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))
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)
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)
results = append(results, BraveSearchResult{
Title: title,
URL: urlStr,
Description: desc,
PublishedDate: pageAge,
Language: lang,
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),
})
return
}
c.JSON(http.StatusOK, gin.H{
"results": results,
"query": gin.H{
"original": braveResp.Query.Original,
"display": braveResp.Query.Display,
},
"count": len(results),
})
// Use the configured provider
if searchSettings.SearchAPIProvider == "brave" {
apiKey := searchSettings.BraveAPIKey
if apiKey == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
return
}
// 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) {
fmt.Printf("DEBUG: SearchNews function called\n")
var req struct {
@@ -151,97 +248,215 @@ func SearchNews(c *gin.Context) {
req.Count = 10
}
apiKey := os.Getenv("BRAVE_API_KEY")
if apiKey == "" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"})
// Get user ID from context (authentication is required)
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required for search functionality"})
return
}
baseURL := "https://api.search.brave.com/res/v1/news/search"
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)
// Get user's search settings from database
searchSettings, err := GetSearchSettingsForAPI(userID.(int))
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"})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get search settings"})
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)
// Check if user has search API key configured
if searchSettings.SearchAPIProvider == "brave" {
apiKey := searchSettings.BraveAPIKey
if apiKey == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
return
}
results = append(results, BraveSearchResult{
Title: title,
URL: urlStr,
Description: desc,
PublishedDate: pubDate,
Language: lang,
baseURL := "https://api.search.brave.com/res/v1/news/search"
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 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
display := braveResp.Query.Display
if original == "" {
original = req.Query
}
if display == "" {
display = req.Query
}
// Use the configured provider
if searchSettings.SearchAPIProvider == "brave" {
apiKey := searchSettings.BraveAPIKey
if apiKey == "" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"})
return
}
c.JSON(http.StatusOK, gin.H{
"results": results,
"query": gin.H{
"original": original,
"display": display,
},
"count": len(results),
})
baseURL := "https://api.search.brave.com/res/v1/news/search"
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 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
+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/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"image/png"
@@ -60,9 +61,13 @@ type TOTPLoginRequest struct {
// encrypt encrypts text using AES-GCM
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 {
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)
@@ -86,9 +91,13 @@ func encrypt(plaintext string) (string, error) {
// decrypt decrypts text using AES-GCM
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 {
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)
+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 -10
View File
@@ -4,6 +4,7 @@ import (
"archive/zip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
@@ -66,18 +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
func CheckForUpdates(c *gin.Context) {
updateMutex.Lock()
defer updateMutex.Unlock()
// Get current version from environment or default
currentVersion := os.Getenv("APP_VERSION")
if currentVersion == "" {
currentVersion = "1.0.0"
}
// Get current version from frontend/package.json
currentVersion := getCurrentVersion()
log.Printf("Checking for updates using Docker registry (current version: %s)", currentVersion)
log.Printf("Checking for updates using GitHub releases (current version: %s)", currentVersion)
// Check for updates using Docker registry
updateInfo, updateAvailable, err := checkForUpdatesWithDocker(currentVersion)
@@ -94,14 +125,20 @@ func CheckForUpdates(c *gin.Context) {
currentUpdate = updateInfo
updateProgress.Available = true
} else {
currentUpdate = nil
// Still preserve updateInfo for displaying latest version, but mark as no update available
currentUpdate = updateInfo
updateProgress.Available = false
}
latestVersion := ""
if updateInfo != nil {
latestVersion = updateInfo.Version
}
c.JSON(http.StatusOK, gin.H{
"updateAvailable": updateAvailable,
"currentVersion": currentVersion,
"latestVersion": updateInfo.Version,
"latestVersion": latestVersion,
"updateInfo": currentUpdate,
})
}
@@ -152,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) {
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)
backendImage := "ghcr.io/dvorinka/trackeep/backend:latest"
frontendImage := "ghcr.io/dvorinka/trackeep/frontend:latest"
@@ -333,7 +504,7 @@ func updateWithDockerCompose() error {
// Check if production docker-compose file exists
composeFile := "docker-compose.prod.yml"
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)
+116 -13
View File
@@ -7,8 +7,10 @@ import (
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/joho/godotenv"
"github.com/trackeep/backend/config"
"github.com/trackeep/backend/handlers"
@@ -22,23 +24,67 @@ func IsDemoMode() bool {
}
func initializeSecuritySecrets() error {
jwtSecret, err := utils.GetOrCreateJWTSecret()
if err != nil {
return err
// Only set JWT_SECRET if not already provided in environment
if os.Getenv("JWT_SECRET") == "" {
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()
if err != nil {
return err
// Only set ENCRYPTION_KEY if not already provided in environment
if os.Getenv("ENCRYPTION_KEY") == "" {
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
}
// 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() {
os.Setenv("APP_VERSION", "1.0.0")
@@ -75,10 +121,28 @@ func main() {
log.Fatal("Failed to initialize security secrets:", err)
}
// Initialize session store
middleware.InitSessionStore()
// Initialize DragonflyDB
dragonflyClient := initializeDragonflyDB()
// Initialize session store with DragonflyDB
middleware.InitSessionStore(dragonflyClient)
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
// go func() {
// SeedData()
@@ -95,7 +159,9 @@ func main() {
// Middleware
r.Use(gin.Logger())
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.InputValidationMiddleware())
@@ -125,10 +191,17 @@ func main() {
// Serve static files (frontend)
r.Static("/assets", "../frontend/dist/assets")
r.StaticFile("/", "../frontend/dist/index.html")
// Serve browser extension download
r.GET("/browser-extension", handlers.DownloadBrowserExtension)
r.NoRoute(func(c *gin.Context) {
c.File("../frontend/dist/index.html")
})
// Version endpoint
r.GET("/api/version", handlers.GetVersionHandler)
// Initialize handlers
memberHandler := handlers.NewMemberHandler(config.GetDB())
timeEntryHandler := handlers.NewTimeEntryHandler(config.GetDB())
@@ -203,11 +276,23 @@ func main() {
authProtected.GET("/ai/settings", handlers.GetAISettings)
authProtected.PUT("/ai/settings", handlers.UpdateAISettings)
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
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 := v1.Group("/dashboard")
dashboard.Use(handlers.AuthMiddleware())
@@ -721,6 +806,24 @@ func main() {
performance.POST("/optimize", performanceHandler.OptimizeDatabase)
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{
+88 -6
View File
@@ -1,12 +1,15 @@
package middleware
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/trackeep/backend/models"
)
@@ -34,13 +37,15 @@ type SessionStore interface {
// RedisSessionStore implements SessionStore using Redis (or fallback to memory)
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
func NewSessionStore() SessionStore {
func NewSessionStore(redisClient *redis.Client) SessionStore {
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 {
sessionData.CreatedAt = 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
return nil
}
// GetSession retrieves a session by ID
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 {
// Update last active time
session.LastActive = time.Now()
return session, nil
}
return nil, fmt.Errorf("session not found")
}
// UpdateSession updates an existing session
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 {
sessionData.LastActive = time.Now()
r.sessions[sessionID] = sessionData
return nil
}
return fmt.Errorf("session not found")
}
// DeleteSession removes a session
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)
return nil
}
@@ -93,8 +175,8 @@ func (r *RedisSessionStore) CleanupExpiredSessions() error {
var sessionStore SessionStore
// InitSessionStore initializes the session store
func InitSessionStore() {
sessionStore = NewSessionStore()
func InitSessionStore(redisClient *redis.Client) {
sessionStore = NewSessionStore(redisClient)
// Start cleanup goroutine
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{},
&AITaskSuggestion{},
&UserAISettings{},
&UserSearchSettings{},
&UserUpdateSettings{},
&AITagSuggestion{},
&AIContentGeneration{},
&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"`
// Privacy Settings
ProfileVisibility string `json:"profile_visibility" gorm:"default:public"` // public, private, friends
ShowEmail bool `json:"show_email" gorm:"default:false"`
ShowActivity bool `json:"show_activity" gorm:"default:true"`
AllowMessages bool `json:"allow_messages" gorm:"default:true"`
ProfileVisibility string `json:"profile_visibility" gorm:"default:private"` // public, private, friends
EmailNotifications bool `json:"email_notifications" gorm:"default:true"`
PushNotifications bool `json:"push_notifications" 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
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,
"name": "Trackeep Saver",
"version": "0.1.0",
"description": "Save the current page or a file to your Trackeep account as a bookmark or upload.",
"version": "0.2.0",
"description": "Smart content detection and quick saving for Trackeep with auto-tagging and recommendations.",
"action": {
"default_popup": "popup.html",
"default_title": "Save to Trackeep"
@@ -15,11 +15,21 @@
"storage",
"tabs",
"activeTab",
"contextMenus"
"contextMenus",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"commands": {
"quick-save": {
"suggested_key": {
"default": "Ctrl+Shift+S",
"mac": "Command+Shift+S"
},
"description": "Quick save current page to Trackeep"
}
},
"icons": {
"16": "icons/icon16.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';
}

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