Compare commits
9 Commits
446bc7acfb
..
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f3a835caa2 | |||
| dee7011192 | |||
| ebd4ba649d | |||
| 9a580c77d2 | |||
| fc913b5641 | |||
| 874efd5452 | |||
| 1e8bf270a1 | |||
| d82e52ad98 | |||
| 083373a24f |
@@ -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
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
README.md
|
README.md
|
||||||
.env
|
|
||||||
.env.local
|
.env.local
|
||||||
.env.production
|
.env.production
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# Server Configuration
|
# Server Configuration
|
||||||
PORT=8080
|
FRONTEND_PORT=3000
|
||||||
|
BACKEND_PORT=8080
|
||||||
|
DB_PORT=5432
|
||||||
|
DRAGONFLY_PORT=6379
|
||||||
GIN_MODE=debug
|
GIN_MODE=debug
|
||||||
READ_TIMEOUT=15s
|
|
||||||
WRITE_TIMEOUT=15s
|
|
||||||
IDLE_TIMEOUT=60s
|
|
||||||
SHUTDOWN_TIMEOUT=30s
|
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
DB_TYPE=postgres
|
DB_TYPE=postgres
|
||||||
@@ -15,20 +14,14 @@ DB_PASSWORD=your_password_here
|
|||||||
DB_NAME=trackeep
|
DB_NAME=trackeep
|
||||||
DB_SSL_MODE=disable
|
DB_SSL_MODE=disable
|
||||||
|
|
||||||
# Docker Compose Database (used by docker-compose.yml)
|
# DragonflyDB Configuration
|
||||||
POSTGRES_DB=trackeep
|
DRAGONFLY_ADDR=dragonfly:6379
|
||||||
POSTGRES_USER=trackeep
|
DRAGONFLY_PASSWORD=your_dragonfly_password_here
|
||||||
POSTGRES_PASSWORD=your_secure_password_here
|
|
||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration (also used for encryption)
|
||||||
# JWT_SECRET is auto-generated on startup and stored in jwt_secret.key
|
JWT_SECRET=your_jwt_secret_here_64_hex_characters_long_exactly
|
||||||
# You can override by setting JWT_SECRET environment variable if needed
|
|
||||||
JWT_EXPIRES_IN=24h
|
JWT_EXPIRES_IN=24h
|
||||||
|
|
||||||
# Encryption Configuration
|
|
||||||
# ENCRYPTION_KEY is auto-generated on startup and stored in encryption.key
|
|
||||||
# You can override by setting ENCRYPTION_KEY environment variable if needed
|
|
||||||
|
|
||||||
# File Upload Configuration
|
# File Upload Configuration
|
||||||
UPLOAD_DIR=./uploads
|
UPLOAD_DIR=./uploads
|
||||||
MAX_FILE_SIZE=10485760
|
MAX_FILE_SIZE=10485760
|
||||||
@@ -36,77 +29,14 @@ MAX_FILE_SIZE=10485760
|
|||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
CORS_ALLOWED_ORIGINS=*
|
CORS_ALLOWED_ORIGINS=*
|
||||||
|
|
||||||
# AI Services Configuration
|
|
||||||
LONGCAT_ON=false
|
|
||||||
LONGCAT_API_KEY=your_longcat_api_key_here
|
|
||||||
LONGCAT_BASE_URL=https://api.longcat.chat
|
|
||||||
LONGCAT_OPENAI_ENDPOINT=https://api.longcat.chat/openai
|
|
||||||
LONGCAT_ANTHROPIC_ENDPOINT=https://api.longcat.chat/anthropic
|
|
||||||
LONGCAT_MODEL=LongCat-Flash-Chat
|
|
||||||
LONGCAT_MODEL_THINKING=LongCat-Flash-Thinking
|
|
||||||
LONGCAT_FORMAT=openai
|
|
||||||
|
|
||||||
# Mistral AI Configuration
|
|
||||||
MISTRAL_ON=false
|
|
||||||
MISTRAL_API_KEY=your_mistral_api_key_here
|
|
||||||
MISTRAL_MODEL=mistral-small-latest
|
|
||||||
MISTRAL_MODEL_THINKING=mistral-large-latest
|
|
||||||
|
|
||||||
# Grok AI Configuration
|
|
||||||
GROK_ON=false
|
|
||||||
GROK_API_KEY=your_grok_api_key_here
|
|
||||||
GROK_BASE_URL=https://api.x.ai/v1
|
|
||||||
GROK_MODEL=grok-4-1-fast-non-reasoning-latest
|
|
||||||
GROK_MODEL_THINKING=grok-4-1-fast-reasoning-latest
|
|
||||||
|
|
||||||
# DeepSeek Configuration
|
|
||||||
DEEPSEEK_ON=false
|
|
||||||
DEEPSEEK_API_KEY=your_deepseek_api_key_here
|
|
||||||
DEEPSEEK_BASE_URL=https://api.deepseek.com
|
|
||||||
DEEPSEEK_MODEL=deepseek-chat
|
|
||||||
DEEPSEEK_MODEL_THINKING=deepseek-reasoner
|
|
||||||
|
|
||||||
# Ollama Configuration
|
|
||||||
OLLAMA_ON=false
|
|
||||||
OLLAMA_BASE_URL=http://localhost:11434
|
|
||||||
OLLAMA_MODEL=llama3.1
|
|
||||||
OLLAMA_MODEL_THINKING=llama3.1
|
|
||||||
|
|
||||||
# OpenRouter Configuration
|
|
||||||
OPENROUTER_ON=false
|
|
||||||
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
|
||||||
OPENROUTER_BASE_URL=https://openrouter.ai/api
|
|
||||||
OPENROUTER_MODEL=openrouter/auto
|
|
||||||
OPENROUTER_MODEL_THINKING=openrouter/auto
|
|
||||||
|
|
||||||
# Demo Mode Configuration
|
# Demo Mode Configuration
|
||||||
VITE_DEMO_MODE=false
|
VITE_DEMO_MODE=false
|
||||||
|
|
||||||
# Browser Search API Configuration
|
# AI Services Configuration
|
||||||
BRAVE_API_KEY=your_brave_api_key_here
|
SEARCH_API_PROVIDER=demo
|
||||||
BRAVE_SEARCH_BASE_URL=https://api.search.brave.com/res/v1/web/search
|
|
||||||
SERPER_API_KEY=your_serper_api_key_here
|
|
||||||
SERPER_BASE_URL=https://google.serper.dev/search
|
|
||||||
SEARCH_API_PROVIDER=brave # Options: brave, serper, demo
|
|
||||||
|
|
||||||
# Search Configuration
|
|
||||||
SEARCH_RESULTS_LIMIT=10
|
SEARCH_RESULTS_LIMIT=10
|
||||||
SEARCH_CACHE_TTL=300
|
|
||||||
SEARCH_RATE_LIMIT=100
|
|
||||||
|
|
||||||
# Update Configuration
|
# Auto Update Configuration
|
||||||
# Application version (used for update checking)
|
|
||||||
APP_VERSION=1.0.0
|
|
||||||
|
|
||||||
# OAuth service configuration (REQUIRED)
|
|
||||||
# The OAuth service must be running for updates to work
|
|
||||||
OAUTH_SERVICE_URL=http://localhost:9090
|
|
||||||
JWT_SECRET=your-jwt-secret-key
|
|
||||||
|
|
||||||
# Update settings
|
|
||||||
AUTO_UPDATE_CHECK=false
|
AUTO_UPDATE_CHECK=false
|
||||||
UPDATE_CHECK_INTERVAL=24h
|
UPDATE_CHECK_INTERVAL=24h
|
||||||
PRERELEASE_UPDATES=false
|
PRERELEASE_UPDATES=false
|
||||||
|
|
||||||
# Note: No GitHub token configuration needed
|
|
||||||
# Updates use the OAuth service which handles GitHub authentication automatically
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ jobs:
|
|||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.24'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Install backend dependencies
|
- name: Install backend dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -91,6 +93,8 @@ jobs:
|
|||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.24'
|
||||||
|
cache: true
|
||||||
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Run go vet
|
- name: Run go vet
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ pids
|
|||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
.playwright
|
||||||
|
.playwright-cli
|
||||||
|
.desloppify
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage/
|
coverage/
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
- generic [ref=e26]:
|
|
||||||
- generic [ref=e27]:
|
|
||||||
- img "Trackeep Logo" [ref=e29]
|
|
||||||
- heading "Trackeep" [level=1] [ref=e30]
|
|
||||||
- paragraph [ref=e31]: Welcome back
|
|
||||||
- generic [ref=e32]:
|
|
||||||
- generic [ref=e35]: Registration Disabled
|
|
||||||
- paragraph [ref=e36]: Accounts can only be created by the administrator. Please contact your admin to get an account.
|
|
||||||
- generic [ref=e37]:
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]: Email
|
|
||||||
- textbox "Email" [ref=e40]:
|
|
||||||
- /placeholder: your@email.com
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]: Password
|
|
||||||
- textbox "Password" [ref=e43]:
|
|
||||||
- /placeholder: ••••••••
|
|
||||||
- button "Sign In" [ref=e44] [cursor=pointer]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
- generic [ref=e26]:
|
|
||||||
- generic [ref=e27]:
|
|
||||||
- img "Trackeep Logo" [ref=e29]
|
|
||||||
- heading "Trackeep" [level=1] [ref=e30]
|
|
||||||
- paragraph [ref=e31]: Welcome back
|
|
||||||
- generic [ref=e32]:
|
|
||||||
- generic [ref=e35]: Registration Disabled
|
|
||||||
- paragraph [ref=e36]: Accounts can only be created by the administrator. Please contact your admin to get an account.
|
|
||||||
- generic [ref=e37]:
|
|
||||||
- generic [ref=e45]: Invalid credentials
|
|
||||||
- generic [ref=e38]:
|
|
||||||
- generic [ref=e39]: Email
|
|
||||||
- textbox "Email" [ref=e40]:
|
|
||||||
- /placeholder: your@email.com
|
|
||||||
- text: demo@trackeep.com
|
|
||||||
- generic [ref=e41]:
|
|
||||||
- generic [ref=e42]: Password
|
|
||||||
- textbox "Password" [ref=e43]:
|
|
||||||
- /placeholder: ••••••••
|
|
||||||
- text: password
|
|
||||||
- button "Sign In" [ref=e44] [cursor=pointer]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
- paragraph [ref=e6]: Checking authentication...
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- img "Trackeep Logo" [ref=e7]
|
|
||||||
- heading "Trackeep" [level=1] [ref=e8]
|
|
||||||
- paragraph [ref=e9]: Welcome back
|
|
||||||
- generic [ref=e10]:
|
|
||||||
- generic [ref=e13]: Registration Disabled
|
|
||||||
- paragraph [ref=e14]: Accounts can only be created by the administrator. Please contact your admin to get an account.
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- generic [ref=e16]:
|
|
||||||
- generic [ref=e17]: Email
|
|
||||||
- textbox "Email" [ref=e18]:
|
|
||||||
- /placeholder: your@email.com
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- generic [ref=e20]: Password
|
|
||||||
- textbox "Password" [ref=e21]:
|
|
||||||
- /placeholder: ••••••••
|
|
||||||
- button "Sign In" [ref=e22] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- img "Trackeep Logo" [ref=e7]
|
|
||||||
- heading "Trackeep" [level=1] [ref=e8]
|
|
||||||
- paragraph [ref=e9]: Welcome back
|
|
||||||
- generic [ref=e10]:
|
|
||||||
- generic [ref=e13]: Registration Disabled
|
|
||||||
- paragraph [ref=e14]: Accounts can only be created by the administrator. Please contact your admin to get an account.
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- generic [ref=e16]:
|
|
||||||
- generic [ref=e17]: Email
|
|
||||||
- textbox "Email" [ref=e18]:
|
|
||||||
- /placeholder: your@email.com
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- generic [ref=e20]: Password
|
|
||||||
- textbox "Password" [ref=e21]:
|
|
||||||
- /placeholder: ••••••••
|
|
||||||
- button "Sign In" [ref=e22] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:21 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:21 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:22 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:22 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
- generic [ref=e279]:
|
|
||||||
- generic [ref=e280]:
|
|
||||||
- heading "Create Workspace" [level=3] [ref=e281]
|
|
||||||
- paragraph [ref=e282]: Add a new workspace for your team or projects.
|
|
||||||
- generic [ref=e283]:
|
|
||||||
- generic [ref=e284]:
|
|
||||||
- text: Name
|
|
||||||
- textbox "Workspace name" [ref=e285]
|
|
||||||
- generic [ref=e286]:
|
|
||||||
- text: Description
|
|
||||||
- textbox "Description" [ref=e287]:
|
|
||||||
- /placeholder: Optional description
|
|
||||||
- generic [ref=e288]:
|
|
||||||
- generic [ref=e289]:
|
|
||||||
- paragraph [ref=e290]: Public workspace
|
|
||||||
- paragraph [ref=e291]: Allow all members to discover this workspace.
|
|
||||||
- switch [ref=e292] [cursor=pointer]
|
|
||||||
- generic [ref=e293]:
|
|
||||||
- button "Cancel" [ref=e294] [cursor=pointer]
|
|
||||||
- button "Create Workspace" [ref=e295] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:22 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [expanded] [active] [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- listbox [ref=e266]:
|
|
||||||
- option "Trackeep Workspace" [ref=e267] [cursor=pointer]:
|
|
||||||
- img [ref=e268]
|
|
||||||
- generic [ref=e271]: Trackeep Workspace
|
|
||||||
- button "Create Workspace" [ref=e274] [cursor=pointer]:
|
|
||||||
- img [ref=e275]
|
|
||||||
- generic [ref=e276]: Create Workspace
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:22 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [expanded] [active] [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- listbox [ref=e266]:
|
|
||||||
- option "Trackeep Workspace" [ref=e267] [cursor=pointer]:
|
|
||||||
- img [ref=e268]
|
|
||||||
- generic [ref=e271]: Trackeep Workspace
|
|
||||||
- button "Create Workspace" [ref=e274] [cursor=pointer]:
|
|
||||||
- img [ref=e275]
|
|
||||||
- generic [ref=e276]: Create Workspace
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:22 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:23 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [expanded] [active] [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- listbox [ref=e266]:
|
|
||||||
- option "Trackeep Workspace" [ref=e267] [cursor=pointer]:
|
|
||||||
- img [ref=e268]
|
|
||||||
- generic [ref=e271]: Trackeep Workspace
|
|
||||||
- button "Create Workspace" [ref=e274] [cursor=pointer]:
|
|
||||||
- img [ref=e275]
|
|
||||||
- generic [ref=e276]: Create Workspace
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:23 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:23 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
- generic [ref=e279]:
|
|
||||||
- generic [ref=e280]:
|
|
||||||
- heading "Create Workspace" [level=3] [ref=e281]
|
|
||||||
- paragraph [ref=e282]: Add a new workspace for your team or projects.
|
|
||||||
- generic [ref=e283]:
|
|
||||||
- generic [ref=e284]:
|
|
||||||
- text: Name
|
|
||||||
- textbox "Workspace name" [ref=e285]
|
|
||||||
- generic [ref=e286]:
|
|
||||||
- text: Description
|
|
||||||
- textbox "Description" [ref=e287]:
|
|
||||||
- /placeholder: Optional description
|
|
||||||
- generic [ref=e288]:
|
|
||||||
- generic [ref=e289]:
|
|
||||||
- paragraph [ref=e290]: Public workspace
|
|
||||||
- paragraph [ref=e291]: Allow all members to discover this workspace.
|
|
||||||
- switch [ref=e292] [cursor=pointer]
|
|
||||||
- generic [ref=e293]:
|
|
||||||
- button "Cancel" [ref=e294] [cursor=pointer]
|
|
||||||
- button "Create Workspace" [ref=e295] [cursor=pointer]
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:23 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
- generic [ref=e279]:
|
|
||||||
- generic [ref=e280]:
|
|
||||||
- heading "Create Workspace" [level=3] [ref=e281]
|
|
||||||
- paragraph [ref=e282]: Add a new workspace for your team or projects.
|
|
||||||
- generic [ref=e283]:
|
|
||||||
- generic [ref=e284]:
|
|
||||||
- text: Name
|
|
||||||
- textbox "Workspace name" [ref=e285]
|
|
||||||
- generic [ref=e286]:
|
|
||||||
- text: Description
|
|
||||||
- textbox "Description" [ref=e287]:
|
|
||||||
- /placeholder: Optional description
|
|
||||||
- generic [ref=e288]:
|
|
||||||
- generic [ref=e289]:
|
|
||||||
- paragraph [ref=e290]: Public workspace
|
|
||||||
- paragraph [ref=e291]: Allow all members to discover this workspace.
|
|
||||||
- switch [ref=e292] [cursor=pointer]
|
|
||||||
- generic [ref=e293]:
|
|
||||||
- button "Cancel" [ref=e294] [cursor=pointer]
|
|
||||||
- button "Create Workspace" [ref=e295] [cursor=pointer]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:23 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- generic [ref=e178]:
|
|
||||||
- button "AU" [active] [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- generic [ref=e267]:
|
|
||||||
- generic [ref=e269]:
|
|
||||||
- generic [ref=e270]: AU
|
|
||||||
- generic [ref=e271]:
|
|
||||||
- paragraph [ref=e272]: Admin User
|
|
||||||
- paragraph [ref=e273]: admin@trackeep.com
|
|
||||||
- generic [ref=e275]:
|
|
||||||
- generic [ref=e276]:
|
|
||||||
- paragraph [ref=e277]: "0"
|
|
||||||
- paragraph [ref=e278]: Bookmarks
|
|
||||||
- generic [ref=e279]:
|
|
||||||
- paragraph [ref=e280]: "0"
|
|
||||||
- paragraph [ref=e281]: Tasks
|
|
||||||
- button "Profile" [ref=e282] [cursor=pointer]:
|
|
||||||
- img [ref=e283]
|
|
||||||
- text: Profile
|
|
||||||
- button "Statistics" [ref=e286] [cursor=pointer]:
|
|
||||||
- img [ref=e287]
|
|
||||||
- text: Statistics
|
|
||||||
- button "Settings" [ref=e289] [cursor=pointer]:
|
|
||||||
- img [ref=e290]
|
|
||||||
- text: Settings
|
|
||||||
- button "Logout" [ref=e294] [cursor=pointer]:
|
|
||||||
- img [ref=e295]
|
|
||||||
- text: Logout
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:23 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- generic [ref=e178]:
|
|
||||||
- button "AU" [active] [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- generic [ref=e267]:
|
|
||||||
- generic [ref=e269]:
|
|
||||||
- generic [ref=e270]: AU
|
|
||||||
- generic [ref=e271]:
|
|
||||||
- paragraph [ref=e272]: Admin User
|
|
||||||
- paragraph [ref=e273]: admin@trackeep.com
|
|
||||||
- generic [ref=e275]:
|
|
||||||
- generic [ref=e276]:
|
|
||||||
- paragraph [ref=e277]: "0"
|
|
||||||
- paragraph [ref=e278]: Bookmarks
|
|
||||||
- generic [ref=e279]:
|
|
||||||
- paragraph [ref=e280]: "0"
|
|
||||||
- paragraph [ref=e281]: Tasks
|
|
||||||
- button "Profile" [ref=e282] [cursor=pointer]:
|
|
||||||
- img [ref=e283]
|
|
||||||
- text: Profile
|
|
||||||
- button "Statistics" [ref=e286] [cursor=pointer]:
|
|
||||||
- img [ref=e287]
|
|
||||||
- text: Statistics
|
|
||||||
- button "Settings" [ref=e289] [cursor=pointer]:
|
|
||||||
- img [ref=e290]
|
|
||||||
- text: Settings
|
|
||||||
- button "Logout" [ref=e294] [cursor=pointer]:
|
|
||||||
- img [ref=e295]
|
|
||||||
- text: Logout
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Tasks" [level=1] [ref=e188]
|
|
||||||
- button "Add Task" [ref=e189] [cursor=pointer]
|
|
||||||
- generic [ref=e190]:
|
|
||||||
- generic [ref=e191]:
|
|
||||||
- paragraph [ref=e192]: "0"
|
|
||||||
- paragraph [ref=e193]: Total Tasks
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- paragraph [ref=e195]: "0"
|
|
||||||
- paragraph [ref=e196]: Active
|
|
||||||
- generic [ref=e197]:
|
|
||||||
- paragraph [ref=e198]: "0"
|
|
||||||
- paragraph [ref=e199]: Completed
|
|
||||||
- generic [ref=e201]:
|
|
||||||
- textbox "Search tasks..." [ref=e202]
|
|
||||||
- combobox [ref=e203]:
|
|
||||||
- option "All Priorities" [selected]
|
|
||||||
- option "high"
|
|
||||||
- option "medium"
|
|
||||||
- option "low"
|
|
||||||
- generic [ref=e204]:
|
|
||||||
- button "all" [ref=e205] [cursor=pointer]
|
|
||||||
- button "active" [ref=e206] [cursor=pointer]
|
|
||||||
- button "completed" [ref=e207] [cursor=pointer]
|
|
||||||
- paragraph [ref=e210]: No tasks yet. Add your first task!
|
|
||||||
- button "AI Assistant" [ref=e211] [cursor=pointer]:
|
|
||||||
- img [ref=e212]
|
|
||||||
- generic [ref=e219]:
|
|
||||||
- generic [ref=e220]:
|
|
||||||
- generic [ref=e221]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e230]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e231]
|
|
||||||
- paragraph [ref=e232]: Always here to help
|
|
||||||
- button [ref=e234] [cursor=pointer]:
|
|
||||||
- img [ref=e235]
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- img [ref=e241]
|
|
||||||
- generic [ref=e248]:
|
|
||||||
- paragraph [ref=e249]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e250]: 04:23 PM
|
|
||||||
- generic [ref=e251]:
|
|
||||||
- generic [ref=e252]:
|
|
||||||
- textbox "Type your message..." [ref=e253]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e255]:
|
|
||||||
- button "longcat icon LongCat" [ref=e257] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e258]
|
|
||||||
- generic [ref=e259]: LongCat
|
|
||||||
- img [ref=e260]
|
|
||||||
- generic [ref=e262]:
|
|
||||||
- generic [ref=e263]: longcat
|
|
||||||
- link "AI settings" [ref=e264] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Add New Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Add Task" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Edit Task" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- textbox "Task title *"
|
|
||||||
- textbox "Description (optional)"
|
|
||||||
- generic:
|
|
||||||
- combobox:
|
|
||||||
- option "Low Priority"
|
|
||||||
- option "Medium Priority" [selected]
|
|
||||||
- option "High Priority"
|
|
||||||
- generic:
|
|
||||||
- button "Due date (optional)":
|
|
||||||
- generic: Due date (optional)
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Save Changes" [disabled]
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:24 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:24 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:24 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:24 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:24 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:25 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:25 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:25 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
- generic [ref=e5]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- heading "Authentication Required" [level=1] [ref=e11]
|
|
||||||
- paragraph [ref=e12]: Please sign in to access Trackeep
|
|
||||||
- generic [ref=e13]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e19]:
|
|
||||||
- heading "Authentication Required" [level=3] [ref=e20]
|
|
||||||
- paragraph [ref=e21]: You need to be authenticated to access this page. Please sign in or create an account to continue.
|
|
||||||
- generic [ref=e22]:
|
|
||||||
- button "Sign In" [ref=e23] [cursor=pointer]
|
|
||||||
- button "Create Account" [ref=e24] [cursor=pointer]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [active] [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:26 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- generic [ref=e4]:
|
|
||||||
- generic [ref=e7]:
|
|
||||||
- link "Trackeep Logo Trackeep" [ref=e9] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- img "Trackeep Logo" [ref=e10]
|
|
||||||
- generic [ref=e11]: Trackeep
|
|
||||||
- group [ref=e13]:
|
|
||||||
- button "Trackeep Workspace" [ref=e14] [cursor=pointer]:
|
|
||||||
- generic [ref=e15]:
|
|
||||||
- img [ref=e17]
|
|
||||||
- generic [ref=e20]: Trackeep Workspace
|
|
||||||
- img [ref=e22]
|
|
||||||
- navigation [ref=e24]:
|
|
||||||
- link "Home" [ref=e25] [cursor=pointer]:
|
|
||||||
- /url: /app
|
|
||||||
- generic [ref=e26]:
|
|
||||||
- img [ref=e27]
|
|
||||||
- generic [ref=e31]: Home
|
|
||||||
- link "Bookmarks" [ref=e33] [cursor=pointer]:
|
|
||||||
- /url: /app/bookmarks
|
|
||||||
- generic [ref=e34]:
|
|
||||||
- img [ref=e35]
|
|
||||||
- generic [ref=e37]: Bookmarks
|
|
||||||
- link "Tasks" [ref=e39] [cursor=pointer]:
|
|
||||||
- /url: /app/tasks
|
|
||||||
- generic [ref=e40]:
|
|
||||||
- img [ref=e41]
|
|
||||||
- generic [ref=e44]: Tasks
|
|
||||||
- link "Time Tracking" [ref=e46] [cursor=pointer]:
|
|
||||||
- /url: /app/time-tracking
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e51]: Time Tracking
|
|
||||||
- link "Calendar" [ref=e53] [cursor=pointer]:
|
|
||||||
- /url: /app/calendar
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e57]: Calendar
|
|
||||||
- link "Files" [ref=e59] [cursor=pointer]:
|
|
||||||
- /url: /app/files
|
|
||||||
- generic [ref=e60]:
|
|
||||||
- img [ref=e61]
|
|
||||||
- generic [ref=e63]: Files
|
|
||||||
- link "Notes" [ref=e65] [cursor=pointer]:
|
|
||||||
- /url: /app/notes
|
|
||||||
- generic [ref=e66]:
|
|
||||||
- img [ref=e67]
|
|
||||||
- generic [ref=e69]: Notes
|
|
||||||
- link "Messages" [ref=e71] [cursor=pointer]:
|
|
||||||
- /url: /app/messages
|
|
||||||
- generic [ref=e72]:
|
|
||||||
- img [ref=e73]
|
|
||||||
- generic [ref=e75]: Messages
|
|
||||||
- link "YouTube" [ref=e77] [cursor=pointer]:
|
|
||||||
- /url: /app/youtube
|
|
||||||
- generic [ref=e78]:
|
|
||||||
- img [ref=e79]
|
|
||||||
- generic [ref=e82]: YouTube
|
|
||||||
- link "Members" [ref=e84] [cursor=pointer]:
|
|
||||||
- /url: /app/members
|
|
||||||
- generic [ref=e85]:
|
|
||||||
- img [ref=e86]
|
|
||||||
- generic [ref=e91]: Members
|
|
||||||
- link "Learning" [ref=e93] [cursor=pointer]:
|
|
||||||
- /url: /app/learning-paths
|
|
||||||
- generic [ref=e94]:
|
|
||||||
- img [ref=e95]
|
|
||||||
- generic [ref=e98]: Learning
|
|
||||||
- link "Stats" [ref=e100] [cursor=pointer]:
|
|
||||||
- /url: /app/stats
|
|
||||||
- generic [ref=e101]:
|
|
||||||
- img [ref=e102]
|
|
||||||
- generic [ref=e104]: Stats
|
|
||||||
- link "GitHub" [ref=e106] [cursor=pointer]:
|
|
||||||
- /url: /app/github
|
|
||||||
- generic [ref=e107]:
|
|
||||||
- img [ref=e108]
|
|
||||||
- generic [ref=e110]: GitHub
|
|
||||||
- link "AI Assistant" [ref=e112] [cursor=pointer]:
|
|
||||||
- /url: /app/chat
|
|
||||||
- generic [ref=e113]:
|
|
||||||
- img [ref=e114]
|
|
||||||
- generic [ref=e121]: AI Assistant
|
|
||||||
- generic [ref=e124]:
|
|
||||||
- generic [ref=e125]: Version 1.0.0
|
|
||||||
- button "Update Failed" [ref=e126] [cursor=pointer]:
|
|
||||||
- generic [ref=e127]:
|
|
||||||
- img [ref=e128]
|
|
||||||
- generic [ref=e130]: Update Failed
|
|
||||||
- navigation [ref=e132]:
|
|
||||||
- link "Removed stuff" [ref=e133] [cursor=pointer]:
|
|
||||||
- /url: /app/removed-stuff
|
|
||||||
- generic [ref=e134]:
|
|
||||||
- img [ref=e135]
|
|
||||||
- generic [ref=e138]: Removed stuff
|
|
||||||
- link "Settings" [ref=e140] [cursor=pointer]:
|
|
||||||
- /url: /app/settings
|
|
||||||
- generic [ref=e141]:
|
|
||||||
- img [ref=e142]
|
|
||||||
- generic [ref=e145]: Settings
|
|
||||||
- button "Logout" [ref=e147] [cursor=pointer]:
|
|
||||||
- generic [ref=e148]:
|
|
||||||
- img [ref=e149]
|
|
||||||
- generic [ref=e153]: Logout
|
|
||||||
- generic [ref=e155]:
|
|
||||||
- generic [ref=e156]:
|
|
||||||
- generic [ref=e157]:
|
|
||||||
- button [ref=e158] [cursor=pointer]:
|
|
||||||
- img [ref=e159]
|
|
||||||
- button "Quick search" [ref=e160] [cursor=pointer]:
|
|
||||||
- img [ref=e161]
|
|
||||||
- text: Quick search
|
|
||||||
- generic [ref=e164]:
|
|
||||||
- button "Import a document" [ref=e165] [cursor=pointer]:
|
|
||||||
- img [ref=e166]
|
|
||||||
- text: Import a document
|
|
||||||
- button [ref=e170] [cursor=pointer]:
|
|
||||||
- img [ref=e171]
|
|
||||||
- img [ref=e176]
|
|
||||||
- button "AU" [ref=e180] [cursor=pointer]:
|
|
||||||
- generic [ref=e181]: AU
|
|
||||||
- img [ref=e182]
|
|
||||||
- main [ref=e184]:
|
|
||||||
- generic [ref=e186]:
|
|
||||||
- generic [ref=e187]:
|
|
||||||
- heading "Files" [level=1] [ref=e188]
|
|
||||||
- button "Upload File" [active] [ref=e189] [cursor=pointer]:
|
|
||||||
- img [ref=e190]
|
|
||||||
- text: Upload File
|
|
||||||
- generic [ref=e194]:
|
|
||||||
- textbox "Search files..." [ref=e195]
|
|
||||||
- combobox [ref=e196]:
|
|
||||||
- option "All Tags" [selected]
|
|
||||||
- paragraph [ref=e198]: No files uploaded yet. Upload your first file!
|
|
||||||
- button "AI Assistant" [ref=e199] [cursor=pointer]:
|
|
||||||
- img [ref=e200]
|
|
||||||
- generic [ref=e207]:
|
|
||||||
- generic [ref=e208]:
|
|
||||||
- generic [ref=e209]:
|
|
||||||
- img [ref=e211]
|
|
||||||
- generic [ref=e218]:
|
|
||||||
- heading "AI Assistant" [level=3] [ref=e219]
|
|
||||||
- paragraph [ref=e220]: Always here to help
|
|
||||||
- button [ref=e222] [cursor=pointer]:
|
|
||||||
- img [ref=e223]
|
|
||||||
- generic [ref=e227]:
|
|
||||||
- img [ref=e229]
|
|
||||||
- generic [ref=e236]:
|
|
||||||
- paragraph [ref=e237]: Hello! I'm your AI assistant. How can I help you today?
|
|
||||||
- paragraph [ref=e238]: 04:26 PM
|
|
||||||
- generic [ref=e239]:
|
|
||||||
- generic [ref=e240]:
|
|
||||||
- textbox "Type your message..." [ref=e241]
|
|
||||||
- button [disabled]:
|
|
||||||
- img
|
|
||||||
- generic [ref=e243]:
|
|
||||||
- button "longcat icon LongCat" [ref=e245] [cursor=pointer]:
|
|
||||||
- img "longcat icon" [ref=e246]
|
|
||||||
- generic [ref=e247]: LongCat
|
|
||||||
- img [ref=e248]
|
|
||||||
- generic [ref=e250]:
|
|
||||||
- generic [ref=e251]: longcat
|
|
||||||
- link "AI settings" [ref=e252] [cursor=pointer]:
|
|
||||||
- /url: /app/settings#ai
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading [level=3]
|
|
||||||
- generic: Unknown size
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic: Unknown file type
|
|
||||||
- generic:
|
|
||||||
- button "Download":
|
|
||||||
- img
|
|
||||||
- text: Download
|
|
||||||
- button "Open":
|
|
||||||
- img
|
|
||||||
- text: Open
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- heading "Import Documents" [level=3]
|
|
||||||
- button:
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- heading "Drop files here" [level=4]
|
|
||||||
- paragraph: or click to browse
|
|
||||||
- button "Browse Files"
|
|
||||||
- generic:
|
|
||||||
- button "Cancel"
|
|
||||||
- button "Upload 0 Files" [disabled]
|
|
||||||
@@ -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,6 +12,8 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="#quick-start">Quick Start</a>
|
<a href="#quick-start">Quick Start</a>
|
||||||
<span> • </span>
|
<span> • </span>
|
||||||
|
<a href="#screenshots">Screenshots</a>
|
||||||
|
<span> • </span>
|
||||||
<a href="#features">Features</a>
|
<a href="#features">Features</a>
|
||||||
<span> • </span>
|
<span> • </span>
|
||||||
<a href="#releases">Releases</a>
|
<a href="#releases">Releases</a>
|
||||||
@@ -27,6 +29,211 @@
|
|||||||
<img src="./scorecard.png" alt="Code Quality Score" width="100%">
|
<img src="./scorecard.png" alt="Code Quality Score" width="100%">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Production Deployment with Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/dvorinka/trackeep.git
|
||||||
|
cd trackeep
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env file with your configuration
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The `docker-compose.prod.yml` file uses environment variables with sensible defaults:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
trackeep-frontend:
|
||||||
|
image: 'ghcr.io/dvorinka/trackeep/frontend:latest'
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
- '443:443'
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
||||||
|
depends_on:
|
||||||
|
- trackeep-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- trackeep-network
|
||||||
|
trackeep-backend:
|
||||||
|
image: 'ghcr.io/dvorinka/trackeep/backend:latest'
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
|
environment:
|
||||||
|
- PORT=${PORT:-8080}
|
||||||
|
- GIN_MODE=${GIN_MODE:-release}
|
||||||
|
- READ_TIMEOUT=${READ_TIMEOUT:-15s}
|
||||||
|
- WRITE_TIMEOUT=${WRITE_TIMEOUT:-15s}
|
||||||
|
- IDLE_TIMEOUT=${IDLE_TIMEOUT:-60s}
|
||||||
|
- SHUTDOWN_TIMEOUT=${SHUTDOWN_TIMEOUT:-30s}
|
||||||
|
- DB_TYPE=${DB_TYPE:-postgres}
|
||||||
|
- DB_HOST=${DB_HOST:-postgres}
|
||||||
|
- DB_PORT=${DB_PORT:-5432}
|
||||||
|
- DB_USER=${DB_USER:-trackeep}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
- DB_NAME=${DB_NAME:-trackeep}
|
||||||
|
- DB_SSL_MODE=${DB_SSL_MODE:-disable}
|
||||||
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
|
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-24h}
|
||||||
|
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||||
|
- UPLOAD_DIR=${UPLOAD_DIR:-./uploads}
|
||||||
|
- MAX_FILE_SIZE=${MAX_FILE_SIZE:-10485760}
|
||||||
|
- 'CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-*}'
|
||||||
|
- VITE_DEMO_MODE=${VITE_DEMO_MODE:-false}
|
||||||
|
- SEARCH_API_PROVIDER=${SEARCH_API_PROVIDER:-demo}
|
||||||
|
- SEARCH_RESULTS_LIMIT=${SEARCH_RESULTS_LIMIT:-10}
|
||||||
|
- SEARCH_CACHE_TTL=${SEARCH_CACHE_TTL:-300}
|
||||||
|
- SEARCH_RATE_LIMIT=${SEARCH_RATE_LIMIT:-100}
|
||||||
|
- 'OAUTH_SERVICE_URL=${OAUTH_SERVICE_URL:-http://localhost:9090}'
|
||||||
|
- AUTO_UPDATE_CHECK=${AUTO_UPDATE_CHECK:-false}
|
||||||
|
- UPDATE_CHECK_INTERVAL=${UPDATE_CHECK_INTERVAL:-24h}
|
||||||
|
- PRERELEASE_UPDATES=${PRERELEASE_UPDATES:-false}
|
||||||
|
volumes:
|
||||||
|
- './data:/data'
|
||||||
|
- './uploads:/app/uploads'
|
||||||
|
- './logs:/app/logs'
|
||||||
|
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- trackeep-network
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- wget
|
||||||
|
- '--no-verbose'
|
||||||
|
- '--tries=1'
|
||||||
|
- '--spider'
|
||||||
|
- 'http://localhost:8080/health'
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
postgres:
|
||||||
|
image: 'postgres:15-alpine'
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-trackeep}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-trackeep}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- 'postgres_data:/var/lib/postgresql/data'
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- trackeep-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-trackeep} -d ${POSTGRES_DB:-trackeep}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data: null
|
||||||
|
|
||||||
|
networks:
|
||||||
|
trackeep-network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Architecture
|
||||||
|
|
||||||
|
Trackeep production deployment consists of **3 essential services**:
|
||||||
|
|
||||||
|
#### **🎯 Frontend Service**
|
||||||
|
- **Image**: `ghcr.io/dvorinka/trackeep/frontend:latest`
|
||||||
|
- **Ports**: `80:80`, `443:443`
|
||||||
|
- **Purpose**: Web interface and user experience
|
||||||
|
- **Health**: Depends on backend service
|
||||||
|
|
||||||
|
#### **🔧 Backend Service**
|
||||||
|
- **Image**: `ghcr.io/dvorinka/trackeep/backend:latest`
|
||||||
|
- **Ports**: `8080:8080`
|
||||||
|
- **Purpose**: API server and business logic
|
||||||
|
- **Health**: Built-in health check endpoint
|
||||||
|
|
||||||
|
#### **🗄️ Database Service**
|
||||||
|
- **Image**: `postgres:15-alpine`
|
||||||
|
- **Purpose**: Data persistence and storage
|
||||||
|
- **Health**: PostgreSQL readiness check
|
||||||
|
- **Storage**: Persistent volume for data
|
||||||
|
|
||||||
|
### Required Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file from the provided `.env.example` and configure these required variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Database Configuration
|
||||||
|
DB_PASSWORD=your_secure_password
|
||||||
|
POSTGRES_PASSWORD=your_secure_password
|
||||||
|
|
||||||
|
# Security Configuration
|
||||||
|
JWT_SECRET=your_jwt_secret_key
|
||||||
|
ENCRYPTION_KEY=your_32_character_encryption_key
|
||||||
|
```
|
||||||
|
|
||||||
|
### AI Services Configuration
|
||||||
|
|
||||||
|
AI services are now configured **only within the Trackeep application**. No environment variables are needed for AI configuration. Simply:
|
||||||
|
|
||||||
|
1. Start Trackeep with the basic configuration above
|
||||||
|
2. Navigate to Settings → AI Services in the application
|
||||||
|
3. Add your API tokens and configure AI providers in the app interface
|
||||||
|
4. Enable/disable AI services as needed through the app settings
|
||||||
|
|
||||||
|
### Version Management
|
||||||
|
|
||||||
|
Trackeep uses GitHub Docker images with the `:latest` tag. The application version is automatically managed through the Docker image tags and update checking is handled through the OAuth service. No manual version configuration is needed in the environment variables.
|
||||||
|
|
||||||
|
### Version Detection
|
||||||
|
|
||||||
|
The system automatically detects the running version through multiple methods:
|
||||||
|
|
||||||
|
- **Docker Detection**: Identifies container image tags
|
||||||
|
- **Environment Variables**: Uses `TRACKEEP_VERSION` if set
|
||||||
|
- **Version Files**: Reads from `/app/VERSION` or similar
|
||||||
|
- **Git Tags**: Detects version when running from source
|
||||||
|
|
||||||
|
Access version information via the API:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/version
|
||||||
|
```
|
||||||
|
|
||||||
|
All other variables have sensible defaults and can be configured as needed.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
<!-- TODO: Add dashboard screenshot -->
|
||||||
|
<p align="center">
|
||||||
|
<img src="./screenshots/dashboard.png" alt="Dashboard Screenshot" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Bookmarks & Links
|
||||||
|
<!-- TODO: Add bookmarks screenshot -->
|
||||||
|
<p align="center">
|
||||||
|
<img src="./screenshots/bookmarks.png" alt="Bookmarks Screenshot" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Task Management
|
||||||
|
<!-- TODO: Add tasks screenshot -->
|
||||||
|
<p align="center">
|
||||||
|
<img src="./screenshots/tasks.png" alt="Tasks Screenshot" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### File Storage & Media
|
||||||
|
<!-- TODO: Add file storage screenshot -->
|
||||||
|
<p align="center">
|
||||||
|
<img src="./screenshots/files.png" alt="Files Screenshot" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Mobile App
|
||||||
|
<!-- TODO: Add mobile screenshot -->
|
||||||
|
<p align="center">
|
||||||
|
<img src="./screenshots/mobile.png" alt="Mobile App Screenshot" width="400">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
@@ -189,6 +396,7 @@ DISABLE_CHINESE_AI=true
|
|||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Docker and Docker Compose
|
- Docker and Docker Compose
|
||||||
- Git
|
- Git
|
||||||
|
- GitHub CLI (optional, for creating releases): `sudo apt install gh` or `sudo snap install gh`
|
||||||
|
|
||||||
### Installation with Docker (Recommended)
|
### Installation with Docker (Recommended)
|
||||||
|
|
||||||
@@ -218,40 +426,6 @@ DISABLE_CHINESE_AI=true
|
|||||||
- Backend API: http://localhost:8080
|
- Backend API: http://localhost:8080
|
||||||
- Health Check: http://localhost:8080/health
|
- Health Check: http://localhost:8080/health
|
||||||
|
|
||||||
### Docker Updates (Easy Way)
|
|
||||||
|
|
||||||
Trackeep now supports automatic Docker updates! Instead of rebuilding from source, you can pull pre-built images:
|
|
||||||
|
|
||||||
#### **Method 1: Quick Update Script**
|
|
||||||
```bash
|
|
||||||
./update.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Method 2: Using Published Images**
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.published.yml pull
|
|
||||||
docker compose -f docker-compose.published.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### **Method 3: Manual Pull**
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/Dvorinka/trackeep/backend:latest
|
|
||||||
docker pull ghcr.io/Dvorinka/trackeep/frontend:latest
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Available Docker Images
|
|
||||||
|
|
||||||
Pre-built images are automatically published to GitHub Container Registry:
|
|
||||||
- `ghcr.io/Dvorinka/trackeep/backend:latest`
|
|
||||||
- `ghcr.io/Dvorinka/trackeep/frontend:latest`
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- 🚀 **Faster updates** - No need to build from source
|
|
||||||
- 🔄 **Automatic builds** - Images published on every push to main
|
|
||||||
- 📦 **Version control** - Images tagged with commit SHAs and branches
|
|
||||||
- 🛡️ **Stable releases** - Tested images ready for production
|
|
||||||
|
|
||||||
### Demo Login
|
### Demo Login
|
||||||
- Email: `demo@trackeep.com`
|
- Email: `demo@trackeep.com`
|
||||||
- Password: `password`
|
- Password: `password`
|
||||||
@@ -306,6 +480,7 @@ Comprehensive documentation is available in the `/docs` directory:
|
|||||||
- **[User Guide](./docs/USER_GUIDE.md)** – Complete user documentation
|
- **[User Guide](./docs/USER_GUIDE.md)** – Complete user documentation
|
||||||
- **[API Documentation](./docs/API.md)** – REST API reference
|
- **[API Documentation](./docs/API.md)** – REST API reference
|
||||||
- **[AI Assistant Features](./docs/AI_ASSISTANT.md)** – AI-powered features guide
|
- **[AI Assistant Features](./docs/AI_ASSISTANT.md)** – AI-powered features guide
|
||||||
|
- **[Release Guide](./docs/RELEASE_GUIDE.md)** – Creating releases and version management
|
||||||
|
|
||||||
Additional documentation files:
|
Additional documentation files:
|
||||||
- **[Development Guide](./docs/DEVELOPMENT.md)** – Development setup and guidelines
|
- **[Development Guide](./docs/DEVELOPMENT.md)** – Development setup and guidelines
|
||||||
@@ -395,14 +570,6 @@ GITHUB_CLIENT_ID=your_github_client_id
|
|||||||
GITHUB_CLIENT_SECRET=your_github_client_secret
|
GITHUB_CLIENT_SECRET=your_github_client_secret
|
||||||
```
|
```
|
||||||
|
|
||||||
**AI Configuration Notes:**
|
|
||||||
- All AI services are optional – Trackeep works perfectly without any AI
|
|
||||||
- Mix and match services based on your budget and privacy preferences
|
|
||||||
- Chinese AI services (DeepSeek, LongCat) offer great pricing but consider your privacy needs
|
|
||||||
- European option (Mistral) for GDPR-compliant AI processing
|
|
||||||
- Local AI (Ollama) for complete offline privacy
|
|
||||||
- Custom endpoints supported for maximum flexibility
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Building Trackeep as a solo developer has been an incredible journey, but it's always better when we build together! Whether you're fixing a typo, adding a feature, or just sharing ideas – your contribution matters.
|
Building Trackeep as a solo developer has been an incredible journey, but it's always better when we build together! Whether you're fixing a typo, adding a feature, or just sharing ideas – your contribution matters.
|
||||||
@@ -453,106 +620,17 @@ This project is built with amazing open-source technologies:
|
|||||||
- **Frontend**: SolidJS, UnoCSS, Kobalte, TanStack Query
|
- **Frontend**: SolidJS, UnoCSS, Kobalte, TanStack Query
|
||||||
- **Backend**: Go, Gin, GORM, PostgreSQL
|
- **Backend**: Go, Gin, GORM, PostgreSQL
|
||||||
- **Mobile**: React Native, React Navigation
|
- **Mobile**: React Native, React Navigation
|
||||||
- **DevOps**: Docker, GitHub Actions
|
- **DevOps**: Docker
|
||||||
|
|
||||||
|
### Creating Releases
|
||||||
|
|
||||||
## 🚀 Releases & Updates
|
For detailed release creation instructions, see **[Release Guide](./docs/RELEASE_GUIDE.md)**.
|
||||||
|
|
||||||
Trackeep uses automated semantic versioning and Docker-based updates. No manual setup required!
|
The guide covers:
|
||||||
|
- GitHub CLI workflow (recommended)
|
||||||
### 📋 How Updates Work
|
- Manual release scripts
|
||||||
|
- Semantic versioning
|
||||||
Users get updates automatically through the built-in update system:
|
- Release notes templates
|
||||||
- ✅ **Auto-checks** every 24 hours for new versions
|
|
||||||
- ✅ **UI notifications** appear in left navigation when updates available
|
|
||||||
- ✅ **One-click install** pulls latest Docker images and restarts services
|
|
||||||
- ✅ **Zero setup** - just run `docker compose up` and it works
|
|
||||||
|
|
||||||
### 🏷️ Version Management
|
|
||||||
|
|
||||||
Versions are managed automatically through semantic versioning (MAJOR.MINOR.PATCH):
|
|
||||||
|
|
||||||
- **Frontend**: Version from `frontend/package.json`
|
|
||||||
- **Backend**: Version from `backend/go.mod`
|
|
||||||
- **Detection**: Automatic from source code (no env vars needed)
|
|
||||||
|
|
||||||
### 🚀 Creating Releases
|
|
||||||
|
|
||||||
#### Method 1: Automated (Recommended)
|
|
||||||
|
|
||||||
For new features or bug fixes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Commit your changes
|
|
||||||
git commit -m "feat: add new amazing feature"
|
|
||||||
|
|
||||||
# 2. Create version tag and push (triggers automated release)
|
|
||||||
git tag v1.2.7
|
|
||||||
git push origin main v1.2.7
|
|
||||||
```
|
|
||||||
|
|
||||||
**What happens automatically:**
|
|
||||||
1. GitHub Actions detects the version tag
|
|
||||||
2. Updates all version files (`package.json`, `go.mod`, docker-compose files)
|
|
||||||
3. Builds Docker images with proper semantic tags
|
|
||||||
4. Pushes to GitHub Container Registry (`latest` + versioned tags)
|
|
||||||
5. Creates GitHub release with changelog
|
|
||||||
6. Updates `latest` tags to point to new version
|
|
||||||
|
|
||||||
#### Method 2: Manual
|
|
||||||
|
|
||||||
For precise control:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use version update script
|
|
||||||
./scripts/update-version.sh 1.2.7
|
|
||||||
|
|
||||||
# Commit and push
|
|
||||||
git add . && git commit -m "chore: bump version to 1.2.7"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🐳 Docker Images
|
|
||||||
|
|
||||||
Images are automatically built and pushed to GitHub Container Registry:
|
|
||||||
|
|
||||||
- **Registry**: `ghcr.io/dvorinka/trackeep`
|
|
||||||
- **Latest tags**: `backend:latest`, `frontend:latest` (for updates)
|
|
||||||
- **Versioned tags**: `backend:1.2.6`, `frontend:1.2.6` (for rollback)
|
|
||||||
- **Automatic builds**: Triggered by Git tags
|
|
||||||
|
|
||||||
### 📖 Semantic Versioning
|
|
||||||
|
|
||||||
Follow industry standard (MAJOR.MINOR.PATCH):
|
|
||||||
|
|
||||||
```
|
|
||||||
1.2.6 → 1.3.0 (MINOR: new features)
|
|
||||||
1.2.6 → 1.2.7 (PATCH: bug fixes)
|
|
||||||
1.2.6 → 2.0.0 (MAJOR: breaking changes)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🔧 Development Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone and run
|
|
||||||
git clone https://github.com/Dvorinka/Trackeep.git
|
|
||||||
cd Trackeep
|
|
||||||
|
|
||||||
# Start with automatic updates
|
|
||||||
docker compose up
|
|
||||||
|
|
||||||
# System automatically:
|
|
||||||
# - Detects version from source code
|
|
||||||
# - Checks for updates every 24h
|
|
||||||
# - Shows update notifications in UI
|
|
||||||
# - Installs updates with one click
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📚 Documentation
|
|
||||||
|
|
||||||
- **Version workflow**: See [docs/SIMPLIFIED_VERSION_SYSTEM.md](docs/SIMPLIFIED_VERSION_SYSTEM.md)
|
|
||||||
- **API documentation**: See [docs/API.md](docs/API.md)
|
|
||||||
- **Update system**: See [docs/AUTO_UPDATE_GUIDE.md](docs/AUTO_UPDATE_GUIDE.md)
|
|
||||||
|
|
||||||
## A Personal Note
|
## A Personal Note
|
||||||
|
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
# 🎉 Trackeep v1.2.5 Release Complete!
|
|
||||||
|
|
||||||
## ✅ What We Accomplished
|
|
||||||
|
|
||||||
### **🏷️ Proper Semantic Versioning**
|
|
||||||
- ✅ Created version `v1.2.5` following MAJOR.MINOR.PATCH format
|
|
||||||
- ✅ Git tag created: `v1.2.5`
|
|
||||||
- ✅ Version pushed to origin
|
|
||||||
- ✅ Ready for GitHub Actions automated builds
|
|
||||||
|
|
||||||
### **🔄 Complete Update System**
|
|
||||||
- ✅ **No OAuth required** - removed authentication dependency
|
|
||||||
- ✅ **Docker-based** - uses container registry pulls
|
|
||||||
- ✅ **Latest tags** - always gets newest versions
|
|
||||||
- ✅ **Integrated UI** - update notifications in left navigation
|
|
||||||
- ✅ **Auto-checking** - every 24 hours in background
|
|
||||||
- ✅ **One-click updates** - users can update directly from UI
|
|
||||||
|
|
||||||
### **🐳 Docker Configuration**
|
|
||||||
- ✅ **docker-compose.yml** - local builds with version variables
|
|
||||||
- ✅ **docker-compose.prod.yml** - production with latest images
|
|
||||||
- ✅ **Version environment** - `APP_VERSION` passed to containers
|
|
||||||
- ✅ **Docker socket** - mounted for in-container updates
|
|
||||||
|
|
||||||
### **🚀 Automated Release Workflow**
|
|
||||||
- ✅ **GitHub Actions** - `.github/workflows/release.yml`
|
|
||||||
- ✅ **Semantic version extraction** - from Git tags
|
|
||||||
- ✅ **Multi-arch builds** - backend and frontend matrix
|
|
||||||
- ✅ **Docker registry push** - automatic with version tags
|
|
||||||
- ✅ **GitHub releases** - automated creation
|
|
||||||
- ✅ **SBOM generation** - security and compliance
|
|
||||||
|
|
||||||
### **📋 Documentation Created**
|
|
||||||
- ✅ **VERSION_WORKFLOW.md** - complete versioning guide
|
|
||||||
- ✅ **Release script** - manual release automation
|
|
||||||
- ✅ **Update guides** - user documentation
|
|
||||||
|
|
||||||
## 🎯 How Users Get Updates
|
|
||||||
|
|
||||||
### **Current Experience:**
|
|
||||||
```bash
|
|
||||||
# User just runs:
|
|
||||||
docker compose up
|
|
||||||
|
|
||||||
# System automatically:
|
|
||||||
# 1. Sets APP_VERSION from environment
|
|
||||||
# 2. Checks for updates every 24h
|
|
||||||
# 3. Shows update button in left nav
|
|
||||||
# 4. Pulls latest images when clicked
|
|
||||||
# 5. Restarts services automatically
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Version Detection:**
|
|
||||||
- **Backend**: Reads `APP_VERSION` environment variable
|
|
||||||
- **Frontend**: Reads `VITE_APP_VERSION` from build
|
|
||||||
- **Comparison**: Current vs `latest` tag in registry
|
|
||||||
|
|
||||||
### **Update Flow:**
|
|
||||||
1. **Background check** → API call to `/api/updates/check`
|
|
||||||
2. **Version compare** → Semantic version comparison
|
|
||||||
3. **UI notification** → Update button appears in left sidebar
|
|
||||||
4. **User action** → Click to install update
|
|
||||||
5. **Docker pull** → Backend pulls `latest` images
|
|
||||||
6. **Service restart** → Automatic with new images
|
|
||||||
|
|
||||||
## 📦 Release Strategy
|
|
||||||
|
|
||||||
### **Tag Management:**
|
|
||||||
```
|
|
||||||
ghcr.io/dvorinka/trackeep/backend:latest ← Always newest
|
|
||||||
ghcr.io/dvorinka/trackeep/backend:1.2.5 ← This release
|
|
||||||
ghcr.io/dvorinka/trackeep/backend:1.2.4 ← Previous release
|
|
||||||
|
|
||||||
ghcr.io/dvorinka/trackeep/frontend:latest ← Always newest
|
|
||||||
ghcr.io/dvorinka/trackeep/frontend:1.2.5 ← This release
|
|
||||||
ghcr.io/dvorinka/trackeep/frontend:1.2.4 ← Previous release
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Semantic Version Rules:**
|
|
||||||
```
|
|
||||||
1.2.5 → 1.3.0 (MINOR: new features)
|
|
||||||
1.2.5 → 1.2.6 (PATCH: bug fixes)
|
|
||||||
1.2.5 → 2.0.0 (MAJOR: breaking changes)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔄 Future Release Process
|
|
||||||
|
|
||||||
### **Automated (Recommended):**
|
|
||||||
```bash
|
|
||||||
# 1. Make changes
|
|
||||||
git commit -m "feat: add new feature"
|
|
||||||
|
|
||||||
# 2. Bump version (semantic-release will handle)
|
|
||||||
# 3. Push to trigger GitHub Actions
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# 4. GitHub Actions automatically:
|
|
||||||
# - Builds Docker images
|
|
||||||
# - Pushes to registry
|
|
||||||
# - Creates GitHub release
|
|
||||||
# - Updates documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Manual:**
|
|
||||||
```bash
|
|
||||||
# 1. Use release script
|
|
||||||
./scripts/release.sh 1.2.6
|
|
||||||
|
|
||||||
# 2. Or manual process
|
|
||||||
export APP_VERSION=1.2.6
|
|
||||||
git tag v1.2.6
|
|
||||||
git push origin v1.2.6
|
|
||||||
docker build & push
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✨ Industry Best Practices Implemented
|
|
||||||
|
|
||||||
### **Version Management:**
|
|
||||||
- ✅ Semantic versioning (MAJOR.MINOR.PATCH)
|
|
||||||
- ✅ Environment variable configuration
|
|
||||||
- ✅ Git tagging with proper format
|
|
||||||
- ✅ Automated changelog generation
|
|
||||||
|
|
||||||
### **Docker Strategy:**
|
|
||||||
- ✅ Multi-stage builds
|
|
||||||
- ✅ Layer caching
|
|
||||||
- ✅ Security scanning (SBOM)
|
|
||||||
- ✅ Proper tagging (latest + versioned)
|
|
||||||
|
|
||||||
### **Release Automation:**
|
|
||||||
- ✅ GitHub Actions CI/CD
|
|
||||||
- ✅ Automated testing
|
|
||||||
- ✅ Artifact management
|
|
||||||
- ✅ Rollback capability
|
|
||||||
|
|
||||||
### **User Experience:**
|
|
||||||
- ✅ Zero-friction updates
|
|
||||||
- ✅ Background checking
|
|
||||||
- ✅ UI notifications
|
|
||||||
- ✅ One-click installation
|
|
||||||
- ✅ No authentication required
|
|
||||||
|
|
||||||
## 🎊 Next Steps
|
|
||||||
|
|
||||||
### **For v1.3.0:**
|
|
||||||
1. **New features** → Add to backlog
|
|
||||||
2. **Bug fixes** → Document in commits
|
|
||||||
3. **Version bump** → `1.3.0` (MINOR version)
|
|
||||||
|
|
||||||
### **Monitoring:**
|
|
||||||
1. **Update analytics** → Track update adoption
|
|
||||||
2. **Error tracking** → Monitor update failures
|
|
||||||
3. **User feedback** → Collect update experience
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 Release Status: COMPLETE
|
|
||||||
|
|
||||||
**Trackeep v1.2.5 is now ready with:**
|
|
||||||
- ✅ Proper semantic versioning
|
|
||||||
- ✅ Automated release workflow
|
|
||||||
- ✅ Docker-based update system
|
|
||||||
- ✅ Complete user documentation
|
|
||||||
- ✅ Industry best practices
|
|
||||||
|
|
||||||
**Users can now:**
|
|
||||||
- 🚀 `docker compose up` and get automatic updates
|
|
||||||
- 🔄 See update notifications in left navigation
|
|
||||||
- ⚡ Install updates with one click
|
|
||||||
- 📦 Always get the latest versions
|
|
||||||
|
|
||||||
**The update system is production-ready!** 🚀
|
|
||||||
@@ -41,7 +41,7 @@ type AppConfig struct {
|
|||||||
func Load() *Config {
|
func Load() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: getEnvWithDefault("PORT", "8080"),
|
Port: getEnvWithDefault("BACKEND_PORT", getEnvWithDefault("PORT", "8080")),
|
||||||
ReadTimeout: getDurationEnv("READ_TIMEOUT", 15*time.Second),
|
ReadTimeout: getDurationEnv("READ_TIMEOUT", 15*time.Second),
|
||||||
WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 15*time.Second),
|
WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 15*time.Second),
|
||||||
IdleTimeout: getDurationEnv("IDLE_TIMEOUT", 60*time.Second),
|
IdleTimeout: getDurationEnv("IDLE_TIMEOUT", 60*time.Second),
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -771,3 +776,243 @@ func formatTimeAgo(t time.Time) string {
|
|||||||
return t.Format("Jan 2, 2006")
|
return t.Format("Jan 2, 2006")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GitHubRelease represents a GitHub release
|
||||||
|
type GitHubRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Draft bool `json:"draft"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
PublishedAt string `json:"published_at"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestVersion fetches the latest version from GitHub releases
|
||||||
|
func GetLatestVersion() (string, error) {
|
||||||
|
// GitHub API endpoint for releases
|
||||||
|
url := "https://api.github.com/repos/dvorinka/trackeep/releases"
|
||||||
|
|
||||||
|
// Create HTTP request
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||||
|
req.Header.Set("User-Agent", "Trackeep-Backend")
|
||||||
|
|
||||||
|
// Make request
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to fetch releases: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
var releases []GitHubRelease
|
||||||
|
if err := json.Unmarshal(body, &releases); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find latest non-draft release
|
||||||
|
for _, release := range releases {
|
||||||
|
if !release.Draft && !release.Prerelease {
|
||||||
|
return release.TagName, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no stable release found, return the latest release (including prerelease)
|
||||||
|
if len(releases) > 0 {
|
||||||
|
return releases[0].TagName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("no releases found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentVersion detects the current running version
|
||||||
|
func GetCurrentVersion() (string, error) {
|
||||||
|
// Method 1: Check if running in Docker and get image info
|
||||||
|
if isRunningInDocker() {
|
||||||
|
if version, err := getDockerImageVersion(); err == nil && version != "" {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Check for version file or environment variable
|
||||||
|
if version := os.Getenv("TRACKEEP_VERSION"); version != "" {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Try to read from version file
|
||||||
|
if version, err := readVersionFile(); err == nil && version != "" {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: Check git tag if running from source
|
||||||
|
if version, err := getGitVersion(); err == nil && version != "" {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Return build time or unknown
|
||||||
|
if buildTime := os.Getenv("BUILD_TIME"); buildTime != "" {
|
||||||
|
return fmt.Sprintf("build-%s", buildTime), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRunningInDocker checks if the application is running in a Docker container
|
||||||
|
func isRunningInDocker() bool {
|
||||||
|
// Check for .dockerenv file
|
||||||
|
if _, err := os.Stat("/.dockerenv"); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Docker in cgroup
|
||||||
|
data, err := os.ReadFile("/proc/1/cgroup")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Contains(string(data), "docker")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDockerImageVersion gets the Docker image tag
|
||||||
|
func getDockerImageVersion() (string, error) {
|
||||||
|
// Try to get container ID from cgroup
|
||||||
|
containerID, err := getContainerID()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to inspect the container to get image info
|
||||||
|
cmd := exec.Command("docker", "inspect", "--format='{{.Config.Image}}'", containerID)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageName := strings.TrimSpace(string(output))
|
||||||
|
if strings.Contains(imageName, ":") {
|
||||||
|
parts := strings.Split(imageName, ":")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
tag := parts[len(parts)-1]
|
||||||
|
// Remove quotes if present
|
||||||
|
tag = strings.Trim(tag, "'")
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "latest", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContainerID attempts to get the current container ID
|
||||||
|
func getContainerID() (string, error) {
|
||||||
|
// Method 1: Read from /proc/self/cgroup
|
||||||
|
data, err := os.ReadFile("/proc/self/cgroup")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "docker") {
|
||||||
|
parts := strings.Split(line, "/")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
containerID := parts[len(parts)-1]
|
||||||
|
// Remove any non-hex characters
|
||||||
|
containerID = strings.Trim(containerID, " \t\r\n")
|
||||||
|
if len(containerID) >= 12 {
|
||||||
|
return containerID[:12], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Try to get from hostname
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err == nil && len(hostname) >= 12 {
|
||||||
|
return hostname[:12], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("could not determine container ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// readVersionFile tries to read version from a file
|
||||||
|
func readVersionFile() (string, error) {
|
||||||
|
// Try multiple possible version file locations
|
||||||
|
versionFiles := []string{
|
||||||
|
"/app/VERSION",
|
||||||
|
"/app/version.txt",
|
||||||
|
"./VERSION",
|
||||||
|
"./version.txt",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range versionFiles {
|
||||||
|
if data, err := os.ReadFile(file); err == nil {
|
||||||
|
return strings.TrimSpace(string(data)), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("no version file found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGitVersion gets version from git tag
|
||||||
|
func getGitVersion() (string, error) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "", errors.New("git version detection not supported on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "describe", "--tags", "--abbrev=0")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
version := strings.TrimSpace(string(output))
|
||||||
|
return strings.TrimPrefix(version, "v"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersionHandler returns the current and latest version
|
||||||
|
func GetVersionHandler(c *gin.Context) {
|
||||||
|
latestVersion, err := GetLatestVersion()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{
|
||||||
|
"error": "Failed to fetch latest version",
|
||||||
|
"details": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current running version
|
||||||
|
currentVersion, err := GetCurrentVersion()
|
||||||
|
if err != nil {
|
||||||
|
currentVersion = "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean the version tag (remove 'v' prefix if present)
|
||||||
|
cleanLatestVersion := strings.TrimPrefix(latestVersion, "v")
|
||||||
|
|
||||||
|
response := gin.H{
|
||||||
|
"current_version": currentVersion,
|
||||||
|
"latest_version": cleanLatestVersion,
|
||||||
|
"latest_tag": latestVersion, // Keep the original tag for reference
|
||||||
|
"is_latest": currentVersion == cleanLatestVersion || currentVersion == "latest",
|
||||||
|
"update_available": currentVersion != cleanLatestVersion && currentVersion != "latest",
|
||||||
|
"running_in_docker": isRunningInDocker(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ReferenceInput struct {
|
||||||
|
EntityType string `json:"entity_type"`
|
||||||
|
EntityID uint `json:"entity_id"`
|
||||||
|
DeepLink string `json:"deep_link"`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateMessageRequest struct {
|
type CreateMessageRequest struct {
|
||||||
Body string `json:"body"`
|
Body string `json:"body"`
|
||||||
Attachments []AttachmentInput `json:"attachments"`
|
Attachments []AttachmentInput `json:"attachments"`
|
||||||
Metadata map[string]interface{} `json:"metadata"`
|
Metadata map[string]interface{} `json:"metadata"`
|
||||||
|
References []ReferenceInput `json:"references"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateMessageRequest struct {
|
type UpdateMessageRequest struct {
|
||||||
@@ -641,8 +648,8 @@ func CreateConversationMessage(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trimmedBody := strings.TrimSpace(req.Body)
|
trimmedBody := strings.TrimSpace(req.Body)
|
||||||
if trimmedBody == "" && len(req.Attachments) == 0 {
|
if trimmedBody == "" && len(req.Attachments) == 0 && len(req.References) == 0 {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Message body or attachments are required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Message body, attachments, or references are required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,6 +663,37 @@ func CreateConversationMessage(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
referenceRows := make([]models.MessageReference, 0, len(req.References))
|
||||||
|
for _, ref := range req.References {
|
||||||
|
entityType := normalizeReferenceType(ref.EntityType)
|
||||||
|
if entityType == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reference entity_type"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ref.EntityID == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reference entity_id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deepLink := strings.TrimSpace(ref.DeepLink)
|
||||||
|
if deepLink == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid reference deep_link"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !isReferenceDeepLinkAllowed(deepLink) {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported reference deep_link"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !canReferenceEntity(models.DB, userID, entityType, ref.EntityID) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "Reference target is not accessible"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
referenceRows = append(referenceRows, models.MessageReference{
|
||||||
|
EntityType: entityType,
|
||||||
|
EntityID: ref.EntityID,
|
||||||
|
DeepLink: deepLink,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
suggestions, inferredAttachments, isSensitive := services.DetectMessageContent(trimmedBody)
|
suggestions, inferredAttachments, isSensitive := services.DetectMessageContent(trimmedBody)
|
||||||
for _, inferred := range inferredAttachments {
|
for _, inferred := range inferredAttachments {
|
||||||
if hasAttachment(attachmentRows, inferred.Kind, inferred.URL) {
|
if hasAttachment(attachmentRows, inferred.Kind, inferred.URL) {
|
||||||
@@ -719,6 +757,13 @@ func CreateConversationMessage(c *gin.Context) {
|
|||||||
models.DB.Create(&attachmentRows)
|
models.DB.Create(&attachmentRows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range referenceRows {
|
||||||
|
referenceRows[i].MessageID = message.ID
|
||||||
|
}
|
||||||
|
if len(referenceRows) > 0 {
|
||||||
|
models.DB.Create(&referenceRows)
|
||||||
|
}
|
||||||
|
|
||||||
if len(suggestions) > 0 {
|
if len(suggestions) > 0 {
|
||||||
suggestionRows := make([]models.MessageSuggestion, 0, len(suggestions))
|
suggestionRows := make([]models.MessageSuggestion, 0, len(suggestions))
|
||||||
for _, s := range suggestions {
|
for _, s := range suggestions {
|
||||||
@@ -2159,6 +2204,33 @@ func normalizeAttachmentKind(kind string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeReferenceType(entityType string) string {
|
||||||
|
t := strings.ToLower(strings.TrimSpace(entityType))
|
||||||
|
switch t {
|
||||||
|
case "task", "bookmark", "calendar_event", "youtube_video", "learning_path", "saved_search", "github", "password_vault_item", "ai_chat_session", "ai_chat_message":
|
||||||
|
return t
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isReferenceDeepLinkAllowed(deepLink string) bool {
|
||||||
|
return strings.HasPrefix(deepLink, "/") || strings.HasPrefix(deepLink, "http://") || strings.HasPrefix(deepLink, "https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
func canReferenceEntity(db *gorm.DB, userID uint, entityType string, entityID uint) bool {
|
||||||
|
switch entityType {
|
||||||
|
case "ai_chat_session":
|
||||||
|
var session models.ChatSession
|
||||||
|
return db.Where("id = ? AND user_id = ?", entityID, userID).First(&session).Error == nil
|
||||||
|
case "ai_chat_message":
|
||||||
|
var message models.ChatMessage
|
||||||
|
return db.Where("id = ? AND user_id = ?", entityID, userID).First(&message).Error == nil
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func compactMessageTitle(text string, limit int) string {
|
func compactMessageTitle(text string, limit int) string {
|
||||||
trimmed := strings.TrimSpace(text)
|
trimmed := strings.TrimSpace(text)
|
||||||
if len(trimmed) <= limit {
|
if len(trimmed) <= limit {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -63,78 +62,176 @@ func SearchWeb(c *gin.Context) {
|
|||||||
req.Count = 10
|
req.Count = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := os.Getenv("BRAVE_API_KEY")
|
// Get user ID from context (authentication is required)
|
||||||
if apiKey == "" {
|
userID, exists := c.Get("user_id")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"})
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required for search functionality"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build Brave Search API request
|
// Get user's search settings from database
|
||||||
baseURL := "https://api.search.brave.com/res/v1/web/search"
|
searchSettings, err := GetSearchSettingsForAPI(userID.(int))
|
||||||
q := url.Values{}
|
|
||||||
q.Set("q", req.Query)
|
|
||||||
q.Set("count", fmt.Sprint(req.Count))
|
|
||||||
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
|
|
||||||
|
|
||||||
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get search settings"})
|
||||||
return
|
|
||||||
}
|
|
||||||
reqHTTP.Header.Set("Accept", "application/json")
|
|
||||||
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(reqHTTP)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave Search API"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave API error: %d", resp.StatusCode)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var braveResp BraveSearchResponse
|
// Check if user has search API key configured
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&braveResp); err != nil {
|
if searchSettings.SearchAPIProvider == "brave" {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave response"})
|
apiKey := searchSettings.BraveAPIKey
|
||||||
return
|
if apiKey == "" {
|
||||||
}
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Prefer web.results, fall back to mixed.results
|
// Build Brave Search API request
|
||||||
resultsRaw := braveResp.Web.Results
|
baseURL := "https://api.search.brave.com/res/v1/web/search"
|
||||||
if len(resultsRaw) == 0 {
|
q := url.Values{}
|
||||||
resultsRaw = braveResp.Mixed.Results
|
q.Set("q", req.Query)
|
||||||
}
|
q.Set("count", fmt.Sprint(req.Count))
|
||||||
|
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
|
||||||
|
|
||||||
results := make([]BraveSearchResult, 0, len(resultsRaw))
|
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
||||||
for _, r := range resultsRaw {
|
if err != nil {
|
||||||
title, _ := r["title"].(string)
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
|
||||||
urlStr, _ := r["url"].(string)
|
return
|
||||||
desc, _ := r["description"].(string)
|
}
|
||||||
lang, _ := r["language"].(string)
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
pageAge, _ := r["page_age"].(string)
|
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
|
||||||
|
|
||||||
results = append(results, BraveSearchResult{
|
resp, err := http.DefaultClient.Do(reqHTTP)
|
||||||
Title: title,
|
if err != nil {
|
||||||
URL: urlStr,
|
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave Search API"})
|
||||||
Description: desc,
|
return
|
||||||
PublishedDate: pageAge,
|
}
|
||||||
Language: lang,
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave API error: %d", resp.StatusCode)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var braveResp BraveSearchResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&braveResp); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave response"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer web.results, fall back to mixed.results
|
||||||
|
resultsRaw := braveResp.Web.Results
|
||||||
|
if len(resultsRaw) == 0 {
|
||||||
|
resultsRaw = braveResp.Mixed.Results
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]BraveSearchResult, 0, len(resultsRaw))
|
||||||
|
for _, r := range resultsRaw {
|
||||||
|
title, _ := r["title"].(string)
|
||||||
|
urlStr, _ := r["url"].(string)
|
||||||
|
desc, _ := r["description"].(string)
|
||||||
|
lang, _ := r["language"].(string)
|
||||||
|
pageAge, _ := r["page_age"].(string)
|
||||||
|
|
||||||
|
results = append(results, BraveSearchResult{
|
||||||
|
Title: title,
|
||||||
|
URL: urlStr,
|
||||||
|
Description: desc,
|
||||||
|
PublishedDate: pageAge,
|
||||||
|
Language: lang,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"results": results,
|
||||||
|
"query": gin.H{
|
||||||
|
"original": braveResp.Query.Original,
|
||||||
|
"display": braveResp.Query.Display,
|
||||||
|
},
|
||||||
|
"count": len(results),
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
// Use the configured provider
|
||||||
"results": results,
|
if searchSettings.SearchAPIProvider == "brave" {
|
||||||
"query": gin.H{
|
apiKey := searchSettings.BraveAPIKey
|
||||||
"original": braveResp.Query.Original,
|
if apiKey == "" {
|
||||||
"display": braveResp.Query.Display,
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
|
||||||
},
|
return
|
||||||
"count": len(results),
|
}
|
||||||
})
|
|
||||||
|
// Build Brave Search API request
|
||||||
|
baseURL := searchSettings.BraveSearchBaseURL
|
||||||
|
q := url.Values{}
|
||||||
|
q.Set("q", req.Query)
|
||||||
|
q.Set("count", fmt.Sprint(req.Count))
|
||||||
|
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
|
||||||
|
|
||||||
|
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
|
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(reqHTTP)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave Search API"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave API error: %d", resp.StatusCode)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var braveResp BraveSearchResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&braveResp); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave response"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer web.results, fall back to mixed.results
|
||||||
|
resultsRaw := braveResp.Web.Results
|
||||||
|
if len(resultsRaw) == 0 {
|
||||||
|
resultsRaw = braveResp.Mixed.Results
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]BraveSearchResult, 0, len(resultsRaw))
|
||||||
|
for _, r := range resultsRaw {
|
||||||
|
title, _ := r["title"].(string)
|
||||||
|
urlStr, _ := r["url"].(string)
|
||||||
|
desc, _ := r["description"].(string)
|
||||||
|
lang, _ := r["language"].(string)
|
||||||
|
pageAge, _ := r["page_age"].(string)
|
||||||
|
|
||||||
|
results = append(results, BraveSearchResult{
|
||||||
|
Title: title,
|
||||||
|
URL: urlStr,
|
||||||
|
Description: desc,
|
||||||
|
PublishedDate: pageAge,
|
||||||
|
Language: lang,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"results": results,
|
||||||
|
"query": gin.H{
|
||||||
|
"original": braveResp.Query.Original,
|
||||||
|
"display": braveResp.Query.Display,
|
||||||
|
},
|
||||||
|
"count": len(results),
|
||||||
|
})
|
||||||
|
} else if searchSettings.SearchAPIProvider == "serper" {
|
||||||
|
// TODO: Implement Serper API integration
|
||||||
|
c.JSON(http.StatusNotImplemented, gin.H{"error": "Serper API integration not yet implemented"})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid search API provider configured. Please configure a search API provider in settings."})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchNews handles POST /api/v1/search/news
|
||||||
func SearchNews(c *gin.Context) {
|
func SearchNews(c *gin.Context) {
|
||||||
fmt.Printf("DEBUG: SearchNews function called\n")
|
fmt.Printf("DEBUG: SearchNews function called\n")
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -151,97 +248,215 @@ func SearchNews(c *gin.Context) {
|
|||||||
req.Count = 10
|
req.Count = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := os.Getenv("BRAVE_API_KEY")
|
// Get user ID from context (authentication is required)
|
||||||
if apiKey == "" {
|
userID, exists := c.Get("user_id")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"})
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required for search functionality"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL := "https://api.search.brave.com/res/v1/news/search"
|
// Get user's search settings from database
|
||||||
q := url.Values{}
|
searchSettings, err := GetSearchSettingsForAPI(userID.(int))
|
||||||
q.Set("q", req.Query)
|
|
||||||
q.Set("count", fmt.Sprint(req.Count))
|
|
||||||
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
|
|
||||||
|
|
||||||
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get search settings"})
|
||||||
return
|
|
||||||
}
|
|
||||||
reqHTTP.Header.Set("Accept", "application/json")
|
|
||||||
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(reqHTTP)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave News API"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
// Check if user has search API key configured
|
||||||
resp.Body.Close()
|
if searchSettings.SearchAPIProvider == "brave" {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave News API error: %d", resp.StatusCode)})
|
apiKey := searchSettings.BraveAPIKey
|
||||||
return
|
if apiKey == "" {
|
||||||
}
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Brave Search API key not configured. Please configure your search API key in settings."})
|
||||||
|
return
|
||||||
// Read the response body for debugging
|
|
||||||
bodyBytes, err := io.ReadAll(resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("DEBUG: Raw Brave News API response: %s\n", string(bodyBytes))
|
|
||||||
|
|
||||||
var braveResp BraveNewsResponse
|
|
||||||
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&braveResp); err != nil {
|
|
||||||
fmt.Printf("DEBUG: JSON decode error: %v\n", err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave news response"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
fmt.Printf("DEBUG: Parsed BraveNewsResponse: %+v\n", braveResp)
|
|
||||||
fmt.Printf("DEBUG: Number of results: %d\n", len(braveResp.Results))
|
|
||||||
|
|
||||||
resultsRaw := braveResp.Results
|
|
||||||
results := make([]BraveSearchResult, 0, len(resultsRaw))
|
|
||||||
for _, r := range resultsRaw {
|
|
||||||
title, _ := r["title"].(string)
|
|
||||||
urlStr, _ := r["url"].(string)
|
|
||||||
desc, _ := r["description"].(string)
|
|
||||||
lang, _ := r["language"].(string)
|
|
||||||
pubDate, _ := r["published_date"].(string)
|
|
||||||
if pubDate == "" {
|
|
||||||
pubDate, _ = r["page_age"].(string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, BraveSearchResult{
|
baseURL := "https://api.search.brave.com/res/v1/news/search"
|
||||||
Title: title,
|
q := url.Values{}
|
||||||
URL: urlStr,
|
q.Set("q", req.Query)
|
||||||
Description: desc,
|
q.Set("count", fmt.Sprint(req.Count))
|
||||||
PublishedDate: pubDate,
|
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
|
||||||
Language: lang,
|
|
||||||
|
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
|
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(reqHTTP)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave News API"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave News API error: %d", resp.StatusCode)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the response body for debugging
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("DEBUG: Raw Brave News API response: %s\n", string(bodyBytes))
|
||||||
|
|
||||||
|
var braveResp BraveNewsResponse
|
||||||
|
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&braveResp); err != nil {
|
||||||
|
fmt.Printf("DEBUG: JSON decode error: %v\n", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave news response"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
fmt.Printf("DEBUG: Parsed BraveNewsResponse: %+v\n", braveResp)
|
||||||
|
fmt.Printf("DEBUG: Number of results: %d\n", len(braveResp.Results))
|
||||||
|
|
||||||
|
resultsRaw := braveResp.Results
|
||||||
|
results := make([]BraveSearchResult, 0, len(resultsRaw))
|
||||||
|
for _, r := range resultsRaw {
|
||||||
|
title, _ := r["title"].(string)
|
||||||
|
urlStr, _ := r["url"].(string)
|
||||||
|
desc, _ := r["description"].(string)
|
||||||
|
lang, _ := r["language"].(string)
|
||||||
|
pubDate, _ := r["published_date"].(string)
|
||||||
|
if pubDate == "" {
|
||||||
|
pubDate, _ = r["page_age"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, BraveSearchResult{
|
||||||
|
Title: title,
|
||||||
|
URL: urlStr,
|
||||||
|
Description: desc,
|
||||||
|
PublishedDate: pubDate,
|
||||||
|
Language: lang,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
original := braveResp.Query.Original
|
||||||
|
display := braveResp.Query.Display
|
||||||
|
if original == "" {
|
||||||
|
original = req.Query
|
||||||
|
}
|
||||||
|
if display == "" {
|
||||||
|
display = req.Query
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"results": results,
|
||||||
|
"query": gin.H{
|
||||||
|
"original": original,
|
||||||
|
"display": display,
|
||||||
|
},
|
||||||
|
"count": len(results),
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
original := braveResp.Query.Original
|
// Use the configured provider
|
||||||
display := braveResp.Query.Display
|
if searchSettings.SearchAPIProvider == "brave" {
|
||||||
if original == "" {
|
apiKey := searchSettings.BraveAPIKey
|
||||||
original = req.Query
|
if apiKey == "" {
|
||||||
}
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Brave API key not configured"})
|
||||||
if display == "" {
|
return
|
||||||
display = req.Query
|
}
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
baseURL := "https://api.search.brave.com/res/v1/news/search"
|
||||||
"results": results,
|
q := url.Values{}
|
||||||
"query": gin.H{
|
q.Set("q", req.Query)
|
||||||
"original": original,
|
q.Set("count", fmt.Sprint(req.Count))
|
||||||
"display": display,
|
endpoint := fmt.Sprintf("%s?%s", baseURL, q.Encode())
|
||||||
},
|
|
||||||
"count": len(results),
|
reqHTTP, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
||||||
})
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Brave request"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
|
reqHTTP.Header.Set("X-Subscription-Token", apiKey)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(reqHTTP)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave News API"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave News API error: %d", resp.StatusCode)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the response body for debugging
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("DEBUG: Raw Brave News API response: %s\n", string(bodyBytes))
|
||||||
|
|
||||||
|
var braveResp BraveNewsResponse
|
||||||
|
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&braveResp); err != nil {
|
||||||
|
fmt.Printf("DEBUG: JSON decode error: %v\n", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave news response"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
fmt.Printf("DEBUG: Parsed BraveNewsResponse: %+v\n", braveResp)
|
||||||
|
fmt.Printf("DEBUG: Number of results: %d\n", len(braveResp.Results))
|
||||||
|
|
||||||
|
resultsRaw := braveResp.Results
|
||||||
|
results := make([]BraveSearchResult, 0, len(resultsRaw))
|
||||||
|
for _, r := range resultsRaw {
|
||||||
|
title, _ := r["title"].(string)
|
||||||
|
urlStr, _ := r["url"].(string)
|
||||||
|
desc, _ := r["description"].(string)
|
||||||
|
lang, _ := r["language"].(string)
|
||||||
|
pubDate, _ := r["published_date"].(string)
|
||||||
|
if pubDate == "" {
|
||||||
|
pubDate, _ = r["page_age"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, BraveSearchResult{
|
||||||
|
Title: title,
|
||||||
|
URL: urlStr,
|
||||||
|
Description: desc,
|
||||||
|
PublishedDate: pubDate,
|
||||||
|
Language: lang,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
original := braveResp.Query.Original
|
||||||
|
display := braveResp.Query.Display
|
||||||
|
if original == "" {
|
||||||
|
original = req.Query
|
||||||
|
}
|
||||||
|
if display == "" {
|
||||||
|
display = req.Query
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"results": results,
|
||||||
|
"query": gin.H{
|
||||||
|
"original": original,
|
||||||
|
"display": display,
|
||||||
|
},
|
||||||
|
"count": len(results),
|
||||||
|
})
|
||||||
|
} else if searchSettings.SearchAPIProvider == "serper" {
|
||||||
|
// TODO: Implement Serper API integration for news
|
||||||
|
c.JSON(http.StatusNotImplemented, gin.H{"error": "Serper API integration not yet implemented"})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid search API provider configured. Please configure a search API provider in settings."})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSearchSuggestions handles GET /api/v1/search/suggestions
|
// GetSearchSuggestions handles GET /api/v1/search/suggestions
|
||||||
|
|||||||
@@ -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/cipher"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
@@ -60,9 +61,13 @@ type TOTPLoginRequest struct {
|
|||||||
|
|
||||||
// encrypt encrypts text using AES-GCM
|
// encrypt encrypts text using AES-GCM
|
||||||
func encrypt(plaintext string) (string, error) {
|
func encrypt(plaintext string) (string, error) {
|
||||||
key := []byte(os.Getenv("ENCRYPTION_KEY"))
|
keyHex := strings.TrimSpace(os.Getenv("JWT_SECRET"))
|
||||||
|
key, err := hex.DecodeString(keyHex)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode JWT secret for encryption: %v", err)
|
||||||
|
}
|
||||||
if len(key) != 32 {
|
if len(key) != 32 {
|
||||||
return "", fmt.Errorf("encryption key must be 32 bytes")
|
return "", fmt.Errorf("JWT secret must be 32 bytes when decoded, got %d", len(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
@@ -86,9 +91,13 @@ func encrypt(plaintext string) (string, error) {
|
|||||||
|
|
||||||
// decrypt decrypts text using AES-GCM
|
// decrypt decrypts text using AES-GCM
|
||||||
func decrypt(ciphertext string) (string, error) {
|
func decrypt(ciphertext string) (string, error) {
|
||||||
key := []byte(os.Getenv("ENCRYPTION_KEY"))
|
keyHex := strings.TrimSpace(os.Getenv("JWT_SECRET"))
|
||||||
|
key, err := hex.DecodeString(keyHex)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode JWT secret for encryption: %v", err)
|
||||||
|
}
|
||||||
if len(key) != 32 {
|
if len(key) != 32 {
|
||||||
return "", fmt.Errorf("encryption key must be 32 bytes")
|
return "", fmt.Errorf("JWT secret must be 32 bytes when decoded, got %d", len(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
block, err := aes.NewCipher(key)
|
block, err := aes.NewCipher(key)
|
||||||
|
|||||||
@@ -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"
|
"archive/zip"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -66,33 +67,48 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getCurrentVersion reads the current version from frontend/package.json
|
||||||
|
func getCurrentVersion() string {
|
||||||
|
// Try to read from frontend/package.json first
|
||||||
|
packageJsonPath := "frontend/package.json"
|
||||||
|
if content, err := os.ReadFile(packageJsonPath); err == nil {
|
||||||
|
var packageJson struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(content, &packageJson); err == nil && packageJson.Version != "" {
|
||||||
|
log.Printf("Found version in frontend/package.json: %s", packageJson.Version)
|
||||||
|
return packageJson.Version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to backend/go.mod
|
||||||
|
goModPath := "go.mod"
|
||||||
|
if content, err := os.ReadFile(goModPath); err == nil {
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "module ") {
|
||||||
|
// Extract version from module path or use a default
|
||||||
|
// For now, return a default version
|
||||||
|
log.Printf("Using fallback version from go.mod")
|
||||||
|
return "1.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final fallback
|
||||||
|
log.Printf("Using default version - could not detect from source files")
|
||||||
|
return "1.2.5"
|
||||||
|
}
|
||||||
|
|
||||||
// CheckForUpdates checks if a new version is available using Docker registry
|
// CheckForUpdates checks if a new version is available using Docker registry
|
||||||
func CheckForUpdates(c *gin.Context) {
|
func CheckForUpdates(c *gin.Context) {
|
||||||
updateMutex.Lock()
|
updateMutex.Lock()
|
||||||
defer updateMutex.Unlock()
|
defer updateMutex.Unlock()
|
||||||
|
|
||||||
// Get current version from go.mod
|
// Get current version from frontend/package.json
|
||||||
currentVersion := "1.2.5"
|
currentVersion := getCurrentVersion()
|
||||||
|
|
||||||
// Try to read from go.mod if running in development
|
log.Printf("Checking for updates using GitHub releases (current version: %s)", currentVersion)
|
||||||
if _, err := os.Stat("go.mod"); err == nil {
|
|
||||||
if content, err := os.ReadFile("go.mod"); err == nil {
|
|
||||||
lines := strings.Split(string(content), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.Contains(line, "go ") && strings.Contains(line, "1.2.5") {
|
|
||||||
// Extract version from go.mod
|
|
||||||
parts := strings.Fields(line)
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
currentVersion = strings.TrimSpace(parts[1])
|
|
||||||
log.Printf("Found version in go.mod: %s", currentVersion)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Checking for updates using Docker registry (current version: %s)", currentVersion)
|
|
||||||
|
|
||||||
// Check for updates using Docker registry
|
// Check for updates using Docker registry
|
||||||
updateInfo, updateAvailable, err := checkForUpdatesWithDocker(currentVersion)
|
updateInfo, updateAvailable, err := checkForUpdatesWithDocker(currentVersion)
|
||||||
@@ -109,14 +125,20 @@ func CheckForUpdates(c *gin.Context) {
|
|||||||
currentUpdate = updateInfo
|
currentUpdate = updateInfo
|
||||||
updateProgress.Available = true
|
updateProgress.Available = true
|
||||||
} else {
|
} else {
|
||||||
currentUpdate = nil
|
// Still preserve updateInfo for displaying latest version, but mark as no update available
|
||||||
|
currentUpdate = updateInfo
|
||||||
updateProgress.Available = false
|
updateProgress.Available = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
latestVersion := ""
|
||||||
|
if updateInfo != nil {
|
||||||
|
latestVersion = updateInfo.Version
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"updateAvailable": updateAvailable,
|
"updateAvailable": updateAvailable,
|
||||||
"currentVersion": currentVersion,
|
"currentVersion": currentVersion,
|
||||||
"latestVersion": updateInfo.Version,
|
"latestVersion": latestVersion,
|
||||||
"updateInfo": currentUpdate,
|
"updateInfo": currentUpdate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -167,8 +189,142 @@ func UpdateProgressWebSocket(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkForUpdatesWithDocker checks for updates using Docker registry
|
// checkForUpdatesWithDocker checks for updates using GitHub releases
|
||||||
func checkForUpdatesWithDocker(currentVersion string) (*UpdateInfo, bool, error) {
|
func checkForUpdatesWithDocker(currentVersion string) (*UpdateInfo, bool, error) {
|
||||||
|
log.Printf("Checking for updates (current version: %s)", currentVersion)
|
||||||
|
|
||||||
|
// Get latest release from GitHub
|
||||||
|
latestRelease, err := getLatestGitHubRelease()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get latest release from GitHub: %v", err)
|
||||||
|
// Fallback to Docker registry check
|
||||||
|
return checkForUpdatesWithDockerRegistry(currentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Latest release from GitHub: %s", latestRelease.Version)
|
||||||
|
|
||||||
|
// Compare versions
|
||||||
|
if isNewerVersion(latestRelease.Version, currentVersion) {
|
||||||
|
log.Printf("Update available: %s -> %s", currentVersion, latestRelease.Version)
|
||||||
|
return latestRelease, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("No updates available - current version %s is latest", currentVersion)
|
||||||
|
return latestRelease, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLatestGitHubRelease fetches the latest release from GitHub API
|
||||||
|
func getLatestGitHubRelease() (*UpdateInfo, error) {
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
url := "https://api.github.com/repos/Dvorinka/Trackeep/releases/latest"
|
||||||
|
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch release: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON response
|
||||||
|
var release struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
PublishedAt string `json:"published_at"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
Draft bool `json:"draft"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode release JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip drafts and prereleases unless specifically allowed
|
||||||
|
if release.Draft {
|
||||||
|
return nil, fmt.Errorf("latest release is a draft")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if prereleases are allowed
|
||||||
|
allowPrerelease := os.Getenv("PRERELEASE_UPDATES") == "true"
|
||||||
|
if release.Prerelease && !allowPrerelease {
|
||||||
|
// Try to get latest non-prerelease
|
||||||
|
return getLatestStableRelease()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean version (remove 'v' prefix if present)
|
||||||
|
version := strings.TrimPrefix(release.TagName, "v")
|
||||||
|
|
||||||
|
updateInfo := &UpdateInfo{
|
||||||
|
Version: version,
|
||||||
|
ReleaseNotes: release.Body,
|
||||||
|
DownloadURL: "", // Docker images don't need download URL
|
||||||
|
Mandatory: false,
|
||||||
|
Size: "Docker images",
|
||||||
|
Checksum: "",
|
||||||
|
PublishedAt: release.PublishedAt,
|
||||||
|
Prerelease: release.Prerelease,
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLatestStableRelease gets the latest stable (non-prerelease) release
|
||||||
|
func getLatestStableRelease() (*UpdateInfo, error) {
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
url := "https://api.github.com/repos/Dvorinka/Trackeep/releases"
|
||||||
|
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch releases: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON response
|
||||||
|
var releases []struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
PublishedAt string `json:"published_at"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
Draft bool `json:"draft"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode releases JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find first stable (non-prerelease, non-draft) release
|
||||||
|
for _, release := range releases {
|
||||||
|
if !release.Draft && !release.Prerelease {
|
||||||
|
version := strings.TrimPrefix(release.TagName, "v")
|
||||||
|
|
||||||
|
updateInfo := &UpdateInfo{
|
||||||
|
Version: version,
|
||||||
|
ReleaseNotes: release.Body,
|
||||||
|
DownloadURL: "",
|
||||||
|
Mandatory: false,
|
||||||
|
Size: "Docker images",
|
||||||
|
Checksum: "",
|
||||||
|
PublishedAt: release.PublishedAt,
|
||||||
|
Prerelease: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateInfo, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no stable releases found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkForUpdatesWithDockerRegistry fallback method using Docker registry
|
||||||
|
func checkForUpdatesWithDockerRegistry(currentVersion string) (*UpdateInfo, bool, error) {
|
||||||
// Define images to check (using latest)
|
// Define images to check (using latest)
|
||||||
backendImage := "ghcr.io/dvorinka/trackeep/backend:latest"
|
backendImage := "ghcr.io/dvorinka/trackeep/backend:latest"
|
||||||
frontendImage := "ghcr.io/dvorinka/trackeep/frontend:latest"
|
frontendImage := "ghcr.io/dvorinka/trackeep/frontend:latest"
|
||||||
@@ -348,7 +504,7 @@ func updateWithDockerCompose() error {
|
|||||||
// Check if production docker-compose file exists
|
// Check if production docker-compose file exists
|
||||||
composeFile := "docker-compose.prod.yml"
|
composeFile := "docker-compose.prod.yml"
|
||||||
if _, err := os.Stat(composeFile); err != nil {
|
if _, err := os.Stat(composeFile); err != nil {
|
||||||
return fmt.Errorf("production docker-compose.yml not found")
|
return fmt.Errorf("production docker-compose file not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use docker compose command directly (assuming Docker is available on host)
|
// Use docker compose command directly (assuming Docker is available on host)
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/trackeep/backend/config"
|
"github.com/trackeep/backend/config"
|
||||||
"github.com/trackeep/backend/handlers"
|
"github.com/trackeep/backend/handlers"
|
||||||
@@ -22,23 +24,67 @@ func IsDemoMode() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initializeSecuritySecrets() error {
|
func initializeSecuritySecrets() error {
|
||||||
jwtSecret, err := utils.GetOrCreateJWTSecret()
|
// Only set JWT_SECRET if not already provided in environment
|
||||||
if err != nil {
|
if os.Getenv("JWT_SECRET") == "" {
|
||||||
return err
|
jwtSecret, err := utils.GetOrCreateJWTSecret()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Setenv("JWT_SECRET", jwtSecret)
|
||||||
|
log.Println("JWT secret initialized from file")
|
||||||
|
} else {
|
||||||
|
log.Println("JWT secret found in environment variable")
|
||||||
}
|
}
|
||||||
os.Setenv("JWT_SECRET", jwtSecret)
|
|
||||||
log.Println("JWT secret initialized successfully")
|
|
||||||
|
|
||||||
encryptionKey, err := utils.GetOrCreateEncryptionKey()
|
// Only set ENCRYPTION_KEY if not already provided in environment
|
||||||
if err != nil {
|
if os.Getenv("ENCRYPTION_KEY") == "" {
|
||||||
return err
|
encryptionKey, err := utils.GetOrCreateEncryptionKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Setenv("ENCRYPTION_KEY", encryptionKey)
|
||||||
|
log.Println("Encryption key initialized from file")
|
||||||
|
} else {
|
||||||
|
log.Println("Encryption key found in environment variable")
|
||||||
}
|
}
|
||||||
os.Setenv("ENCRYPTION_KEY", encryptionKey)
|
|
||||||
log.Println("Encryption key initialized successfully")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initializeDragonflyDB initializes DragonflyDB (Redis-compatible) connection
|
||||||
|
func initializeDragonflyDB() *redis.Client {
|
||||||
|
dragonflyAddr := os.Getenv("DRAGONFLY_ADDR")
|
||||||
|
dragonflyPassword := os.Getenv("DRAGONFLY_PASSWORD")
|
||||||
|
|
||||||
|
if dragonflyAddr == "" {
|
||||||
|
log.Println("DRAGONFLY_ADDR not set, using default: localhost:6379")
|
||||||
|
dragonflyAddr = "localhost:6379"
|
||||||
|
}
|
||||||
|
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: dragonflyAddr,
|
||||||
|
Password: dragonflyPassword,
|
||||||
|
DialTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 3 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
PoolSize: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test connection
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := rdb.Ping(ctx).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Failed to connect to DragonflyDB at %s: %v", dragonflyAddr, err)
|
||||||
|
log.Println("Falling back to in-memory cache and sessions")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Successfully connected to DragonflyDB at %s", dragonflyAddr)
|
||||||
|
return rdb
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
os.Setenv("APP_VERSION", "1.0.0")
|
os.Setenv("APP_VERSION", "1.0.0")
|
||||||
|
|
||||||
@@ -75,10 +121,28 @@ func main() {
|
|||||||
log.Fatal("Failed to initialize security secrets:", err)
|
log.Fatal("Failed to initialize security secrets:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize session store
|
// Initialize DragonflyDB
|
||||||
middleware.InitSessionStore()
|
dragonflyClient := initializeDragonflyDB()
|
||||||
|
|
||||||
|
// Initialize session store with DragonflyDB
|
||||||
|
middleware.InitSessionStore(dragonflyClient)
|
||||||
log.Println("Session store initialized successfully")
|
log.Println("Session store initialized successfully")
|
||||||
|
|
||||||
|
// Initialize cache middleware with DragonflyDB
|
||||||
|
var cacheConfig middleware.CacheConfig
|
||||||
|
if dragonflyClient != nil {
|
||||||
|
cacheConfig = middleware.CacheConfig{
|
||||||
|
Duration: 5 * time.Minute,
|
||||||
|
KeyPrefix: "trackeep:",
|
||||||
|
Enabled: true,
|
||||||
|
RedisClient: dragonflyClient,
|
||||||
|
}
|
||||||
|
log.Println("DragonflyDB cache middleware initialized")
|
||||||
|
} else {
|
||||||
|
cacheConfig = middleware.DefaultCacheConfig()
|
||||||
|
log.Println("Using in-memory cache fallback")
|
||||||
|
}
|
||||||
|
|
||||||
// Seed demo data in background
|
// Seed demo data in background
|
||||||
// go func() {
|
// go func() {
|
||||||
// SeedData()
|
// SeedData()
|
||||||
@@ -95,7 +159,9 @@ func main() {
|
|||||||
// Middleware
|
// Middleware
|
||||||
r.Use(gin.Logger())
|
r.Use(gin.Logger())
|
||||||
r.Use(gin.Recovery())
|
r.Use(gin.Recovery())
|
||||||
r.Use(middleware.SessionMiddleware()) // Add session middleware
|
r.Use(middleware.CacheMiddleware(cacheConfig)) // Add DragonflyDB cache middleware
|
||||||
|
r.Use(middleware.CacheInvalidationMiddleware(dragonflyClient)) // Add cache invalidation
|
||||||
|
r.Use(middleware.SessionMiddleware()) // Add session middleware
|
||||||
r.Use(middleware.AuditMiddleware())
|
r.Use(middleware.AuditMiddleware())
|
||||||
r.Use(middleware.InputValidationMiddleware())
|
r.Use(middleware.InputValidationMiddleware())
|
||||||
|
|
||||||
@@ -125,10 +191,17 @@ func main() {
|
|||||||
// Serve static files (frontend)
|
// Serve static files (frontend)
|
||||||
r.Static("/assets", "../frontend/dist/assets")
|
r.Static("/assets", "../frontend/dist/assets")
|
||||||
r.StaticFile("/", "../frontend/dist/index.html")
|
r.StaticFile("/", "../frontend/dist/index.html")
|
||||||
|
|
||||||
|
// Serve browser extension download
|
||||||
|
r.GET("/browser-extension", handlers.DownloadBrowserExtension)
|
||||||
|
|
||||||
r.NoRoute(func(c *gin.Context) {
|
r.NoRoute(func(c *gin.Context) {
|
||||||
c.File("../frontend/dist/index.html")
|
c.File("../frontend/dist/index.html")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Version endpoint
|
||||||
|
r.GET("/api/version", handlers.GetVersionHandler)
|
||||||
|
|
||||||
// Initialize handlers
|
// Initialize handlers
|
||||||
memberHandler := handlers.NewMemberHandler(config.GetDB())
|
memberHandler := handlers.NewMemberHandler(config.GetDB())
|
||||||
timeEntryHandler := handlers.NewTimeEntryHandler(config.GetDB())
|
timeEntryHandler := handlers.NewTimeEntryHandler(config.GetDB())
|
||||||
@@ -203,11 +276,23 @@ func main() {
|
|||||||
authProtected.GET("/ai/settings", handlers.GetAISettings)
|
authProtected.GET("/ai/settings", handlers.GetAISettings)
|
||||||
authProtected.PUT("/ai/settings", handlers.UpdateAISettings)
|
authProtected.PUT("/ai/settings", handlers.UpdateAISettings)
|
||||||
authProtected.POST("/ai/test-connection", handlers.TestAIConnection)
|
authProtected.POST("/ai/test-connection", handlers.TestAIConnection)
|
||||||
|
|
||||||
|
// Search Settings routes
|
||||||
|
authProtected.GET("/search/settings", handlers.GetSearchSettings)
|
||||||
|
authProtected.PUT("/search/settings", handlers.UpdateSearchSettings)
|
||||||
|
|
||||||
|
// Update Settings routes
|
||||||
|
authProtected.GET("/update/settings", handlers.GetUpdateSettings)
|
||||||
|
authProtected.PUT("/update/settings", handlers.UpdateUpdateSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test AI settings without auth
|
// Test AI settings without auth
|
||||||
v1.GET("/test-ai-settings", handlers.GetAISettings)
|
v1.GET("/test-ai-settings", handlers.GetAISettings)
|
||||||
|
|
||||||
|
// Test search and update settings without auth (for demo mode)
|
||||||
|
v1.GET("/test-search-settings", handlers.GetTestSearchSettings)
|
||||||
|
v1.GET("/test-update-settings", handlers.GetTestUpdateSettings)
|
||||||
|
|
||||||
// Dashboard routes (protected)
|
// Dashboard routes (protected)
|
||||||
dashboard := v1.Group("/dashboard")
|
dashboard := v1.Group("/dashboard")
|
||||||
dashboard.Use(handlers.AuthMiddleware())
|
dashboard.Use(handlers.AuthMiddleware())
|
||||||
@@ -721,6 +806,24 @@ func main() {
|
|||||||
performance.POST("/optimize", performanceHandler.OptimizeDatabase)
|
performance.POST("/optimize", performanceHandler.OptimizeDatabase)
|
||||||
performance.POST("/cleanup-audit-logs", performanceHandler.CleanupOldAuditLogs)
|
performance.POST("/cleanup-audit-logs", performanceHandler.CleanupOldAuditLogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Browser Extension API routes
|
||||||
|
browserExt := v1.Group("/browser-extension")
|
||||||
|
browserExt.Use(handlers.AuthMiddleware())
|
||||||
|
{
|
||||||
|
// API Key management
|
||||||
|
browserExt.POST("/api-keys/generate", handlers.GenerateAPIKey)
|
||||||
|
browserExt.GET("/api-keys", handlers.GetAPIKeys)
|
||||||
|
browserExt.DELETE("/api-keys/:id", handlers.RevokeAPIKey)
|
||||||
|
|
||||||
|
// Extension registration and validation
|
||||||
|
browserExt.POST("/register", handlers.RegisterBrowserExtension)
|
||||||
|
browserExt.GET("/extensions", handlers.GetBrowserExtensions)
|
||||||
|
browserExt.DELETE("/extensions/:id", handlers.RevokeBrowserExtension)
|
||||||
|
|
||||||
|
// Public endpoints (for extension validation)
|
||||||
|
browserExt.GET("/validate", handlers.ValidateAPIKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/trackeep/backend/models"
|
"github.com/trackeep/backend/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,13 +37,15 @@ type SessionStore interface {
|
|||||||
|
|
||||||
// RedisSessionStore implements SessionStore using Redis (or fallback to memory)
|
// RedisSessionStore implements SessionStore using Redis (or fallback to memory)
|
||||||
type RedisSessionStore struct {
|
type RedisSessionStore struct {
|
||||||
sessions map[string]*SessionData // Fallback in-memory store
|
redisClient *redis.Client
|
||||||
|
sessions map[string]*SessionData // Fallback in-memory store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSessionStore creates a new session store
|
// NewSessionStore creates a new session store
|
||||||
func NewSessionStore() SessionStore {
|
func NewSessionStore(redisClient *redis.Client) SessionStore {
|
||||||
return &RedisSessionStore{
|
return &RedisSessionStore{
|
||||||
sessions: make(map[string]*SessionData),
|
redisClient: redisClient,
|
||||||
|
sessions: make(map[string]*SessionData),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,32 +53,109 @@ func NewSessionStore() SessionStore {
|
|||||||
func (r *RedisSessionStore) CreateSession(sessionData *SessionData) error {
|
func (r *RedisSessionStore) CreateSession(sessionData *SessionData) error {
|
||||||
sessionData.CreatedAt = time.Now()
|
sessionData.CreatedAt = time.Now()
|
||||||
sessionData.LastActive = time.Now()
|
sessionData.LastActive = time.Now()
|
||||||
|
|
||||||
|
// Try Redis first
|
||||||
|
if r.redisClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sessionJSON, err := json.Marshal(sessionData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal session data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in Redis with 24 hour expiration
|
||||||
|
err = r.redisClient.Set(ctx, "session:"+sessionData.SessionID, sessionJSON, 24*time.Hour).Err()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Fall back to memory if Redis fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to in-memory storage
|
||||||
r.sessions[sessionData.SessionID] = sessionData
|
r.sessions[sessionData.SessionID] = sessionData
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSession retrieves a session by ID
|
// GetSession retrieves a session by ID
|
||||||
func (r *RedisSessionStore) GetSession(sessionID string) (*SessionData, error) {
|
func (r *RedisSessionStore) GetSession(sessionID string) (*SessionData, error) {
|
||||||
|
// Try Redis first
|
||||||
|
if r.redisClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sessionJSON, err := r.redisClient.Get(ctx, "session:"+sessionID).Result()
|
||||||
|
if err == nil {
|
||||||
|
var sessionData SessionData
|
||||||
|
if err := json.Unmarshal([]byte(sessionJSON), &sessionData); err == nil {
|
||||||
|
// Update last active time
|
||||||
|
sessionData.LastActive = time.Now()
|
||||||
|
// Update in Redis
|
||||||
|
updatedJSON, _ := json.Marshal(sessionData)
|
||||||
|
r.redisClient.Set(ctx, "session:"+sessionID, updatedJSON, 24*time.Hour)
|
||||||
|
return &sessionData, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to memory if Redis fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to in-memory storage
|
||||||
if session, exists := r.sessions[sessionID]; exists {
|
if session, exists := r.sessions[sessionID]; exists {
|
||||||
// Update last active time
|
// Update last active time
|
||||||
session.LastActive = time.Now()
|
session.LastActive = time.Now()
|
||||||
return session, nil
|
return session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("session not found")
|
return nil, fmt.Errorf("session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateSession updates an existing session
|
// UpdateSession updates an existing session
|
||||||
func (r *RedisSessionStore) UpdateSession(sessionID string, sessionData *SessionData) error {
|
func (r *RedisSessionStore) UpdateSession(sessionID string, sessionData *SessionData) error {
|
||||||
|
sessionData.LastActive = time.Now()
|
||||||
|
|
||||||
|
// Try Redis first
|
||||||
|
if r.redisClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sessionJSON, err := json.Marshal(sessionData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal session data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.redisClient.Set(ctx, "session:"+sessionID, sessionJSON, 24*time.Hour).Err()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Fall back to memory if Redis fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to in-memory storage
|
||||||
if _, exists := r.sessions[sessionID]; exists {
|
if _, exists := r.sessions[sessionID]; exists {
|
||||||
sessionData.LastActive = time.Now()
|
|
||||||
r.sessions[sessionID] = sessionData
|
r.sessions[sessionID] = sessionData
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("session not found")
|
return fmt.Errorf("session not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteSession removes a session
|
// DeleteSession removes a session
|
||||||
func (r *RedisSessionStore) DeleteSession(sessionID string) error {
|
func (r *RedisSessionStore) DeleteSession(sessionID string) error {
|
||||||
|
// Try Redis first
|
||||||
|
if r.redisClient != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err := r.redisClient.Del(ctx, "session:"+sessionID).Err()
|
||||||
|
if err == nil {
|
||||||
|
// Also remove from memory fallback
|
||||||
|
delete(r.sessions, sessionID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Fall back to memory if Redis fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to in-memory storage
|
||||||
delete(r.sessions, sessionID)
|
delete(r.sessions, sessionID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -93,8 +175,8 @@ func (r *RedisSessionStore) CleanupExpiredSessions() error {
|
|||||||
var sessionStore SessionStore
|
var sessionStore SessionStore
|
||||||
|
|
||||||
// InitSessionStore initializes the session store
|
// InitSessionStore initializes the session store
|
||||||
func InitSessionStore() {
|
func InitSessionStore(redisClient *redis.Client) {
|
||||||
sessionStore = NewSessionStore()
|
sessionStore = NewSessionStore(redisClient)
|
||||||
|
|
||||||
// Start cleanup goroutine
|
// Start cleanup goroutine
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -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{},
|
&AISummary{},
|
||||||
&AITaskSuggestion{},
|
&AITaskSuggestion{},
|
||||||
&UserAISettings{},
|
&UserAISettings{},
|
||||||
|
&UserSearchSettings{},
|
||||||
|
&UserUpdateSettings{},
|
||||||
&AITagSuggestion{},
|
&AITagSuggestion{},
|
||||||
&AIContentGeneration{},
|
&AIContentGeneration{},
|
||||||
&AICodeReview{},
|
&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"`
|
LockedUntil *time.Time `json:"locked_until"`
|
||||||
|
|
||||||
// Privacy Settings
|
// Privacy Settings
|
||||||
ProfileVisibility string `json:"profile_visibility" gorm:"default:public"` // public, private, friends
|
ProfileVisibility string `json:"profile_visibility" gorm:"default:private"` // public, private, friends
|
||||||
ShowEmail bool `json:"show_email" gorm:"default:false"`
|
EmailNotifications bool `json:"email_notifications" gorm:"default:true"`
|
||||||
ShowActivity bool `json:"show_activity" gorm:"default:true"`
|
PushNotifications bool `json:"push_notifications" gorm:"default:true"`
|
||||||
AllowMessages bool `json:"allow_messages" gorm:"default:true"`
|
|
||||||
|
// Social Features
|
||||||
|
ShowEmail bool `json:"show_email" gorm:"default:false"`
|
||||||
|
ShowActivity bool `json:"show_activity" gorm:"default:true"`
|
||||||
|
AllowMessages bool `json:"allow_messages" gorm:"default:true"`
|
||||||
|
|
||||||
// Social Stats
|
// Social Stats
|
||||||
FollowersCount int `json:"followers_count" gorm:"default:0"`
|
FollowersCount int `json:"followers_count" gorm:"default:0"`
|
||||||
|
|||||||
@@ -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,
|
"manifest_version": 3,
|
||||||
"name": "Trackeep Saver",
|
"name": "Trackeep Saver",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Save the current page or a file to your Trackeep account as a bookmark or upload.",
|
"description": "Smart content detection and quick saving for Trackeep with auto-tagging and recommendations.",
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_title": "Save to Trackeep"
|
"default_title": "Save to Trackeep"
|
||||||
@@ -15,11 +15,21 @@
|
|||||||
"storage",
|
"storage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"contextMenus"
|
"contextMenus",
|
||||||
|
"scripting"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
|
"commands": {
|
||||||
|
"quick-save": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Shift+S",
|
||||||
|
"mac": "Command+Shift+S"
|
||||||
|
},
|
||||||
|
"description": "Quick save current page to Trackeep"
|
||||||
|
}
|
||||||
|
},
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
"32": "icons/icon32.png",
|
"32": "icons/icon32.png",
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -85,7 +85,44 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced logo animation */
|
/* Icon Definitions */
|
||||||
|
.icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
fill: white;
|
||||||
|
stroke: white;
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-sm {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-lg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-xl {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* External SVG Icons */
|
||||||
|
img.icon {
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.icon-sm {
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
img.icon-xl {
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
.logo {
|
.logo {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -496,10 +533,85 @@
|
|||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
/* Enhanced Header Styles */
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-save-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-save-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
background: linear-gradient(135deg, #059669 0%, #047857 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-type-indicator {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 8px 0;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags Suggestions */
|
||||||
|
.tags-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-tag {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-tag:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-help {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 4px;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
@@ -741,30 +853,38 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Header -->
|
<!-- Enhanced Header with Smart Detection -->
|
||||||
<header class="header">
|
<div class="header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<div class="logo">T</div>
|
<div class="logo">
|
||||||
<h1 class="title">Trackeep</h1>
|
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/trackeep.svg" alt="Trackeep" class="icon-xl" style="width: 24px; height: 24px; fill: white;" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="title">Trackeep Saver</div>
|
||||||
|
<div id="statusIndicator" class="status-indicator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button id="quickSaveBtn" class="quick-save-btn" title="Quick Save (Ctrl+Shift+S)">
|
||||||
|
<img src="https://www.svgrepo.com/show/521819/save.svg" alt="Save" class="icon-sm" style="width: 14px; height: 14px;" />
|
||||||
|
<span>Quick Save</span>
|
||||||
|
</button>
|
||||||
|
<button id="openOptions" class="options-btn" title="Options">
|
||||||
|
<img src="https://www.svgrepo.com/show/505495/settings.svg" alt="Settings" class="icon" style="width: 16px; height: 16px;" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="options-btn" id="openOptions" title="Settings">
|
<div id="statusText" class="status-text">Loading...</div>
|
||||||
<svg class="icon icon-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<div id="contentTypeIndicator" class="content-type-indicator" style="display: none;"></div>
|
||||||
<circle cx="12" cy="12" r="3"/>
|
|
||||||
<path d="M12 1v6m0 6v6m4.22-13.22l4.24 4.24M1.54 1.54l4.24 4.24M1 12h6m6 0h6"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Status Bar -->
|
<!-- Status Bar -->
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<div class="status-indicator" id="statusIndicator"></div>
|
</div>
|
||||||
<div class="status-text" id="statusText">Checking configuration...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" data-tab="bookmark">
|
<button class="tab active" data-tab="bookmark">
|
||||||
@@ -776,7 +896,6 @@
|
|||||||
<button class="tab" data-tab="file">
|
<button class="tab" data-tab="file">
|
||||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
<polyline points="14,2 14,8 20,8"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
<span>File</span>
|
<span>File</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -787,32 +906,35 @@
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="section-icon">
|
<div class="section-icon">
|
||||||
<svg class="icon-xl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<img src="https://www.svgrepo.com/show/447845/website-click.svg" alt="Website" class="icon-xl" style="width: 24px; height: 24px;" />
|
||||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
||||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 class="section-title">Save Current Page</h2>
|
<h2 class="section-title">Save Current Page</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Enhanced Bookmark Form with Smart Suggestions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bookmarkTitle">Title</label>
|
<label for="bookmarkTitle">Title</label>
|
||||||
<input id="bookmarkTitle" type="text" placeholder="Page title will be auto-filled" />
|
<input type="text" id="bookmarkTitle" placeholder="Page title..." class="form-input" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bookmarkUrl">URL</label>
|
<label for="bookmarkUrl">URL</label>
|
||||||
<input id="bookmarkUrl" type="url" placeholder="https://example.com" />
|
<input type="url" id="bookmarkUrl" placeholder="https://..." class="form-input" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bookmarkDescription">Description</label>
|
<label for="bookmarkDescription">Description</label>
|
||||||
<textarea id="bookmarkDescription" placeholder="Why is this page important? Add notes..."></textarea>
|
<textarea id="bookmarkDescription" placeholder="Optional description..." rows="3" class="form-input"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Smart Tags Suggestions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="bookmarkTags">Tags</label>
|
<label for="bookmarkTags">Tags</label>
|
||||||
<input id="bookmarkTags" type="text" placeholder="reading, development, tutorial" />
|
<div class="tags-container">
|
||||||
|
<input type="text" id="bookmarkTags" placeholder="comma-separated tags..." class="form-input" />
|
||||||
|
<div id="suggestedTags" class="suggested-tags" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tags-help">Click suggested tags to add them automatically</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -841,11 +963,7 @@
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<div class="section-icon">
|
<div class="section-icon">
|
||||||
<svg class="icon-xl" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<img src="https://www.svgrepo.com/show/532805/file-shredder.svg" alt="File" class="icon-xl" style="width: 24px; height: 24px;" />
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
||||||
<polyline points="17,8 12,3 7,8"/>
|
|
||||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h2 class="section-title">Upload File</h2>
|
<h2 class="section-title">Upload File</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -857,14 +975,16 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="fileDescription">Description</label>
|
<label for="fileDescription">Description</label>
|
||||||
<textarea id="fileDescription" placeholder="Describe this file..."></textarea>
|
<textarea id="fileDescription" placeholder="Describe this file..." rows="3"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary btn-block" id="uploadFileBtn">
|
<button class="btn btn-primary btn-block" id="uploadFileBtn">
|
||||||
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
<path d="M21 15v4a2 2 0 0 1-2h-4a2 2 0 0 0 1 2 2v4"/>
|
||||||
<polyline points="17,8 12,3 7,8"/>
|
<polyline points="17,8 12,8 8,8"/>
|
||||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
<polyline points="10,9 9,9 8,9"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Upload File</span>
|
<span>Upload File</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
/* global chrome */
|
/* global chrome, browser */
|
||||||
|
|
||||||
|
// Browser compatibility polyfill
|
||||||
|
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
|
||||||
|
browser = chrome;
|
||||||
|
}
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const statusIndicatorEl = document.getElementById('statusIndicator');
|
const statusIndicatorEl = document.getElementById('statusIndicator');
|
||||||
@@ -23,11 +28,19 @@ const fileInput = document.getElementById('fileInput');
|
|||||||
const fileDescriptionInput = document.getElementById('fileDescription');
|
const fileDescriptionInput = document.getElementById('fileDescription');
|
||||||
const uploadFileBtn = document.getElementById('uploadFileBtn');
|
const uploadFileBtn = document.getElementById('uploadFileBtn');
|
||||||
|
|
||||||
|
// Smart suggestion elements
|
||||||
|
const suggestedTagsContainer = document.getElementById('suggestedTags');
|
||||||
|
const contentTypeIndicator = document.getElementById('contentTypeIndicator');
|
||||||
|
const quickSaveBtn = document.getElementById('quickSaveBtn');
|
||||||
|
|
||||||
let trackeepConfig = {
|
let trackeepConfig = {
|
||||||
apiBaseUrl: '',
|
apiBaseUrl: '',
|
||||||
authToken: ''
|
authToken: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let smartData = null;
|
||||||
|
let isQuickSaveMode = false;
|
||||||
|
|
||||||
// Tab switching functionality
|
// Tab switching functionality
|
||||||
function initTabs() {
|
function initTabs() {
|
||||||
tabBtns.forEach(btn => {
|
tabBtns.forEach(btn => {
|
||||||
@@ -112,7 +125,7 @@ function disableForms(disabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig(callback) {
|
function loadConfig(callback) {
|
||||||
chrome.storage.sync.get(['trackeepApiBaseUrl', 'trackeepAuthToken'], (items) => {
|
browser.storage.sync.get(['trackeepApiBaseUrl', 'trackeepAuthToken'], (items) => {
|
||||||
const apiBaseUrl = (items.trackeepApiBaseUrl || '').trim();
|
const apiBaseUrl = (items.trackeepApiBaseUrl || '').trim();
|
||||||
const authToken = (items.trackeepAuthToken || '').trim();
|
const authToken = (items.trackeepAuthToken || '').trim();
|
||||||
|
|
||||||
@@ -135,7 +148,7 @@ function loadConfig(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectTrackeepDomain(callback) {
|
function detectTrackeepDomain(callback) {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
const tab = tabs && tabs[0];
|
const tab = tabs && tabs[0];
|
||||||
if (!tab || !tab.url) {
|
if (!tab || !tab.url) {
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
@@ -147,9 +160,9 @@ function detectTrackeepDomain(callback) {
|
|||||||
const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost';
|
const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost';
|
||||||
if (isTrackeepDomain && url.protocol === 'https:') {
|
if (isTrackeepDomain && url.protocol === 'https:') {
|
||||||
const candidate = `${url.origin}/api/v1`;
|
const candidate = `${url.origin}/api/v1`;
|
||||||
chrome.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
|
browser.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
|
||||||
if (!items.trackeepApiBaseUrl) {
|
if (!items.trackeepApiBaseUrl) {
|
||||||
chrome.storage.sync.set({ trackeepApiBaseUrl: candidate }, () => {
|
browser.storage.sync.set({ trackeepApiBaseUrl: candidate }, () => {
|
||||||
console.log('Auto-detected Trackeep API URL:', candidate);
|
console.log('Auto-detected Trackeep API URL:', candidate);
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
});
|
});
|
||||||
@@ -167,13 +180,18 @@ function detectTrackeepDomain(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initActiveTab() {
|
function initActiveTab() {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||||
const tab = tabs && tabs[0];
|
const tab = tabs && tabs[0];
|
||||||
if (!tab) return;
|
if (!tab) return;
|
||||||
|
|
||||||
chrome.storage.local.get(['contextMenuData'], (items) => {
|
browser.storage.local.get(['contextMenuData'], (items) => {
|
||||||
const ctx = items.contextMenuData;
|
const ctx = items.contextMenuData;
|
||||||
|
|
||||||
if (ctx && ctx.timestamp && Date.now() - ctx.timestamp < 5000) {
|
if (ctx && ctx.timestamp && Date.now() - ctx.timestamp < 5000) {
|
||||||
|
// Use context menu data
|
||||||
|
smartData = ctx.smartData || null;
|
||||||
|
isQuickSaveMode = ctx.isQuickSave || false;
|
||||||
|
|
||||||
if (ctx.url && !bookmarkUrlInput.value) {
|
if (ctx.url && !bookmarkUrlInput.value) {
|
||||||
bookmarkUrlInput.value = ctx.url;
|
bookmarkUrlInput.value = ctx.url;
|
||||||
}
|
}
|
||||||
@@ -183,8 +201,22 @@ function initActiveTab() {
|
|||||||
if (ctx.selection && !bookmarkDescriptionInput.value) {
|
if (ctx.selection && !bookmarkDescriptionInput.value) {
|
||||||
bookmarkDescriptionInput.value = ctx.selection;
|
bookmarkDescriptionInput.value = ctx.selection;
|
||||||
}
|
}
|
||||||
chrome.storage.local.remove(['contextMenuData']);
|
|
||||||
|
// Apply smart suggestions
|
||||||
|
if (smartData) {
|
||||||
|
applySmartSuggestions(smartData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle quick save mode
|
||||||
|
if (isQuickSaveMode) {
|
||||||
|
handleQuickSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.storage.local.remove(['contextMenuData']);
|
||||||
} else {
|
} else {
|
||||||
|
// Regular tab detection
|
||||||
|
detectAndApplySmartData(tab);
|
||||||
|
|
||||||
if (tab.title && !bookmarkTitleInput.value) {
|
if (tab.title && !bookmarkTitleInput.value) {
|
||||||
bookmarkTitleInput.value = tab.title;
|
bookmarkTitleInput.value = tab.title;
|
||||||
}
|
}
|
||||||
@@ -196,6 +228,98 @@ function initActiveTab() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Smart data detection for regular tab
|
||||||
|
async function detectAndApplySmartData(tab) {
|
||||||
|
try {
|
||||||
|
const info = { linkUrl: tab.url, srcUrl: tab.url };
|
||||||
|
smartData = await detectContentType(info, tab);
|
||||||
|
if (smartData) {
|
||||||
|
applySmartSuggestions(smartData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Smart detection failed:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply smart suggestions to UI
|
||||||
|
function applySmartSuggestions(data) {
|
||||||
|
// Show content type indicator
|
||||||
|
if (contentTypeIndicator) {
|
||||||
|
const typeColors = {
|
||||||
|
video: '#ff0000',
|
||||||
|
social: '#1da1f2',
|
||||||
|
code: '#0969da',
|
||||||
|
article: '#ff6900',
|
||||||
|
documentation: '#6f42c1',
|
||||||
|
news: '#ff4500',
|
||||||
|
shopping: '#ff9500',
|
||||||
|
general: '#6b7280'
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeIcons = {
|
||||||
|
video: '🎥',
|
||||||
|
social: '💬',
|
||||||
|
code: '💻',
|
||||||
|
article: '📝',
|
||||||
|
documentation: '📚',
|
||||||
|
news: '📰',
|
||||||
|
shopping: '🛒',
|
||||||
|
general: '🔗'
|
||||||
|
};
|
||||||
|
|
||||||
|
contentTypeIndicator.innerHTML = `
|
||||||
|
<span style="color: ${typeColors[data.type] || typeColors.general}; font-weight: 600;">
|
||||||
|
${typeIcons[data.type] || typeIcons.general} ${data.type.charAt(0).toUpperCase() + data.type.slice(1)}
|
||||||
|
</span>
|
||||||
|
${data.platform ? `<span style="color: #6b7280; font-size: 0.85em; margin-left: 8px;">• ${data.platform}</span>` : ''}
|
||||||
|
`;
|
||||||
|
contentTypeIndicator.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show suggested tags
|
||||||
|
if (suggestedTagsContainer && data.suggestedTags) {
|
||||||
|
suggestedTagsContainer.innerHTML = '';
|
||||||
|
data.suggestedTags.forEach(tag => {
|
||||||
|
const tagEl = document.createElement('span');
|
||||||
|
tagEl.className = 'suggested-tag';
|
||||||
|
tagEl.textContent = tag;
|
||||||
|
tagEl.onclick = () => addSuggestedTag(tag);
|
||||||
|
suggestedTagsContainer.appendChild(tagEl);
|
||||||
|
});
|
||||||
|
suggestedTagsContainer.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add suggested tag to input
|
||||||
|
function addSuggestedTag(tag) {
|
||||||
|
const currentTags = bookmarkTagsInput.value
|
||||||
|
.split(',')
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(t => t);
|
||||||
|
|
||||||
|
if (!currentTags.includes(tag)) {
|
||||||
|
currentTags.push(tag);
|
||||||
|
bookmarkTagsInput.value = currentTags.join(', ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle quick save
|
||||||
|
function handleQuickSave() {
|
||||||
|
if (isQuickSaveMode && smartData) {
|
||||||
|
// Auto-fill with smart data and save immediately
|
||||||
|
if (smartData.suggestedTags && !bookmarkTagsInput.value) {
|
||||||
|
bookmarkTagsInput.value = smartData.suggestedTags.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (bookmarkUrlInput.value && bookmarkTitleInput.value) {
|
||||||
|
saveBookmark(new Event('submit'));
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveBookmark(event) {
|
async function saveBookmark(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
hideMessage();
|
hideMessage();
|
||||||
@@ -350,10 +474,10 @@ async function uploadFile(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openOptions() {
|
function openOptions() {
|
||||||
if (chrome.runtime.openOptionsPage) {
|
if (browser.runtime.openOptionsPage) {
|
||||||
chrome.runtime.openOptionsPage();
|
browser.runtime.openOptionsPage();
|
||||||
} else {
|
} else {
|
||||||
window.open(chrome.runtime.getURL('options.html'));
|
window.open(browser.runtime.getURL('options.html'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +488,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
openOptionsBtn.addEventListener('click', openOptions);
|
openOptionsBtn.addEventListener('click', openOptions);
|
||||||
|
quickSaveBtn.addEventListener('click', handleQuickSave);
|
||||||
saveBookmarkBtn.addEventListener('click', (e) => {
|
saveBookmarkBtn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
saveBookmark(e);
|
saveBookmark(e);
|
||||||
@@ -373,6 +498,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
uploadFile(e);
|
uploadFile(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcut for quick save
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleQuickSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize configuration and active tab
|
// Initialize configuration and active tab
|
||||||
detectTrackeepDomain(() => {
|
detectTrackeepDomain(() => {
|
||||||
loadConfig(() => {
|
loadConfig(() => {
|
||||||