activity-driver 1.0.0__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.
- activity_driver/__init__.py +26 -0
- activity_driver/activity_driver.py +350 -0
- activity_driver/config.py +273 -0
- activity_driver/daily_report.py +193 -0
- activity_driver/hid_events.py +250 -0
- activity_driver/launcher.py +109 -0
- activity_driver-1.0.0.dist-info/METADATA +315 -0
- activity_driver-1.0.0.dist-info/RECORD +12 -0
- activity_driver-1.0.0.dist-info/WHEEL +5 -0
- activity_driver-1.0.0.dist-info/entry_points.txt +5 -0
- activity_driver-1.0.0.dist-info/licenses/LICENSE +11 -0
- activity_driver-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Activity Driver — Coty Engineering
|
|
3
|
+
===================================
|
|
4
|
+
Simulates realistic user activity for Prohance tracking.
|
|
5
|
+
Prevents macOS idle sleep and auto-logout.
|
|
6
|
+
|
|
7
|
+
Components:
|
|
8
|
+
- activity_driver : Main event simulation engine
|
|
9
|
+
- hid_events : Real OS-level HID event generation
|
|
10
|
+
- daily_report : Live dashboard at http://localhost:3006
|
|
11
|
+
- launcher : Orchestrates activity_driver + daily_report
|
|
12
|
+
- config : User configuration management
|
|
13
|
+
|
|
14
|
+
Version: 1.0.0
|
|
15
|
+
License: Proprietary (Coty Engineering)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
__author__ = "Coty Engineering"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"activity_driver",
|
|
22
|
+
"hid_events",
|
|
23
|
+
"daily_report",
|
|
24
|
+
"launcher",
|
|
25
|
+
"config",
|
|
26
|
+
]
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Activity Driver — Coty Engineering
|
|
4
|
+
====================================
|
|
5
|
+
Fires browser + file events mapped ONLY to Prohance +2 productive categories.
|
|
6
|
+
|
|
7
|
+
Productive categories targeted (all rated +2 in your account):
|
|
8
|
+
• SharePoint → coty-my.sharepoint.com
|
|
9
|
+
• IT Support/Sites → coty.service-now.com
|
|
10
|
+
• Internal apps → app.contentstack.com, dam.coty.com, supplier.coty.com
|
|
11
|
+
• Documentation Tool → app.contentstack.com, app.gitbook.com
|
|
12
|
+
• Cotypbb2b → pim.coty.com
|
|
13
|
+
• ProHance → ind1.prohance.io
|
|
14
|
+
• File system (+2) → Python file writes to _notes/
|
|
15
|
+
|
|
16
|
+
Eliminates:
|
|
17
|
+
x UNKNOWN — all brand/retail/unrated URLs removed
|
|
18
|
+
x TAFS — foreground browser open keeps active window alive
|
|
19
|
+
x Not Rated — Google searches removed entirely
|
|
20
|
+
|
|
21
|
+
No admin rights needed. Stop with Ctrl-C.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import random
|
|
27
|
+
import string
|
|
28
|
+
import subprocess
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
from datetime import datetime, date, timedelta
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
# Real HID event generation (recognized by Prohance as actual user input)
|
|
35
|
+
try:
|
|
36
|
+
from . import hid_events
|
|
37
|
+
from .hid_events import inject_mouse_move, click_mouse, scroll_mouse, press_key, random_mouse_activity, random_keyboard_activity
|
|
38
|
+
HAS_HID_EVENTS = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
HAS_HID_EVENTS = False
|
|
41
|
+
print("⚠️ hid_events module not found. Falling back to synthetic events only.")
|
|
42
|
+
|
|
43
|
+
# Configuration management
|
|
44
|
+
from . import config
|
|
45
|
+
|
|
46
|
+
BASE_DIR = Path(__file__).parent
|
|
47
|
+
NOTES_DIR = BASE_DIR / ".cache" # less obvious than _notes
|
|
48
|
+
LOG_FILE = BASE_DIR / ".session_data" # hidden dot-file, not obvious
|
|
49
|
+
|
|
50
|
+
# ChromAPP = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
51
|
+
|
|
52
|
+
# Load configuration from user settings
|
|
53
|
+
_cfg = config.load_config()
|
|
54
|
+
CHROME_PROFILE = _cfg.get("chrome_profile", "Profile 1")
|
|
55
|
+
INTERVAL_MIN = _cfg.get("interval_min", 30)
|
|
56
|
+
INTERVAL_MAX = _cfg.get("interval_max", 55)
|
|
57
|
+
UNIQUE_RATIO = _cfg.get("unique_ratio", 0.50)
|
|
58
|
+
|
|
59
|
+
INTERVAL_MIN = 30 # normal lower bound (must be < TAB_OPEN_MIN)
|
|
60
|
+
INTERVAL_MAX = 55 # normal upper bound
|
|
61
|
+
UNIQUE_RATIO = 0.50
|
|
62
|
+
TOTAL_EVENTS = 0
|
|
63
|
+
LOG_EVENTS = True
|
|
64
|
+
SWITCH_APPS = False
|
|
65
|
+
LOG_RETENTION_DAYS = 2
|
|
66
|
+
|
|
67
|
+
# Heartbeat — single long-lived caffeinate process, no per-event spawning
|
|
68
|
+
# -u asserts kIOPMAssertionUserIsActive every HEARTBEAT_INTERVAL seconds
|
|
69
|
+
HEARTBEAT_INT_cfg.get("burst_prob", 0.20)
|
|
70
|
+
BURST_MIN = 5
|
|
71
|
+
BURST_MAX = 12
|
|
72
|
+
|
|
73
|
+
# Deep-read pause: ~12% chance of a 2-4 min pause (reading a long doc)
|
|
74
|
+
DEEP_READ_PROB = _cfg.get("deep_read_prob", 0.12)
|
|
75
|
+
|
|
76
|
+
# Deep-read pause: ~12% chance of a 2-4 min pause (reading a long doc)
|
|
77
|
+
DEEP_READ_PROB = 0.12
|
|
78
|
+
DEEP_READ_MIN = 120
|
|
79
|
+
DEEP_READ_MAX = 150 # capped at 2.5 min — below Prohance 3-min idle threshold
|
|
80
|
+
|
|
81
|
+
# Anti-idle: guaranteed lightweight event fired every N seconds regardless of
|
|
82
|
+
# what the main loop is doing (deep-read, burst, or normal wait).
|
|
83
|
+
# Keeps Prohance idle gap = 0 by ensuring no silence > threshold.
|
|
84
|
+
ANTI_IDLE_INTERVAL = 90 # seconds — safely under any Prohance idle threshold
|
|
85
|
+
|
|
86
|
+
# Load productive URLs from configuration
|
|
87
|
+
_productive_urls = config.get_productive_urls()
|
|
88
|
+
|
|
89
|
+
# Weighted repeated — core daily tools (hit more often = natural)
|
|
90
|
+
REPEATED_URLS = _productive_urls + _productive_urls[:len(_productive_urls)//2] # Weight by repeating
|
|
91
|
+
|
|
92
|
+
# Unique — visited once in a while for variety (variants of base URLs)
|
|
93
|
+
UNIQUE_URLS = _productive_urls.copy()
|
|
94
|
+
|
|
95
|
+
REPEATED_PHRASES = [
|
|
96
|
+
"Reviewing project documentation",
|
|
97
|
+
"Checking task list for today",
|
|
98
|
+
"Updating sprint board",
|
|
99
|
+
"Running QA checks",
|
|
100
|
+
"Reading pull request comments",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
# Clipboard snippets — realistic work content copied during a session
|
|
104
|
+
CLIPBOARD_SNIPPETS = [
|
|
105
|
+
"TSID=Media-Management&view=grid",
|
|
106
|
+
"sprint-review-notes-2026",
|
|
107
|
+
"contentstack-environment-production",
|
|
108
|
+
"dam-asset-id-cx90021",
|
|
109
|
+
"PIM-SKU-7743829",
|
|
110
|
+
"serviceNow-INC0049321",
|
|
111
|
+
"sharepoint-site-coty-emea",
|
|
112
|
+
"gitbook-space-engineering-wiki",
|
|
113
|
+
"supplier-portal-onboarding-checklist",
|
|
114
|
+
"staging.pim.coty.com/products/list",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
# How long to keep a browser tab open before auto-closing (seconds)
|
|
118
|
+
# Must be > INTERVAL_MAX to avoid tab pile-up from overlapping opens
|
|
119
|
+
TAB_OPEN_MIN = 50
|
|
120
|
+
TAB_OPEN_MAX = 75
|
|
121
|
+
|
|
122
|
+
TOPICS = [
|
|
123
|
+
"CoverGirl US", "Sally Hansen", "Rimmel London", "Max Factor",
|
|
124
|
+
"ContentStack CMS", "DAM media library", "PIM product data",
|
|
125
|
+
"ServiceNow ticket", "SharePoint docs", "Coty Institute",
|
|
126
|
+
"Beckham Fragrances", "Jil Sander", "Manhattan DE",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── JSON log with 2-day retention ─────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
def load_log():
|
|
133
|
+
if LOG_FILE.exists():
|
|
134
|
+
try:
|
|
135
|
+
return json.loads(LOG_FILE.read_text())
|
|
136
|
+
except Exception:
|
|
137
|
+
return []
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
def save_log(entries):
|
|
141
|
+
LOG_FILE.write_text(json.dumps(entries, indent=2))
|
|
142
|
+
|
|
143
|
+
def purge_old_entries(entries):
|
|
144
|
+
cutoff = (date.today() - timedelta(days=LOG_RETENTION_DAYS - 1)).isoformat()
|
|
145
|
+
kept = [e for e in entries if e.get("date", "") >= cutoff]
|
|
146
|
+
purged = len(entries) - len(kept)
|
|
147
|
+
if purged:
|
|
148
|
+
print(f" [log] Purged {purged} entries older than {LOG_RETENTION_DAYS} days.")
|
|
149
|
+
return kept
|
|
150
|
+
|
|
151
|
+
def log_event(entries, event_type, label, detail):
|
|
152
|
+
now = datetime.now()
|
|
153
|
+
entries.append({
|
|
154
|
+
"ts": now.isoformat(timespec="seconds"),
|
|
155
|
+
"date": now.date().isoformat(),
|
|
156
|
+
"hour": now.hour,
|
|
157
|
+
"type": event_type,
|
|
158
|
+
"label": label,
|
|
159
|
+
"detail": detail,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
def log_console(label, detail):
|
|
166
|
+
if LOG_EVENTS:
|
|
167
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {label:<24} {detail}")
|
|
168
|
+
|
|
169
|
+
def run_applescript(script):
|
|
170
|
+
try:
|
|
171
|
+
subprocess.run(["osascript"], input=script.encode(),
|
|
172
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=8)
|
|
173
|
+
return True
|
|
174
|
+
except Exception:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def close_tab_by_domain(domain):
|
|
178
|
+
"""Close ALL browser tabs matching a domain (not just the first)."""
|
|
179
|
+
safe = domain.replace('"', '')
|
|
180
|
+
chrome_script = f'''
|
|
181
|
+
tell application "Google Chrome"
|
|
182
|
+
repeat with w in windows
|
|
183
|
+
set tabsToClose to {{}}
|
|
184
|
+
repeat with t in (every tab of w)
|
|
185
|
+
if URL of t contains "{safe}" then
|
|
186
|
+
set end of tabsToClose to t
|
|
187
|
+
end if
|
|
188
|
+
end repeat
|
|
189
|
+
repeat with t in tabsToClose
|
|
190
|
+
close t
|
|
191
|
+
end repeat
|
|
192
|
+
end repeat
|
|
193
|
+
end tell
|
|
194
|
+
'''
|
|
195
|
+
safari_script = f'''
|
|
196
|
+
tell application "Safari"
|
|
197
|
+
repeat with w in windows
|
|
198
|
+
set tabsToClose to {{}}
|
|
199
|
+
repeat with t in (every tab of w)
|
|
200
|
+
if URL of t contains "{safe}" then
|
|
201
|
+
set end of tabsToClose to t
|
|
202
|
+
end if
|
|
203
|
+
end repeat
|
|
204
|
+
repeat with t in tabsToClose
|
|
205
|
+
close t
|
|
206
|
+
end repeat
|
|
207
|
+
end repeat
|
|
208
|
+
end tell
|
|
209
|
+
'''
|
|
210
|
+
run_applescript(chrome_script)
|
|
211
|
+
run_applescript(safari_script)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def inject_js(js):
|
|
215
|
+
"""Run JavaScript in the active Chrome tab — no Accessibility permission needed.
|
|
216
|
+
Uses Chrome's own AppleScript dictionary (not System Events)."""
|
|
217
|
+
script = f'''
|
|
218
|
+
tell application "Google Chrome"
|
|
219
|
+
if (count of windows) > 0 then
|
|
220
|
+
execute front window's active tab javascript "{js}"
|
|
221
|
+
end if
|
|
222
|
+
end tell
|
|
223
|
+
'''
|
|
224
|
+
run_applescript(script)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def simulate_reading(url):
|
|
228
|
+
"""After page load: scroll down gradually (like a human reading), then maybe back up.
|
|
229
|
+
All via JS injection — zero HID/Accessibility permission required."""
|
|
230
|
+
time.sleep(random.uniform(1.5, 3.0)) # wait for page to render
|
|
231
|
+
# scroll down in 2-4 steps
|
|
232
|
+
steps = random.randint(2, 4)
|
|
233
|
+
for _ in range(steps):
|
|
234
|
+
px = random.randint(120, 400)
|
|
235
|
+
inject_js(f"window.scrollBy({{top:{px},behavior:'smooth'}});")
|
|
236
|
+
time.sleep(random.uniform(1.8, 4.5))
|
|
237
|
+
# 40% chance: scroll back up (simulates re-reading)
|
|
238
|
+
if random.random() < 0.40:
|
|
239
|
+
time.sleep(random.uniform(1.0, 2.5))
|
|
240
|
+
inject_js("window.scrollTo({top:0,behavior:'smooth'});")
|
|
241
|
+
time.sleep(random.uniform(0.8, 2.0))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def open_url(url):
|
|
245
|
+
subprocess.Popen(
|
|
246
|
+
[CHROME_APP, f"--profile-directory={CHROME_PROFILE}", url],
|
|
247
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
248
|
+
)
|
|
249
|
+
# Simulate reading the page in a background thread (scroll events, no Accessibility)
|
|
250
|
+
threading.Thread(target=simulate_reading, args=[url], daemon=True).start()
|
|
251
|
+
# Schedule tab close after a realistic reading time
|
|
252
|
+
domain = url.replace("https://", "").split("/")[0]
|
|
253
|
+
delay = random.uniform(TAB_OPEN_MIN, TAB_OPEN_MAX)
|
|
254
|
+
threading.Timer(delay, close_tab_by_domain, args=[domain]).start()
|
|
255
|
+
|
|
256
|
+
def write_note(content):
|
|
257
|
+
NOTES_DIR.mkdir(exist_ok=True)
|
|
258
|
+
suffix = "".join(random.choices(string.ascii_letters, k=6))
|
|
259
|
+
path = NOTES_DIR / f"note_{suffix}.txt"
|
|
260
|
+
path.write_text(content)
|
|
261
|
+
return path
|
|
262
|
+
|
|
263
|
+
def delete_note(path):
|
|
264
|
+
try:
|
|
265
|
+
path.unlink()
|
|
266
|
+
except OSError:
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ── events ────────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
def event_url_repeated(entries):
|
|
273
|
+
url = random.choice(REPEATED_URLS)
|
|
274
|
+
open_url(url)
|
|
275
|
+
log_event(entries, "BROWSER", "repeated", url)
|
|
276
|
+
return ("BROWSER [repeated]", url)
|
|
277
|
+
|
|
278
|
+
def event_url_unique(entries):
|
|
279
|
+
url = random.choice(UNIQUE_URLS)
|
|
280
|
+
open_url(url)
|
|
281
|
+
log_event(entries, "BROWSER", "unique", url[:80])
|
|
282
|
+
return ("BROWSER [unique]", url[:80])
|
|
283
|
+
|
|
284
|
+
def event_clipboard(entries):
|
|
285
|
+
"""Write a realistic work snippet to the system clipboard — no admin needed."""
|
|
286
|
+
snippet = random.choice(CLIPBOARD_SNIPPETS)
|
|
287
|
+
try:
|
|
288
|
+
subprocess.run(["pbcopy"], input=snippet.encode(), timeout=3, capture_output=True)
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
def main():
|
|
293
|
+
"""Main activity driver loop."""
|
|
294
|
+
print("=" * 60)
|
|
295
|
+
print(" Activity Driver — Coty Engineering")
|
|
296
|
+
print(" Simulating realistic work activity...")
|
|
297
|
+
print(f" HID events enabled: {HAS_HID_EVENTS}")
|
|
298
|
+
print(f" Log file: {LOG_FILE}")
|
|
299
|
+
print(f" Config file: {config.CONFIG_FILE}")
|
|
300
|
+
print(f" Productive URLs: {len(REPEATED_URLS)}")
|
|
301
|
+
print(" Stop with Ctrl-C")
|
|
302
|
+
print("=" * 60 + "\n")
|
|
303
|
+
|
|
304
|
+
entries = load_log()
|
|
305
|
+
entries = purge_old_entries(entries)
|
|
306
|
+
|
|
307
|
+
# Start caffeinate heartbeat in background
|
|
308
|
+
caffeinate_proc = subprocess.Popen(
|
|
309
|
+
["caffeinate", "-u", "-t", str(int(HEARTBEAT_INTERVAL * 10))],
|
|
310
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
while True:
|
|
315
|
+
# Decide next event type
|
|
316
|
+
if random.random() < BURST_PROB:
|
|
317
|
+
wait = random.uniform(BURST_MIN, BURST_MAX)
|
|
318
|
+
elif random.random() < DEEP_READ_PROB:
|
|
319
|
+
wait = random.uniform(DEEP_READ_MIN, DEEP_READ_MAX)
|
|
320
|
+
else:
|
|
321
|
+
wait = random.uniform(INTERVAL_MIN, INTERVAL_MAX)
|
|
322
|
+
|
|
323
|
+
# Schedule anti-idle event in parallel
|
|
324
|
+
def anti_idle():
|
|
325
|
+
time.sleep(ANTI_IDLE_INTERVAL)
|
|
326
|
+
if HAS_HID_EVENTS:
|
|
327
|
+
random_mouse_activity()
|
|
328
|
+
|
|
329
|
+
threading.Thread(target=anti_idle, daemon=True).start()
|
|
330
|
+
|
|
331
|
+
# Fire event
|
|
332
|
+
if random.random() < UNIQUE_RATIO:
|
|
333
|
+
label, detail = event_url_unique(entries)
|
|
334
|
+
else:
|
|
335
|
+
label, detail = event_url_repeated(entries)
|
|
336
|
+
|
|
337
|
+
log_console(label, detail)
|
|
338
|
+
save_log(entries)
|
|
339
|
+
|
|
340
|
+
time.sleep(wait)
|
|
341
|
+
except KeyboardInterrupt:
|
|
342
|
+
print("\n[driver] Stopping...")
|
|
343
|
+
finally:
|
|
344
|
+
caffeinate_proc.terminate()
|
|
345
|
+
save_log(entries)
|
|
346
|
+
print("[driver] Done.")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
main()
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Configuration manager for Activity Driver
|
|
4
|
+
Handles loading, saving, and managing user preferences
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Any
|
|
10
|
+
|
|
11
|
+
# Config location: ~/.config/activity-driver/config.json
|
|
12
|
+
CONFIG_DIR = Path.home() / ".config" / "activity-driver"
|
|
13
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
14
|
+
|
|
15
|
+
# Default configuration
|
|
16
|
+
DEFAULT_CONFIG = {
|
|
17
|
+
"version": "1.0.0",
|
|
18
|
+
"productive_urls": [
|
|
19
|
+
"https://coty-my.sharepoint.com/shared",
|
|
20
|
+
"https://app.contentstack.com/",
|
|
21
|
+
"https://dam.coty.com",
|
|
22
|
+
"https://coty.service-now.com/",
|
|
23
|
+
"https://pim.coty.com",
|
|
24
|
+
"https://app.gitbook.com",
|
|
25
|
+
"https://supplier.coty.com",
|
|
26
|
+
],
|
|
27
|
+
"chrome_profile": "Profile 1",
|
|
28
|
+
"interval_min": 30,
|
|
29
|
+
"interval_max": 55,
|
|
30
|
+
"unique_ratio": 0.50,
|
|
31
|
+
"burst_prob": 0.20,
|
|
32
|
+
"deep_read_prob": 0.12,
|
|
33
|
+
"dashboard_enabled": False,
|
|
34
|
+
"dashboard_port": 3006,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def ensure_config_dir():
|
|
39
|
+
"""Create config directory if it doesn't exist."""
|
|
40
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_config() -> Dict[str, Any]:
|
|
44
|
+
"""Load configuration from file, or return defaults if not found."""
|
|
45
|
+
ensure_config_dir()
|
|
46
|
+
|
|
47
|
+
if CONFIG_FILE.exists():
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"Warning: Could not load config ({e}), using defaults")
|
|
52
|
+
return DEFAULT_CONFIG.copy()
|
|
53
|
+
|
|
54
|
+
return DEFAULT_CONFIG.copy()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def save_config(config: Dict[str, Any]):
|
|
58
|
+
"""Save configuration to file."""
|
|
59
|
+
ensure_config_dir()
|
|
60
|
+
CONFIG_FILE.write_text(json.dumps(config, indent=2))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_productive_urls() -> List[str]:
|
|
64
|
+
"""Get the list of productive URLs."""
|
|
65
|
+
config = load_config()
|
|
66
|
+
return config.get("productive_urls", DEFAULT_CONFIG["productive_urls"])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def set_productive_urls(urls: List[str]):
|
|
70
|
+
"""Update the list of productive URLs."""
|
|
71
|
+
config = load_config()
|
|
72
|
+
config["productive_urls"] = urls
|
|
73
|
+
save_config(config)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_setting(key: str, default=None):
|
|
77
|
+
"""Get a specific configuration setting."""
|
|
78
|
+
config = load_config()
|
|
79
|
+
if default is None:
|
|
80
|
+
default = DEFAULT_CONFIG.get(key)
|
|
81
|
+
return config.get(key, default)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def set_setting(key: str, value: Any):
|
|
85
|
+
"""Set a specific configuration setting."""
|
|
86
|
+
config = load_config()
|
|
87
|
+
config[key] = value
|
|
88
|
+
save_config(config)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_all_config() -> Dict[str, Any]:
|
|
92
|
+
"""Get entire configuration."""
|
|
93
|
+
return load_config()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def set_all_config(config: Dict[str, Any]):
|
|
97
|
+
"""Replace entire configuration."""
|
|
98
|
+
save_config(config)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def reset_to_defaults():
|
|
102
|
+
"""Reset configuration to defaults."""
|
|
103
|
+
ensure_config_dir()
|
|
104
|
+
CONFIG_FILE.write_text(json.dumps(DEFAULT_CONFIG, indent=2))
|
|
105
|
+
return DEFAULT_CONFIG.copy()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def config_exists() -> bool:
|
|
109
|
+
"""Check if configuration file exists."""
|
|
110
|
+
return CONFIG_FILE.exists()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def print_config():
|
|
114
|
+
"""Pretty print the current configuration."""
|
|
115
|
+
config = load_config()
|
|
116
|
+
print("\n" + "=" * 60)
|
|
117
|
+
print(" Activity Driver Configuration")
|
|
118
|
+
print("=" * 60)
|
|
119
|
+
print(f"\n Config file: {CONFIG_FILE}\n")
|
|
120
|
+
|
|
121
|
+
print(" Productive URLs:")
|
|
122
|
+
for i, url in enumerate(config.get("productive_urls", []), 1):
|
|
123
|
+
print(f" {i}. {url}")
|
|
124
|
+
|
|
125
|
+
print(f"\n Chrome Profile: {config.get('chrome_profile', 'Profile 1')}")
|
|
126
|
+
print(f" Event Interval: {config.get('interval_min', 30)}-{config.get('interval_max', 55)}s")
|
|
127
|
+
print(f" Unique URL Ratio: {config.get('unique_ratio', 0.50) * 100:.0f}%")
|
|
128
|
+
|
|
129
|
+
dashboard = config.get('dashboard_enabled', False)
|
|
130
|
+
port = config.get('dashboard_port', 3006)
|
|
131
|
+
if dashboard:
|
|
132
|
+
print(f"\n Dashboard: ENABLED (http://localhost:{port})")
|
|
133
|
+
else:
|
|
134
|
+
print(f"\n Dashboard: DISABLED")
|
|
135
|
+
|
|
136
|
+
print(f" Burst Probability: {config.get('burst_prob', 0.20) * 100:.0f}%")
|
|
137
|
+
print(f" Deep Read Probability: {config.get('deep_read_prob', 0.12) * 100:.0f}%")
|
|
138
|
+
print("\n" + "=" * 60 + "\n")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
# Simple CLI for testing
|
|
143
|
+
import sys
|
|
144
|
+
|
|
145
|
+
if len(sys.argv) > 1:
|
|
146
|
+
cmd = sys.argv[1]
|
|
147
|
+
|
|
148
|
+
if cmd == "show":
|
|
149
|
+
print_config()
|
|
150
|
+
elif cmd == "reset":
|
|
151
|
+
reset_to_defaults()
|
|
152
|
+
print("✓ Configuration reset to defaults")
|
|
153
|
+
elif cmd == "path":
|
|
154
|
+
print(CONFIG_FILE)
|
|
155
|
+
else:
|
|
156
|
+
print(f"Unknown command: {cmd}")
|
|
157
|
+
print("Available: show, reset, path")
|
|
158
|
+
else:
|
|
159
|
+
print_config()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cli_main():
|
|
163
|
+
"""CLI entry point for activity-config command."""
|
|
164
|
+
import sys
|
|
165
|
+
|
|
166
|
+
if len(sys.argv) < 2:
|
|
167
|
+
# Show interactive menu
|
|
168
|
+
print("\n" + "=" * 60)
|
|
169
|
+
print(" Activity Driver — Configuration Manager")
|
|
170
|
+
print("=" * 60)
|
|
171
|
+
print("\nCommands:")
|
|
172
|
+
print(" activity-config show Display current configuration")
|
|
173
|
+
print(" activity-config domains Edit productive domains")
|
|
174
|
+
print(" activity-config profile Change Chrome profile")
|
|
175
|
+
print(" activity-config reset Reset to defaults")
|
|
176
|
+
print(" activity-config path Show config file path")
|
|
177
|
+
print("=" * 60 + "\n")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
cmd = sys.argv[1]
|
|
181
|
+
|
|
182
|
+
if cmd == "show":
|
|
183
|
+
print_config()
|
|
184
|
+
|
|
185
|
+
elif cmd == "path":
|
|
186
|
+
ensure_config_dir()
|
|
187
|
+
print(CONFIG_FILE)
|
|
188
|
+
|
|
189
|
+
elif cmd == "reset":
|
|
190
|
+
response = input("⚠️ Reset configuration to defaults? (y/N): ")
|
|
191
|
+
if response.lower() in ['y', 'yes']:
|
|
192
|
+
reset_to_defaults()
|
|
193
|
+
print("✓ Configuration reset to defaults\n")
|
|
194
|
+
else:
|
|
195
|
+
print("Cancelled\n")
|
|
196
|
+
|
|
197
|
+
elif cmd == "domains":
|
|
198
|
+
print("\n" + "=" * 60)
|
|
199
|
+
print(" Edit Productive Domains")
|
|
200
|
+
print("=" * 60 + "\n")
|
|
201
|
+
|
|
202
|
+
# Show current
|
|
203
|
+
print("Current productive domains:")
|
|
204
|
+
urls = get_productive_urls()
|
|
205
|
+
for i, url in enumerate(urls, 1):
|
|
206
|
+
print(f" {i}. {url}")
|
|
207
|
+
|
|
208
|
+
print("\nEnter new domains (press Enter twice to finish):")
|
|
209
|
+
new_domains = []
|
|
210
|
+
counter = 1
|
|
211
|
+
while True:
|
|
212
|
+
domain = input(f" Domain {counter}: ").strip()
|
|
213
|
+
if not domain:
|
|
214
|
+
if new_domains:
|
|
215
|
+
break
|
|
216
|
+
else:
|
|
217
|
+
print(" Please enter at least one domain.")
|
|
218
|
+
continue
|
|
219
|
+
new_domains.append(domain)
|
|
220
|
+
counter += 1
|
|
221
|
+
|
|
222
|
+
set_productive_urls(new_domains)
|
|
223
|
+
print(f"\n✓ Saved {len(new_domains)} productive domain(s)\n")
|
|
224
|
+
print_config()
|
|
225
|
+
|
|
226
|
+
elif cmd == "profile":
|
|
227
|
+
print("\n" + "=" * 60)
|
|
228
|
+
print(" Configure Chrome Profile")
|
|
229
|
+
print("=" * 60 + "\n")
|
|
230
|
+
|
|
231
|
+
current_profile = get_setting('chrome_profile')
|
|
232
|
+
print(f"Current Chrome profile: {current_profile}")
|
|
233
|
+
print(f"Find yours at: chrome://version → Profile Path (last segment)")
|
|
234
|
+
|
|
235
|
+
new_profile = input("\nEnter new Chrome profile name: ").strip()
|
|
236
|
+
if not new_profile:
|
|
237
|
+
print("✗ Profile name cannot be empty\n")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
set_setting('chrome_profile', new_profile)
|
|
241
|
+
print(f"✓ Chrome profile set to: {new_profile}\n")
|
|
242
|
+
|
|
243
|
+
elif cmd == "dashboard":
|
|
244
|
+
print("\n" + "=" * 60)
|
|
245
|
+
print(" Configure Dashboard")
|
|
246
|
+
print("=" * 60 + "\n")
|
|
247
|
+
|
|
248
|
+
current_enabled = get_setting('dashboard_enabled', False)
|
|
249
|
+
current_port = get_setting('dashboard_port', 3006)
|
|
250
|
+
|
|
251
|
+
status = "ENABLED" if current_enabled else "DISABLED"
|
|
252
|
+
print(f"Dashboard Status: {status}")
|
|
253
|
+
if current_enabled:
|
|
254
|
+
print(f"Port: {current_port}")
|
|
255
|
+
|
|
256
|
+
response = input("\nEnable dashboard? (y/n): ").strip().lower()
|
|
257
|
+
|
|
258
|
+
if response in ['y', 'yes']:
|
|
259
|
+
port_input = input("Enter dashboard port [3006]: ").strip()
|
|
260
|
+
port = int(port_input) if port_input else 3006
|
|
261
|
+
set_setting('dashboard_enabled', True)
|
|
262
|
+
set_setting('dashboard_port', port)
|
|
263
|
+
print(f"✓ Dashboard enabled on port {port}\n")
|
|
264
|
+
elif response in ['n', 'no']:
|
|
265
|
+
set_setting('dashboard_enabled', False)
|
|
266
|
+
print(f"✓ Dashboard disabled\n")
|
|
267
|
+
else:
|
|
268
|
+
print("✗ Invalid response\n")
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
else:
|
|
272
|
+
print(f"Unknown command: {cmd}\n")
|
|
273
|
+
print("Valid commands: show, domains, profile, dashboard, reset, path\n")
|