Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3a835caa2 | |||
| dee7011192 | |||
| ebd4ba649d | |||
| 9a580c77d2 | |||
| fc913b5641 | |||
| 874efd5452 | |||
| 1e8bf270a1 | |||
| d82e52ad98 | |||
| 083373a24f | |||
| 446bc7acfb | |||
| 90f0b90cc7 | |||
| ecd31f4e3b | |||
| 9c17f80d5d | |||
| 3b8e14c6b8 |
@@ -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
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ node_modules
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
Dockerfile
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,8 +12,12 @@
|
||||
<p align="center">
|
||||
<a href="#quick-start">Quick Start</a>
|
||||
<span> • </span>
|
||||
<a href="#screenshots">Screenshots</a>
|
||||
<span> • </span>
|
||||
<a href="#features">Features</a>
|
||||
<span> • </span>
|
||||
<a href="#releases">Releases</a>
|
||||
<span> • </span>
|
||||
<a href="#tech-stack">Tech Stack</a>
|
||||
<span> • </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
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -49,6 +49,8 @@ func AutoMigrate() {
|
||||
&AISummary{},
|
||||
&AITaskSuggestion{},
|
||||
&UserAISettings{},
|
||||
&UserSearchSettings{},
|
||||
&UserUpdateSettings{},
|
||||
&AITagSuggestion{},
|
||||
&AIContentGeneration{},
|
||||
&AICodeReview{},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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, " ", " ")
|
||||
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))
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
}
|
||||