repr-cli 0.2.16__py3-none-any.whl → 0.2.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- repr/__init__.py +1 -1
- repr/api.py +363 -62
- repr/auth.py +47 -38
- repr/change_synthesis.py +478 -0
- repr/cli.py +4099 -280
- repr/config.py +119 -11
- repr/configure.py +889 -0
- repr/cron.py +419 -0
- repr/dashboard/__init__.py +9 -0
- repr/dashboard/build.py +126 -0
- repr/dashboard/dist/assets/index-BYFVbEev.css +1 -0
- repr/dashboard/dist/assets/index-BrrhyJFO.css +1 -0
- repr/dashboard/dist/assets/index-CcEg74ts.js +270 -0
- repr/dashboard/dist/assets/index-Cerc-iA_.js +377 -0
- repr/dashboard/dist/assets/index-CjVcBW2L.css +1 -0
- repr/dashboard/dist/assets/index-Dfl3mR5E.js +377 -0
- repr/dashboard/dist/favicon.svg +4 -0
- repr/dashboard/dist/index.html +14 -0
- repr/dashboard/manager.py +234 -0
- repr/dashboard/server.py +1298 -0
- repr/db.py +980 -0
- repr/hooks.py +3 -2
- repr/loaders/__init__.py +22 -0
- repr/loaders/base.py +156 -0
- repr/loaders/claude_code.py +287 -0
- repr/loaders/clawdbot.py +313 -0
- repr/loaders/gemini_antigravity.py +381 -0
- repr/mcp_server.py +1196 -0
- repr/models.py +503 -0
- repr/openai_analysis.py +25 -0
- repr/session_extractor.py +481 -0
- repr/storage.py +328 -0
- repr/story_synthesis.py +1296 -0
- repr/templates.py +68 -4
- repr/timeline.py +710 -0
- repr/tools.py +17 -8
- {repr_cli-0.2.16.dist-info → repr_cli-0.2.17.dist-info}/METADATA +48 -10
- repr_cli-0.2.17.dist-info/RECORD +52 -0
- {repr_cli-0.2.16.dist-info → repr_cli-0.2.17.dist-info}/WHEEL +1 -1
- {repr_cli-0.2.16.dist-info → repr_cli-0.2.17.dist-info}/entry_points.txt +1 -0
- repr_cli-0.2.16.dist-info/RECORD +0 -26
- {repr_cli-0.2.16.dist-info → repr_cli-0.2.17.dist-info}/licenses/LICENSE +0 -0
- {repr_cli-0.2.16.dist-info → repr_cli-0.2.17.dist-info}/top_level.txt +0 -0
repr/cron.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cron-based story generation scheduling for repr.
|
|
3
|
+
|
|
4
|
+
Provides predictable, scheduled story generation as an alternative to
|
|
5
|
+
hook-triggered generation. Runs every 4 hours by default, skipping
|
|
6
|
+
if there aren't enough commits in the queue.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .config import REPR_HOME, load_config, save_config
|
|
18
|
+
|
|
19
|
+
# Cron job identifier - used to find/modify our entry
|
|
20
|
+
CRON_MARKER = "# repr-auto-generate"
|
|
21
|
+
CRON_MARKER_PAUSED = "# repr-auto-generate-PAUSED"
|
|
22
|
+
|
|
23
|
+
# Default interval: every 4 hours
|
|
24
|
+
DEFAULT_INTERVAL_HOURS = 4
|
|
25
|
+
|
|
26
|
+
# Minimum commits needed to trigger generation
|
|
27
|
+
DEFAULT_MIN_COMMITS = 3
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_repr_path() -> str:
|
|
31
|
+
"""Get the path to repr executable."""
|
|
32
|
+
# Check common installation paths first
|
|
33
|
+
common_paths = [
|
|
34
|
+
"/usr/local/bin/repr",
|
|
35
|
+
"/opt/homebrew/bin/repr",
|
|
36
|
+
Path.home() / ".local/bin/repr",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
for p in common_paths:
|
|
40
|
+
if Path(p).exists():
|
|
41
|
+
return str(p)
|
|
42
|
+
|
|
43
|
+
# Try to find repr in PATH (avoiding venv paths)
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
["which", "-a", "repr"],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
)
|
|
50
|
+
if result.returncode == 0:
|
|
51
|
+
for path in result.stdout.strip().split('\n'):
|
|
52
|
+
# Skip venv/virtualenv paths
|
|
53
|
+
if 'venv' not in path and '.venv' not in path:
|
|
54
|
+
return path
|
|
55
|
+
# If all are venv, use the first one
|
|
56
|
+
return result.stdout.strip().split('\n')[0]
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# Fallback to python -m repr (using system python, not venv)
|
|
61
|
+
return "python3 -m repr"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_helper_script_path() -> Path:
|
|
65
|
+
"""Get path to the cron helper script."""
|
|
66
|
+
return REPR_HOME / "bin" / "cron-generate.sh"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _create_helper_script(min_commits: int = DEFAULT_MIN_COMMITS) -> Path:
|
|
70
|
+
"""
|
|
71
|
+
Create the helper script for cron job.
|
|
72
|
+
|
|
73
|
+
This is cleaner than embedding complex shell in crontab.
|
|
74
|
+
"""
|
|
75
|
+
repr_path = _get_repr_path()
|
|
76
|
+
log_file = REPR_HOME / "logs" / "cron.log"
|
|
77
|
+
|
|
78
|
+
script_path = _get_helper_script_path()
|
|
79
|
+
script_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
script = f'''#!/bin/bash
|
|
82
|
+
# repr cron helper - auto-generated, do not edit
|
|
83
|
+
# Checks queue and generates stories if enough commits
|
|
84
|
+
|
|
85
|
+
LOG="{log_file}"
|
|
86
|
+
MIN_COMMITS={min_commits}
|
|
87
|
+
|
|
88
|
+
# Get total queue count across all repos
|
|
89
|
+
QUEUE_COUNT=$({repr_path} hooks status --json 2>/dev/null | \\
|
|
90
|
+
python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(r.get('queue_count',0) for r in d))" 2>/dev/null || echo 0)
|
|
91
|
+
|
|
92
|
+
if [ "$QUEUE_COUNT" -ge "$MIN_COMMITS" ]; then
|
|
93
|
+
echo "[$(date -Iseconds)] Generating stories (queue: $QUEUE_COUNT commits)" >> "$LOG"
|
|
94
|
+
{repr_path} generate --local >> "$LOG" 2>&1
|
|
95
|
+
else
|
|
96
|
+
echo "[$(date -Iseconds)] Skipping (queue: $QUEUE_COUNT < $MIN_COMMITS)" >> "$LOG"
|
|
97
|
+
fi
|
|
98
|
+
'''
|
|
99
|
+
|
|
100
|
+
script_path.write_text(script)
|
|
101
|
+
# Make executable
|
|
102
|
+
script_path.chmod(script_path.stat().st_mode | 0o755)
|
|
103
|
+
|
|
104
|
+
return script_path
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _get_cron_command(min_commits: int = DEFAULT_MIN_COMMITS) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Build the cron command.
|
|
110
|
+
|
|
111
|
+
Creates a helper script and returns the path to run it.
|
|
112
|
+
"""
|
|
113
|
+
script_path = _create_helper_script(min_commits)
|
|
114
|
+
return str(script_path)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _get_cron_line(interval_hours: int = DEFAULT_INTERVAL_HOURS, min_commits: int = DEFAULT_MIN_COMMITS) -> str:
|
|
118
|
+
"""Build the full crontab line."""
|
|
119
|
+
cmd = _get_cron_command(min_commits)
|
|
120
|
+
# Run at minute 0 every N hours
|
|
121
|
+
# Format: minute hour day month weekday command
|
|
122
|
+
return f"0 */{interval_hours} * * * {cmd} {CRON_MARKER}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _read_crontab() -> list[str]:
|
|
126
|
+
"""Read current user's crontab."""
|
|
127
|
+
try:
|
|
128
|
+
result = subprocess.run(
|
|
129
|
+
["crontab", "-l"],
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
)
|
|
133
|
+
if result.returncode == 0:
|
|
134
|
+
return result.stdout.strip().split('\n') if result.stdout.strip() else []
|
|
135
|
+
return []
|
|
136
|
+
except Exception:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _write_crontab(lines: list[str]) -> bool:
|
|
141
|
+
"""Write lines to user's crontab."""
|
|
142
|
+
try:
|
|
143
|
+
content = '\n'.join(lines) + '\n' if lines else ''
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
["crontab", "-"],
|
|
146
|
+
input=content,
|
|
147
|
+
capture_output=True,
|
|
148
|
+
text=True,
|
|
149
|
+
)
|
|
150
|
+
return result.returncode == 0
|
|
151
|
+
except Exception:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _find_repr_cron_line(lines: list[str]) -> tuple[int, bool]:
|
|
156
|
+
"""
|
|
157
|
+
Find repr cron line in crontab.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
(index, is_paused) - index is -1 if not found
|
|
161
|
+
"""
|
|
162
|
+
for i, line in enumerate(lines):
|
|
163
|
+
# Check PAUSED first since MARKER is a substring of MARKER_PAUSED
|
|
164
|
+
if CRON_MARKER_PAUSED in line:
|
|
165
|
+
return i, True
|
|
166
|
+
if CRON_MARKER in line:
|
|
167
|
+
return i, False
|
|
168
|
+
return -1, False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_cron_status() -> dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Get current cron job status.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dict with 'installed', 'paused', 'interval_hours', 'next_run' keys
|
|
177
|
+
"""
|
|
178
|
+
lines = _read_crontab()
|
|
179
|
+
idx, is_paused = _find_repr_cron_line(lines)
|
|
180
|
+
|
|
181
|
+
if idx == -1:
|
|
182
|
+
return {
|
|
183
|
+
"installed": False,
|
|
184
|
+
"paused": False,
|
|
185
|
+
"interval_hours": None,
|
|
186
|
+
"cron_line": None,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
line = lines[idx]
|
|
190
|
+
|
|
191
|
+
# Parse interval from cron expression (e.g., "0 */4 * * *")
|
|
192
|
+
interval_hours = DEFAULT_INTERVAL_HOURS
|
|
193
|
+
match = re.search(r'\*/(\d+)', line)
|
|
194
|
+
if match:
|
|
195
|
+
interval_hours = int(match.group(1))
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"installed": True,
|
|
199
|
+
"paused": is_paused,
|
|
200
|
+
"interval_hours": interval_hours,
|
|
201
|
+
"cron_line": line if not is_paused else line.lstrip('# '),
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def install_cron(
|
|
206
|
+
interval_hours: int = DEFAULT_INTERVAL_HOURS,
|
|
207
|
+
min_commits: int = DEFAULT_MIN_COMMITS,
|
|
208
|
+
) -> dict[str, Any]:
|
|
209
|
+
"""
|
|
210
|
+
Install cron job for automatic story generation.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
interval_hours: Hours between runs (default 4)
|
|
214
|
+
min_commits: Minimum commits in queue to trigger generation
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Dict with 'success', 'message', 'already_installed' keys
|
|
218
|
+
"""
|
|
219
|
+
# Ensure log directory exists
|
|
220
|
+
log_dir = REPR_HOME / "logs"
|
|
221
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
222
|
+
|
|
223
|
+
lines = _read_crontab()
|
|
224
|
+
idx, is_paused = _find_repr_cron_line(lines)
|
|
225
|
+
|
|
226
|
+
cron_line = _get_cron_line(interval_hours, min_commits)
|
|
227
|
+
|
|
228
|
+
if idx >= 0:
|
|
229
|
+
if is_paused:
|
|
230
|
+
# Replace paused line with active one
|
|
231
|
+
lines[idx] = cron_line
|
|
232
|
+
if _write_crontab(lines):
|
|
233
|
+
_update_config_cron_status(True, interval_hours, min_commits)
|
|
234
|
+
return {
|
|
235
|
+
"success": True,
|
|
236
|
+
"message": "Cron job resumed and updated",
|
|
237
|
+
"already_installed": False,
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
"success": False,
|
|
241
|
+
"message": "Failed to write crontab",
|
|
242
|
+
"already_installed": False,
|
|
243
|
+
}
|
|
244
|
+
else:
|
|
245
|
+
# Update existing active line
|
|
246
|
+
lines[idx] = cron_line
|
|
247
|
+
if _write_crontab(lines):
|
|
248
|
+
_update_config_cron_status(True, interval_hours, min_commits)
|
|
249
|
+
return {
|
|
250
|
+
"success": True,
|
|
251
|
+
"message": "Cron job updated",
|
|
252
|
+
"already_installed": True,
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
"success": False,
|
|
256
|
+
"message": "Failed to write crontab",
|
|
257
|
+
"already_installed": True,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# Add new line
|
|
261
|
+
lines.append(cron_line)
|
|
262
|
+
if _write_crontab(lines):
|
|
263
|
+
_update_config_cron_status(True, interval_hours, min_commits)
|
|
264
|
+
return {
|
|
265
|
+
"success": True,
|
|
266
|
+
"message": f"Cron job installed (every {interval_hours}h, min {min_commits} commits)",
|
|
267
|
+
"already_installed": False,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"success": False,
|
|
272
|
+
"message": "Failed to write crontab",
|
|
273
|
+
"already_installed": False,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def remove_cron() -> dict[str, Any]:
|
|
278
|
+
"""
|
|
279
|
+
Remove cron job for story generation.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dict with 'success', 'message' keys
|
|
283
|
+
"""
|
|
284
|
+
lines = _read_crontab()
|
|
285
|
+
idx, _ = _find_repr_cron_line(lines)
|
|
286
|
+
|
|
287
|
+
if idx == -1:
|
|
288
|
+
return {
|
|
289
|
+
"success": True,
|
|
290
|
+
"message": "No cron job to remove",
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
lines.pop(idx)
|
|
294
|
+
if _write_crontab(lines):
|
|
295
|
+
_update_config_cron_status(False, None, None)
|
|
296
|
+
return {
|
|
297
|
+
"success": True,
|
|
298
|
+
"message": "Cron job removed",
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
"success": False,
|
|
303
|
+
"message": "Failed to write crontab",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def pause_cron() -> dict[str, Any]:
|
|
308
|
+
"""
|
|
309
|
+
Pause cron job by commenting it out.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Dict with 'success', 'message' keys
|
|
313
|
+
"""
|
|
314
|
+
lines = _read_crontab()
|
|
315
|
+
idx, is_paused = _find_repr_cron_line(lines)
|
|
316
|
+
|
|
317
|
+
if idx == -1:
|
|
318
|
+
return {
|
|
319
|
+
"success": False,
|
|
320
|
+
"message": "No cron job installed",
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if is_paused:
|
|
324
|
+
return {
|
|
325
|
+
"success": True,
|
|
326
|
+
"message": "Cron job already paused",
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Comment out the line, change marker to PAUSED
|
|
330
|
+
line = lines[idx]
|
|
331
|
+
paused_line = "# " + line.replace(CRON_MARKER, CRON_MARKER_PAUSED)
|
|
332
|
+
lines[idx] = paused_line
|
|
333
|
+
|
|
334
|
+
if _write_crontab(lines):
|
|
335
|
+
config = load_config()
|
|
336
|
+
if "cron" not in config:
|
|
337
|
+
config["cron"] = {}
|
|
338
|
+
config["cron"]["paused"] = True
|
|
339
|
+
save_config(config)
|
|
340
|
+
return {
|
|
341
|
+
"success": True,
|
|
342
|
+
"message": "Cron job paused",
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"success": False,
|
|
347
|
+
"message": "Failed to write crontab",
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def resume_cron() -> dict[str, Any]:
|
|
352
|
+
"""
|
|
353
|
+
Resume paused cron job.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
Dict with 'success', 'message' keys
|
|
357
|
+
"""
|
|
358
|
+
lines = _read_crontab()
|
|
359
|
+
idx, is_paused = _find_repr_cron_line(lines)
|
|
360
|
+
|
|
361
|
+
if idx == -1:
|
|
362
|
+
return {
|
|
363
|
+
"success": False,
|
|
364
|
+
"message": "No cron job installed",
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if not is_paused:
|
|
368
|
+
return {
|
|
369
|
+
"success": True,
|
|
370
|
+
"message": "Cron job already active",
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# Uncomment the line, change marker back
|
|
374
|
+
line = lines[idx].lstrip('# ')
|
|
375
|
+
active_line = line.replace(CRON_MARKER_PAUSED, CRON_MARKER)
|
|
376
|
+
lines[idx] = active_line
|
|
377
|
+
|
|
378
|
+
if _write_crontab(lines):
|
|
379
|
+
config = load_config()
|
|
380
|
+
if "cron" not in config:
|
|
381
|
+
config["cron"] = {}
|
|
382
|
+
config["cron"]["paused"] = False
|
|
383
|
+
save_config(config)
|
|
384
|
+
return {
|
|
385
|
+
"success": True,
|
|
386
|
+
"message": "Cron job resumed",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
"success": False,
|
|
391
|
+
"message": "Failed to write crontab",
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _update_config_cron_status(
|
|
396
|
+
installed: bool,
|
|
397
|
+
interval_hours: int | None,
|
|
398
|
+
min_commits: int | None,
|
|
399
|
+
) -> None:
|
|
400
|
+
"""Update cron status in config file."""
|
|
401
|
+
config = load_config()
|
|
402
|
+
|
|
403
|
+
if installed:
|
|
404
|
+
config["cron"] = {
|
|
405
|
+
"installed": True,
|
|
406
|
+
"paused": False,
|
|
407
|
+
"interval_hours": interval_hours,
|
|
408
|
+
"min_commits": min_commits,
|
|
409
|
+
"installed_at": datetime.now().isoformat(),
|
|
410
|
+
}
|
|
411
|
+
else:
|
|
412
|
+
config["cron"] = {
|
|
413
|
+
"installed": False,
|
|
414
|
+
"paused": False,
|
|
415
|
+
"interval_hours": None,
|
|
416
|
+
"min_commits": None,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
save_config(config)
|
repr/dashboard/build.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Build script for the dashboard
|
|
4
|
+
Inlines all CSS and JavaScript into a single HTML file for distribution
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_file(path: Path) -> str:
|
|
12
|
+
"""Read file contents"""
|
|
13
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
14
|
+
return f.read()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def inline_css(html: str, src_dir: Path) -> str:
|
|
18
|
+
"""Replace CSS link tags with inline styles"""
|
|
19
|
+
styles = []
|
|
20
|
+
|
|
21
|
+
# Read all CSS files
|
|
22
|
+
css_files = [
|
|
23
|
+
src_dir / 'styles' / 'main.css',
|
|
24
|
+
src_dir / 'styles' / 'components.css'
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
for css_file in css_files:
|
|
28
|
+
if css_file.exists():
|
|
29
|
+
styles.append(read_file(css_file))
|
|
30
|
+
|
|
31
|
+
# Combine all styles
|
|
32
|
+
combined_styles = '\n'.join(styles)
|
|
33
|
+
|
|
34
|
+
# Replace link tags with inline style
|
|
35
|
+
import re
|
|
36
|
+
pattern = r'<link rel="stylesheet" href="styles/[^"]+\.css">'
|
|
37
|
+
|
|
38
|
+
# Find first occurrence and replace with all styles
|
|
39
|
+
match = re.search(pattern, html)
|
|
40
|
+
if match:
|
|
41
|
+
replacement = f'<style>\n{combined_styles}\n </style>'
|
|
42
|
+
html = html[:match.start()] + replacement + re.sub(pattern, '', html[match.end():])
|
|
43
|
+
|
|
44
|
+
return html
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def inline_js(html: str, src_dir: Path) -> str:
|
|
48
|
+
"""Replace JavaScript script tags with inline scripts"""
|
|
49
|
+
scripts = []
|
|
50
|
+
|
|
51
|
+
# Read all JS files in order
|
|
52
|
+
js_files = [
|
|
53
|
+
src_dir / 'scripts' / 'utils.js',
|
|
54
|
+
src_dir / 'scripts' / 'api.js',
|
|
55
|
+
src_dir / 'scripts' / 'state.js',
|
|
56
|
+
src_dir / 'scripts' / 'theme.js',
|
|
57
|
+
src_dir / 'scripts' / 'keyboard.js',
|
|
58
|
+
src_dir / 'scripts' / 'stories.js',
|
|
59
|
+
src_dir / 'scripts' / 'settings.js',
|
|
60
|
+
src_dir / 'scripts' / 'repos.js'
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
for js_file in js_files:
|
|
64
|
+
if js_file.exists():
|
|
65
|
+
scripts.append(read_file(js_file))
|
|
66
|
+
|
|
67
|
+
# Combine all scripts
|
|
68
|
+
combined_scripts = '\n\n'.join(scripts)
|
|
69
|
+
|
|
70
|
+
# Replace script tags with inline script
|
|
71
|
+
import re
|
|
72
|
+
pattern = r'<script src="scripts/[^"]+\.js"></script>'
|
|
73
|
+
|
|
74
|
+
# Find first occurrence and replace with all scripts
|
|
75
|
+
match = re.search(pattern, html)
|
|
76
|
+
if match:
|
|
77
|
+
replacement = f'<script>\n{combined_scripts}\n </script>'
|
|
78
|
+
html = html[:match.start()] + replacement + re.sub(pattern, '', html[match.end():])
|
|
79
|
+
|
|
80
|
+
return html
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build():
|
|
84
|
+
"""Build the single-file dashboard"""
|
|
85
|
+
# Get paths
|
|
86
|
+
dashboard_dir = Path(__file__).parent
|
|
87
|
+
src_dir = dashboard_dir / 'src'
|
|
88
|
+
src_html = src_dir / 'index.html'
|
|
89
|
+
output_html = dashboard_dir / 'index.html'
|
|
90
|
+
|
|
91
|
+
if not src_html.exists():
|
|
92
|
+
print(f"Error: Source HTML not found at {src_html}")
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
print(f"Building dashboard from {src_dir}...")
|
|
96
|
+
|
|
97
|
+
# Read source HTML
|
|
98
|
+
html = read_file(src_html)
|
|
99
|
+
|
|
100
|
+
# Inline CSS
|
|
101
|
+
print(" Inlining CSS...")
|
|
102
|
+
html = inline_css(html, src_dir)
|
|
103
|
+
|
|
104
|
+
# Inline JavaScript
|
|
105
|
+
print(" Inlining JavaScript...")
|
|
106
|
+
html = inline_js(html, src_dir)
|
|
107
|
+
|
|
108
|
+
# Write output
|
|
109
|
+
print(f" Writing to {output_html}...")
|
|
110
|
+
with open(output_html, 'w', encoding='utf-8') as f:
|
|
111
|
+
f.write(html)
|
|
112
|
+
|
|
113
|
+
# Get file sizes
|
|
114
|
+
src_size = sum(f.stat().st_size for f in src_dir.rglob('*') if f.is_file())
|
|
115
|
+
output_size = output_html.stat().st_size
|
|
116
|
+
|
|
117
|
+
print(f"\n✓ Build complete!")
|
|
118
|
+
print(f" Source: {len(list(src_dir.rglob('*.js')))} JS + {len(list(src_dir.rglob('*.css')))} CSS files ({src_size:,} bytes)")
|
|
119
|
+
print(f" Output: {output_html.name} ({output_size:,} bytes)")
|
|
120
|
+
print(f" Lines: {len(html.splitlines())}")
|
|
121
|
+
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == '__main__':
|
|
126
|
+
exit(build())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg: #f9f7f4;--surface: #ffffff;--surface-hover: #f8fafc;--surface-card: #ffffff;--border: #eaecf0;--text-primary: #111827;--text-secondary: #4b5563;--text-muted: #9ca3af;--accent: #FF5A1F;--accent-hover: #e04f1a;--green: #16a34a;--red: #dc2626;--font-mono: "JetBrains Mono", "Monaco", monospace;--font-serif: "Spectral", "Georgia", serif}[data-theme=dark]{--bg: #0d1117;--surface: #161b22;--surface-hover: #21262d;--surface-card: #0d1117;--border: #30363d;--text-primary: #e6edf3;--text-secondary: #8b949e;--text-muted: #6e7681;--accent: #ff7a33;--accent-hover: #ff8f52;--green: #3fb950;--red: #f85149}*{margin:0;padding:0;box-sizing:border-box}body{font-family:Inter,-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text-primary);line-height:1.4;overflow-y:scroll}.dashboard{display:grid;grid-template-columns:200px 1fr;min-height:100vh;padding:0;gap:0;width:100%;align-items:start}.main-content{grid-column:2;width:100%;min-height:100vh;background:var(--bg);display:flex;justify-content:center;padding:20px}.main-layout{display:grid;grid-template-columns:minmax(auto,600px) 300px;gap:32px;width:100%;max-width:1000px;align-items:start;margin-top:20px}.feed-column{flex:1;max-width:600px;border-right:1px solid var(--border);min-height:100vh;background:var(--surface)}.view-title{font-size:20px;font-weight:700;padding:0 16px;display:none}@media (min-width: 1024px){.view-title{display:block}}.right-sidebar{width:350px;padding:12px 24px;display:none;position:sticky;top:24px;align-self:start;height:-moz-fit-content;height:fit-content;z-index:10}@media (min-width: 1100px){.right-sidebar{display:block}}.widget{background:var(--surface-hover);border-radius:16px;margin-bottom:16px;overflow:hidden;border:1px solid transparent}.widget-title{font-size:19px;font-weight:800;padding:12px 16px;color:var(--text-primary)}.widget-list{display:flex;flex-direction:column}.widget-item{padding:12px 16px;cursor:pointer;transition:background .2s;display:flex;align-items:center;gap:12px}.widget-item:hover{background:#00000008}[data-theme=dark] .widget-item:hover{background:#ffffff08}.widget-item-avatar{width:40px;height:40px;border-radius:8px;background:var(--accent);display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;flex-shrink:0}.widget-item-info{flex:1;min-width:0}.widget-item-name{font-weight:700;font-size:19px;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.widget-item-subtitle{font-size:13px;color:var(--text-muted)}.btn-text{width:100%;padding:16px;text-align:left;background:none;border:none;color:var(--accent);font-size:15px;cursor:pointer}.btn-text:hover{background:#00000008}.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--border);padding:1px}.stat-box{background:var(--surface-hover);padding:16px;text-align:center}.stat-label{font-size:12px;color:var(--text-muted);text-transform:uppercase;margin-bottom:4px}.stat-value{font-size:20px;font-weight:700;color:var(--text-primary)}.right-sidebar-footer{padding:16px;font-size:13px;color:var(--text-muted)}.right-sidebar-footer nav{display:flex;flex-wrap:wrap;gap:8px 12px}.right-sidebar-footer a{color:var(--text-muted);text-decoration:none}.right-sidebar-footer a:hover{text-decoration:underline}.content-header{padding:28px 32px;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:50;display:flex;flex-direction:column}.mobile-menu-toggle{display:none;font-size:24px;cursor:pointer;margin-bottom:12px;width:-moz-fit-content;width:fit-content}.content-header h1{font-family:var(--font-serif);font-size:28px;font-weight:700;color:var(--text-primary);letter-spacing:-.02em}.content-header-subtitle{font-size:14px;color:var(--text-muted);margin-top:4px}.content-body{padding:0}.container{max-width:800px;margin:0 auto;min-height:100vh;background:#fff}.feed-container{max-width:680px;margin:0 auto}.settings-container{max-width:700px;margin:0 auto}.loading-text{color:var(--text-muted);font-size:14px;padding:20px;text-align:center}.empty-state{text-align:center;padding:40px 20px;color:var(--text-muted)}.empty-state-title{font-size:16px;font-weight:500;color:var(--text-secondary);margin-bottom:8px}@keyframes slideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}.post{padding:28px 24px;border-bottom:1px solid var(--border);cursor:pointer;transition:all .2s;background:var(--surface);animation:fadeIn .4s ease-out}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}#view-detail{display:none;width:100%}#view-detail.open{display:block}#view-profile{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:var(--bg);display:none;overflow-y:auto}#view-profile.open{display:block}#view-user{position:fixed;top:0;right:0;bottom:0;left:0;z-index:200;background:var(--bg);display:none;overflow-y:auto}#view-user.open{display:block}#view-profile .sidebar{position:relative;height:100%}#view-profile .main-content{margin-left:0}.mobile-nav{display:none}@media (max-width: 768px){.sidebar{transform:translate(-100%);transition:transform .3s ease;box-shadow:2px 0 10px #0000001a}.sidebar.open{transform:translate(0)}.main-content{margin-left:0!important}.content-header{padding:16px 20px}.date-header{padding:16px 20px 8px}.date-header:before{left:20px;right:20px}.mobile-menu-toggle{display:block}.feed-container,.settings-container{padding:0 10px}.feed-wrapper{border-left:none;border-right:none}.post-card{padding:12px 16px}.post-avatar{width:36px;height:36px;font-size:14px}.post-text{font-size:15px}.post-actions{max-width:100%}.date-header{padding:10px 16px;top:0}.tabs-bar{flex-direction:column;align-items:flex-start;padding:10px 16px;gap:12px}.search-container{width:100%;margin-left:0}.search-input{flex:1;width:100%!important}.sidebar-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:#0000004d;z-index:90}.sidebar-overlay.open{display:block}.mobile-nav{display:flex;position:fixed;bottom:0;left:0;right:0;height:64px;background:var(--surface);border-top:1px solid var(--border);z-index:100;justify-content:space-around;align-items:center;padding-bottom:env(safe-area-inset-bottom)}.mobile-nav-item{display:flex;flex-direction:column;align-items:center;gap:4px;font-size:11px;color:var(--text-muted);cursor:pointer}.mobile-nav-item.active{color:var(--accent)}.mobile-nav-item span:first-child{font-size:20px}body{padding-bottom:64px}}.btn,.sidebar-item,.repo-tab,.repo-action-btn{min-height:44px;display:flex;align-items:center}.date-header{padding:12px 16px;background:var(--surface);border-bottom:1px solid var(--border);position:sticky;top:56px;z-index:40;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#ffffffd9}[data-theme=dark] .date-header{background:#161b22d9}.date-header-label{font-size:15px;font-weight:700;color:var(--text-primary)}#recommended-repos{max-height:195px;overflow:hidden;transition:max-height .3s ease-in-out}#recommended-repos.expanded{max-height:400px;overflow-y:auto}.sidebar{grid-column:1;width:100%;flex-shrink:0;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;position:sticky;top:0;height:100vh;z-index:100;padding:0 12px;overflow-y:auto}.sidebar-header{padding:24px 12px;border-bottom:none}.sidebar-logo{display:flex;align-items:center;gap:12px;padding:12px;border-radius:50%;width:-moz-fit-content;width:fit-content;transition:background .2s}.sidebar-logo:hover{background:#ff5a1f1a}.sidebar-avatar{width:44px;height:44px;border-radius:50%;background:var(--accent);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:20px;color:#fff;box-shadow:0 4px 12px #ff5a1f33}.sidebar-title{font-size:19px;font-weight:800;color:var(--text-primary);letter-spacing:-.5px}.sidebar-subtitle{font-size:12px;color:var(--text-muted)}.sidebar-nav{flex:1;padding:12px 0}.sidebar-section{margin-bottom:32px}.sidebar-section-title{display:none}.sidebar-item{display:flex;align-items:center;gap:16px;padding:12px 16px;border-radius:30px;font-size:19px;font-weight:500;color:var(--text-primary);cursor:pointer;transition:background .2s;margin-bottom:4px;width:-moz-fit-content;width:fit-content}.sidebar-item:hover{background:var(--surface-hover)}.sidebar-item.active{background:transparent;color:var(--text-primary);font-weight:700}.sidebar-item.active .sidebar-icon{color:var(--text-primary)}.sidebar-btn{margin-top:24px;width:100%;height:52px;border-radius:26px;font-size:17px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:12px;box-shadow:0 4px 12px #ff5a1f33}.sidebar-btn-icon{display:none}@media (max-width: 1280px){.sidebar{width:80px;align-items:center}.sidebar-btn{width:52px;padding:0}.sidebar-btn-text,.sidebar-title,.sidebar-subtitle,.sidebar-item span:last-child{display:none}.sidebar-btn-icon{display:block;width:24px;height:24px}.sidebar-item{padding:12px;border-radius:50%}}.sidebar-icon{width:26px;height:26px;display:flex;align-items:center;justify-content:center;color:var(--text-primary)}.sidebar-icon svg{width:26px;height:26px;stroke-width:2px}.sidebar-footer{padding:24px 12px;border-top:none}.tabs-bar{display:flex;align-items:center;background:#ffffffd9;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 16px;position:sticky;top:0;z-index:50;height:53px}[data-theme=dark] .tabs-bar{background:#0d1117d9}.post-card{display:flex;flex-direction:column;gap:2px;padding:24px 20px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s ease;background:var(--surface)}.post-avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px;color:#fff;flex-shrink:0;overflow:hidden;position:relative;top:8px}.post-avatar img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.post-card-header{display:flex;align-items:center;gap:12px}.post-header-info{display:flex;flex-direction:column}.post-header-author{line-height:1.4}.post-header-meta{display:flex;align-items:center;gap:6px;margin-top:2px;color:var(--text-secondary);font-size:13.5px}.post-author{font-weight:600;font-size:15px;color:var(--text-primary)}.post-author:hover{text-decoration:underline}.post-title-inline{color:var(--text-primary);font-weight:500;font-size:15px}.post-badge-container{margin-left:auto}.post-content-full{margin-top:0;margin-left:40px}.post-handle,.post-time,.post-meta-sep{font-size:13px;color:inherit}.post-handle,.category-meta,.post-time,.post-meta-sep{font-size:13.5px;color:inherit;font-weight:400}.category-meta{text-transform:lowercase;letter-spacing:normal}.post-handle:hover{text-decoration:underline}.post-text{font-size:15px;line-height:1.5;margin-top:0}.post-text strong{display:block;font-size:15px;margin-bottom:4px;font-weight:400;letter-spacing:normal;color:var(--text-primary)}.post-text strong:hover{text-decoration:none;cursor:text}.post-actions{display:flex;justify-content:space-between;margin-top:12px;max-width:425px}.post-action{display:flex;align-items:center;gap:8px;color:var(--text-muted);font-size:13px;transition:all .2s}.post-action:hover{color:var(--accent)}.post-action:hover svg{background:#ff5a1f1a;border-radius:50%}.post-action svg{width:18px;height:18px;padding:8px;margin:-8px;box-sizing:content-box}.post-detail{padding:16px;background:var(--surface)}.post-detail-header{display:flex;gap:12px;align-items:center;margin-bottom:20px}.post-detail-author-info{flex:1}.post-detail-title{font-family:var(--font-serif);font-size:28px;font-weight:800;line-height:1.2;margin-bottom:24px;color:var(--text-primary);letter-spacing:-.5px}.post-detail-text{font-size:16px;white-space:pre-wrap;word-wrap:break-word}.detail-list{padding-left:20px;color:var(--text-secondary);font-size:16px;line-height:1.6;margin-bottom:24px}.detail-list li{margin-bottom:8px}.post-detail-footer{margin-top:40px}.post-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.hashtag{font-size:15px;color:var(--text-secondary);font-weight:500;opacity:.8}.hashtag:hover{color:var(--accent);opacity:1;text-decoration:underline}.post-actions{display:flex;justify-content:space-between;max-width:400px;margin-top:12px}.post-action{display:flex;align-items:center;gap:8px;color:var(--text-muted);font-size:13px;padding:8px;margin:-8px;border-radius:50%;transition:all .2s;cursor:pointer}.post-action:hover{color:var(--accent);background:#ff5a1f1a}.post-action svg{width:18px;height:18px}.post-media{margin-top:12px;border:1px solid var(--border);border-radius:16px;overflow:hidden}.post-diagram,.post-output{margin:0;padding:12px 16px;font-family:var(--font-mono);font-size:13px;line-height:1.5;color:var(--text-secondary);background:var(--bg);white-space:pre;overflow-x:auto}.post-output{background:#0d1117;color:#e6edf3}[data-theme=dark] .post-diagram{background:#161b22}.post{padding:24px 0;border-bottom:1px solid var(--border);cursor:pointer}.post-content{flex:1;min-width:0}.name{font-weight:600;font-size:14px;color:var(--text-primary)}.category{font-size:11px;padding:2px 8px;border-radius:4px;font-weight:500;text-transform:uppercase;letter-spacing:.03em}.category.feature{background:#e8f4fd;color:#1e6bb8}.category.bugfix{background:#fde8e8;color:#b91c1c}.category.refactor{background:#fef3cd;color:#92400e}.category.docs{background:#dcfce7;color:#15803d}.category.chore{background:#f3f4f6;color:#4b5563}.category.infra{background:#f3e8ff;color:#7c3aed}[data-theme=dark] .category.feature{background:#1e6bb833;color:#58a6ff}[data-theme=dark] .category.bugfix{background:#b91c1c33;color:#f85149}[data-theme=dark] .category.refactor{background:#92400e33;color:#d29922}[data-theme=dark] .category.docs{background:#15803d33;color:#3fb950}[data-theme=dark] .category.chore{background:#4b556333;color:#8b949e}[data-theme=dark] .category.infra{background:#7c3aed33;color:#a371f7}.time{color:var(--text-muted);font-size:14px}.post-title{font-family:var(--font-serif);font-weight:700;font-size:20px;margin:0 0 8px;line-height:1.4;color:var(--text-primary);letter-spacing:-.01em;transition:color .15s ease}.post-card.selected{background:var(--surface-hover);margin:0 -16px;padding-left:16px;padding-right:16px;border-radius:8px}.skills-inline{display:inline-flex;flex-wrap:wrap;gap:6px}.skill-tag{color:var(--text-muted);font-size:12px;font-weight:500}.skill-tag+.skill-tag:before{content:"·";margin-right:6px}.verified-badge{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;background:var(--accent);color:#fff;border-radius:50%;font-size:9px;margin-left:4px;vertical-align:middle}.diagram-block{background:var(--surface-card);border-left:2px solid var(--border);padding:12px 16px;margin-top:12px;border-radius:0 6px 6px 0;overflow-x:auto}.diagram-block pre{font-family:var(--font-mono);font-size:11px;line-height:1.4;color:var(--text-secondary);margin:0;white-space:pre}.file-metrics-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.file-metric-card{background:var(--surface-card);padding:16px;border-radius:8px;border:1px solid var(--border)}.file-name{font-family:var(--font-mono);font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:12px;word-break:break-all}.metric-row{display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px}.metric-label{color:var(--text-muted)}.metric-val{font-family:var(--font-mono)}.insight-box{background:linear-gradient(135deg,#fff7ed,#ffedd5);border-left:4px solid var(--accent);padding:24px;border-radius:0 8px 8px 0;margin-bottom:24px}[data-theme=dark] .insight-box{background:linear-gradient(135deg,#ff67191a,#ff7a330d)}.insight-label{font-size:11px;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:.1em;margin-bottom:10px}.insight-text{font-family:var(--font-serif);font-size:16px;line-height:1.6;color:var(--text-primary);font-weight:400;font-style:italic}.snippet-block{background:#0d1117;border-radius:8px;overflow:hidden;margin-bottom:16px;font-family:var(--font-mono)}.snippet-header{background:#161b22;padding:10px 16px;font-size:13px;color:#c9d1d9;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center}.snippet-code{padding:16px;font-size:13px;line-height:1.6;color:#e6edf3;overflow-x:auto}.snippet-code pre{margin:0;font-family:var(--font-mono)}.section-subtitle{font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:8px;margin-top:16px}.search-container{display:flex;align-items:center;margin:0 auto;position:relative;width:100%;max-width:600px}.search-icon{position:absolute;left:16px;color:var(--text-muted);pointer-events:none;display:flex;align-items:center}.search-input{background:var(--bg);border:2px solid var(--border);padding:12px 16px 12px 44px;border-radius:12px;font-size:15px;color:var(--text-primary);width:100%;outline:none;transition:all .2s ease}.search-input::-moz-placeholder{color:var(--text-muted)}.search-input::placeholder{color:var(--text-muted)}.search-input:hover{border-color:var(--text-muted)}.search-input:focus{border-color:var(--accent);background:var(--surface);box-shadow:0 0 0 4px #ff7a331a}.search-input:focus+.search-shortcut{opacity:0}.search-shortcut{position:absolute;right:12px;display:flex;align-items:center;gap:2px;padding:4px 8px;background:var(--surface-hover);border:1px solid var(--border);border-radius:6px;font-family:var(--font-mono);font-size:11px;color:var(--text-muted);pointer-events:none;transition:opacity .15s}.search-shortcut-mod:after{content:"⌘"}body.is-windows .search-shortcut-mod:after{content:"Ctrl+"}@media (pointer: coarse){.search-shortcut{display:none}}.highlight{background-color:#ffeb3b66;border-radius:2px;padding:0 1px}[data-theme=dark] .highlight{background-color:#d299224d}.files-row{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:24px}.file-tag{background:var(--surface-card);border:1px solid var(--border);padding:4px 10px;border-radius:4px;font-family:var(--font-mono);font-size:12px;color:var(--text-secondary);display:inline-block}.settings-nav{display:flex;gap:0;border-bottom:1px solid var(--border);background:var(--surface);margin:24px 24px 0;border-radius:8px 8px 0 0}.settings-nav-item{padding:14px 24px;font-size:14px;font-weight:500;color:var(--text-secondary);cursor:pointer;border-bottom:2px solid transparent;transition:all .2s}.settings-nav-item:hover{color:var(--text-primary)}.settings-nav-item.active{color:var(--accent);border-bottom-color:var(--accent)}.settings-content{padding:32px 24px;background:var(--surface);margin:0 24px 24px;border-radius:0 0 8px 8px;border:1px solid var(--border);border-top:none}.settings-container>.settings-content:first-child{margin-top:24px;border-radius:8px;border-top:1px solid var(--border)}.settings-section{margin-bottom:32px}.settings-section-title{font-family:var(--font-serif);font-size:18px;font-weight:600;color:var(--text-primary);margin-bottom:20px;padding-bottom:12px;border-bottom:1px solid var(--border);letter-spacing:-.01em}.settings-group{margin-bottom:20px}.settings-label{display:block;font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:6px}.settings-hint{font-size:12px;color:var(--text-muted);margin-top:4px}.settings-input{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:6px;font-size:14px;color:var(--text-primary);background:var(--bg);transition:border-color .2s}.settings-input:focus{outline:none;border-color:var(--accent)}.settings-select{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:6px;font-size:14px;color:var(--text-primary);background:var(--bg);cursor:pointer}.settings-checkbox-row{display:flex;align-items:center;gap:10px;padding:8px 0}.settings-checkbox{width:18px;height:18px;cursor:pointer}.settings-checkbox-label{font-size:14px;color:var(--text-primary);cursor:pointer}.settings-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}.json-editor{width:100%;min-height:500px;padding:16px;font-family:var(--font-mono);font-size:13px;line-height:1.5;border:1px solid var(--border);border-radius:8px;background:#0d1117;color:#e6edf3;resize:vertical}.json-editor:focus{outline:none;border-color:var(--accent)}.btn{padding:10px 20px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s;border:none}.btn-primary{background:var(--accent);color:#fff}.btn-primary:hover{background:var(--accent-hover)}.btn-primary:disabled{background:#ccc;cursor:not-allowed}.btn-secondary{background:var(--surface-card);color:var(--text-primary);border:1px solid var(--border)}.btn-secondary:hover{background:var(--surface-hover)}.btn-row{display:flex;gap:12px;margin-top:20px}.toast{position:fixed;bottom:24px;right:24px;padding:12px 20px;border-radius:8px;font-size:14px;font-weight:500;z-index:1000;animation:slideIn .3s ease}.toast.success{background:var(--green);color:#fff}.toast.error{background:var(--red);color:#fff}.tags-input{display:flex;flex-wrap:wrap;gap:6px;padding:8px;border:1px solid var(--border);border-radius:6px;background:var(--bg);min-height:42px;align-items:center}.tag-item{display:flex;align-items:center;gap:4px;background:var(--surface-card);border:1px solid var(--border);padding:4px 8px;border-radius:4px;font-size:13px}.tag-remove{cursor:pointer;color:var(--text-muted);font-size:16px;line-height:1}.tag-remove:hover{color:var(--red)}.tags-input input{border:none;outline:none;flex:1;min-width:100px;font-size:14px;background:transparent}.repos-list{display:flex;flex-direction:column;gap:12px}.repo-item{display:flex;align-items:center;gap:16px;padding:16px;background:var(--bg);border:1px solid var(--border);border-radius:8px}.repo-item.paused{opacity:.6}.repo-item.missing{border-color:var(--red);background:#fef2f2}.repo-info{flex:1;min-width:0}.repo-name-row{display:flex;align-items:center;gap:8px;margin-bottom:4px}.repo-name{font-weight:600;font-size:15px;color:var(--text-primary);cursor:pointer;border-bottom:1px dashed transparent;transition:all .15s}.repo-name:hover{color:var(--accent);border-bottom-color:var(--accent)}.repo-edit-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:14px;padding:2px 6px;border-radius:4px;opacity:.4;transition:all .15s}.repo-name-row:hover .repo-edit-btn{opacity:1}.repo-edit-btn:hover{color:var(--accent);opacity:1;background:var(--surface-hover);color:var(--text-primary)}.repo-link{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:4px;color:var(--text-muted);text-decoration:none;transition:all .15s;font-size:13px;line-height:1}.repo-link:hover{color:var(--accent);background:var(--surface-hover)}.repo-link-icon{display:inline-block;transform:translateY(-1px)}.repo-path{font-family:var(--font-mono);font-size:12px;color:var(--text-muted);word-break:break-all}.repo-status{display:flex;align-items:center;gap:8px;margin-top:6px}.repo-badge{font-size:11px;padding:2px 8px;border-radius:12px;font-weight:500}.repo-badge.active{background:#dcfce7;color:#15803d}.repo-badge.paused{background:#fef3cd;color:#92400e}.repo-badge.missing{background:#fde8e8;color:#b91c1c}.repo-badge.hook{background:#e8f4fd;color:#1e6bb8}[data-theme=dark] .repo-badge.active{background:#15803d33;color:#3fb950}[data-theme=dark] .repo-badge.paused{background:#92400e33;color:#d29922}[data-theme=dark] .repo-badge.missing{background:#b91c1c33;color:#f85149}[data-theme=dark] .repo-badge.hook{background:#1e6bb833;color:#58a6ff}.repo-actions{display:flex;gap:8px}.repo-action-btn{padding:6px 12px;font-size:12px;font-weight:500;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text-secondary);cursor:pointer;transition:all .15s}.repo-action-btn:hover{background:var(--surface-hover);color:var(--text-primary)}.repo-action-btn.danger:hover{background:#fde8e8;border-color:var(--red);color:var(--red)}.cron-status-box{padding:20px;background:var(--bg);border:1px solid var(--border);border-radius:8px}.cron-status-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0}.cron-status-label{font-size:14px;color:var(--text-secondary)}.cron-status-value{font-size:14px;font-weight:500;color:var(--text-primary)}.cron-status-value.active{color:var(--green)}.cron-status-value.inactive{color:var(--text-muted)}.cron-status-value.paused{color:#92400e}.cli-hint{margin-bottom:16px}.cli-hint code{display:block;font-family:var(--font-mono);font-size:13px;background:#1a1a1a;color:#e6edf3;padding:12px 16px;border-radius:6px;margin-bottom:6px}.skeleton{background:var(--surface-hover);background:linear-gradient(90deg,var(--surface-hover) 25%,var(--border) 50%,var(--surface-hover) 75%);background-size:200% 100%;animation:shimmer 1.5s infinite linear;border-radius:4px}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.skeleton-card{display:flex;gap:12px;padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:16px}.skeleton-card .skeleton-avatar{width:44px;height:44px;border-radius:50%;flex-shrink:0}.skeleton-body{flex:1}.skeleton-header{height:14px;width:40%;margin-bottom:8px}.skeleton-title{height:18px;width:80%;margin-bottom:8px}.skeleton-text{height:14px;margin-bottom:6px;width:100%}.skeleton-post{padding:28px 24px;border-bottom:1px solid var(--border)}.skeleton-avatar{width:20px}.date-header{padding:24px 24px 8px;margin-top:8px;border-bottom:1px solid var(--border)}.date-header:first-child{margin-top:0;padding-top:16px}.date-header-label{font-size:12px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#0006;display:flex;align-items:center;justify-content:center;z-index:1000;opacity:0;visibility:hidden;transition:all .2s ease;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.modal-overlay.open{opacity:1;visibility:visible}.modal-container{background:var(--surface);border-radius:12px;width:100%;max-width:480px;box-shadow:0 20px 25px -5px #0000001a,0 10px 10px -5px #0000000a;transform:translateY(20px);transition:all .3s cubic-bezier(.34,1.56,.64,1);overflow:hidden;border:1px solid var(--border)}.modal-overlay.open .modal-container{transform:translateY(0)}.modal-header{padding:20px 24px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}.modal-title{font-family:var(--font-serif);font-size:18px;font-weight:700;color:var(--text-primary)}.modal-close{background:none;border:none;font-size:24px;color:var(--text-muted);cursor:pointer;padding:4px;line-height:1;border-radius:4px}.modal-close:hover{background:var(--surface-hover);color:var(--text-primary)}.modal-body{padding:24px}.modal-footer{padding:16px 24px;border-top:1px solid var(--border);background:var(--bg);display:flex;justify-content:flex-end;gap:12px}.editable-field-container{position:relative;display:flex;align-items:center}.editable-field-container .settings-input{padding-right:40px}.editable-field-icon{position:absolute;right:12px;color:var(--text-muted);pointer-events:none;font-size:14px}.profile-container{max-width:600px;margin:0 auto;min-height:100vh;background:var(--surface);border-left:1px solid var(--border);border-right:1px solid var(--border);position:relative}.profile-header-nav{position:sticky;top:0;z-index:100;background:#ffffffd9;-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid transparent;padding:0 16px;height:53px;display:flex;align-items:center;gap:20px}[data-theme=dark] .profile-header-nav{background:#0d1117d9}.back-button{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s ease;color:var(--text-primary);margin-left:-8px;background:transparent}.back-button:hover{background:var(--surface-hover);transform:scale(1.05)}.back-button:active{background:var(--surface-active);transform:scale(.95)}.profile-nav-title{font-weight:700;font-size:17px;color:var(--text-primary)}.profile-cover{height:200px;width:100%;background:linear-gradient(135deg,#ff9a9e,#fecfef 99%,#fecfef);background-size:cover;background-position:center}[data-theme=dark] .profile-cover{background:linear-gradient(135deg,#4b2c34,#3e1f2b)}.profile-content{position:relative}.profile-top-section{padding:12px 16px;display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:12px}.profile-avatar-wrapper{margin-top:-15%;padding:4px;background:var(--surface);border-radius:50%;display:inline-block}.profile-avatar-large{width:134px;height:134px;border-radius:50%;background:var(--green);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:56px;color:#fff;border:4px solid var(--surface);position:relative;overflow:hidden}.profile-avatar-large img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.profile-actions{display:flex;gap:8px}.btn-sm{padding:6px 16px;font-size:14px;border-radius:20px;border-width:1px}.profile-details{padding:0 16px 16px}.profile-name-row{display:flex;align-items:center;gap:4px;margin-bottom:2px}.profile-name-row h1{font-size:20px;font-weight:800;color:var(--text-primary);margin:0;line-height:1.2}.verified-badge-large{display:inline-flex;align-items:center;justify-content:center;color:var(--accent);width:20px;height:20px}.profile-handle{font-size:15px;color:var(--text-secondary);margin-bottom:12px}.profile-bio{font-size:15px;color:var(--text-primary);line-height:1.5;margin-bottom:12px;white-space:pre-wrap}.profile-metadata-row{display:flex;flex-wrap:wrap;gap:12px;margin-bottom:12px;align-items:center}.profile-pill{display:inline-flex;align-items:center;gap:6px;background:var(--surface-card);padding:4px 12px;border-radius:16px;font-size:13px;color:var(--text-secondary);font-weight:500}.profile-pill:hover{background:var(--surface-hover);color:var(--text-primary);cursor:pointer}.pill-icon{font-size:14px}.profile-pill-count{font-size:13px;color:var(--text-secondary);font-weight:500;background:var(--surface-card);padding:4px 12px;border-radius:16px}.profile-stats-row{display:flex;gap:20px;font-size:14px;color:var(--text-secondary)}.stat-item strong{color:var(--text-primary);font-weight:700}.profile-tabs{display:flex;border-bottom:1px solid var(--border);margin-top:16px}.profile-tab{flex:1;text-align:center;padding:16px 0;font-size:15px;font-weight:500;color:var(--text-secondary);cursor:pointer;position:relative;transition:all .2s}.profile-tab:hover{background:var(--surface-hover);color:var(--text-primary)}.profile-tab.active{color:var(--text-primary);font-weight:700}.profile-tab.active:after{content:"";position:absolute;bottom:0;left:50%;transform:translate(-50%);width:56px;height:4px;background:var(--accent);border-radius:2px}.auth-section{padding:12px;border-top:1px solid var(--border)}.auth-connect-btn{width:100%;display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--accent);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}.auth-connect-btn:hover{opacity:.9;transform:scale(1.02)}.auth-connect-btn .auth-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center}.auth-connect-btn .auth-icon svg{width:18px;height:18px}.auth-connected{display:flex;align-items:center;justify-content:space-between;gap:8px}.auth-user{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.auth-user .auth-icon{width:32px;height:32px;min-width:32px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff}.auth-user .auth-icon svg{width:18px;height:18px}.auth-name{font-size:13px;font-weight:500;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.auth-logout-btn{width:32px;height:32px;min-width:32px;padding:0;background:transparent;border:none;border-radius:50%;color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}.auth-logout-btn:hover{background:var(--surface-hover);color:var(--red)}.auth-logout-btn svg{width:18px;height:18px}.auth-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s}.auth-modal.open{opacity:1;visibility:visible}.auth-modal-content{pointer-events:auto;background:var(--surface);border-radius:16px;width:100%;max-width:400px;margin:20px;position:relative;box-shadow:0 20px 40px #0003;animation:modalSlideIn .2s ease-out}@keyframes modalSlideIn{0%{opacity:0;transform:translateY(-20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}.auth-modal-close{position:absolute;top:16px;right:16px;width:32px;height:32px;padding:0;background:transparent;border:none;border-radius:50%;color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}.auth-modal-close:hover{background:var(--surface-hover);color:var(--text-primary)}.auth-modal-close svg{width:20px;height:20px}.auth-modal-header{padding:32px 32px 24px;text-align:center;border-bottom:1px solid var(--border)}.auth-modal-icon{width:56px;height:56px;margin:0 auto 16px;background:var(--accent);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff}.auth-modal-icon svg{width:28px;height:28px}.auth-modal-header h2{font-size:20px;font-weight:700;color:var(--text-primary);margin-bottom:8px}.auth-modal-header p{font-size:14px;color:var(--text-secondary)}.auth-modal-body{padding:24px 32px;text-align:center}.auth-code{font-size:32px;font-weight:700;font-family:SF Mono,Monaco,Cascadia Code,monospace;letter-spacing:4px;color:var(--text-primary);background:var(--surface-hover);padding:16px 24px;border-radius:12px;margin-bottom:20px;-webkit-user-select:all;-moz-user-select:all;user-select:all}.auth-verify-btn{display:inline-flex;align-items:center;gap:8px;padding:12px 24px;background:var(--accent);color:#fff;text-decoration:none;border-radius:24px;font-size:14px;font-weight:600;transition:all .2s;margin-bottom:20px}.auth-verify-btn:hover{opacity:.9;transform:scale(1.02)}.auth-verify-btn svg{width:16px;height:16px}.auth-modal-status{display:flex;align-items:center;justify-content:center;gap:10px;font-size:13px;color:var(--text-secondary)}.auth-spinner{width:16px;height:16px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.auth-modal-footer{padding:16px 32px 24px;display:flex;gap:12px;justify-content:center}.auth-cancel-btn{padding:10px 24px;background:transparent;border:1px solid var(--border);border-radius:24px;color:var(--text-secondary);font-size:14px;cursor:pointer;transition:all .2s}.auth-cancel-btn:hover{background:var(--surface-hover);color:var(--text-primary)}.auth-retry-btn{padding:10px 24px;background:var(--accent);border:none;border-radius:24px;color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}.auth-retry-btn:hover{opacity:.9}.auth-modal-icon-error{background:var(--red)}
|