MB-IDK commited on
Commit
d69e60a
Β·
verified Β·
1 Parent(s): 6ec1d0e

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +115 -0
  2. app (1).py +460 -0
Dockerfile ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ###############################################################################
2
+ # Dockerfile pour Hugging Face Spaces - Microsoft Rewards Script
3
+ #
4
+ # Ce Dockerfile :
5
+ # 1. Clone le repo GitHub et compile le projet TypeScript
6
+ # 2. Installe Python + Gradio pour l'interface web (port 7860)
7
+ # 3. Lance app.py qui gère les credentials via les HF Secrets
8
+ #
9
+ # Secrets a configurer dans HF Spaces :
10
+ # - MS_ACCOUNTS : JSON array, ex: [{"email":"ton@mail.com","password":"tonmdp"}]
11
+ # ou bien separement :
12
+ # - MS_EMAIL : ton email Microsoft
13
+ # - MS_PASSWORD : ton mot de passe Microsoft
14
+ ###############################################################################
15
+
16
+ FROM node:22-slim
17
+
18
+ # Meta pour Hugging Face Spaces
19
+ LABEL maintainer="user"
20
+ LABEL description="Microsoft Rewards Script - Hugging Face Space"
21
+
22
+ # Variables d'environnement
23
+ ENV DEBIAN_FRONTEND=noninteractive \
24
+ PLAYWRIGHT_BROWSERS_PATH=0 \
25
+ NODE_ENV=production \
26
+ TZ=Europe/Paris \
27
+ FORCE_HEADLESS=1 \
28
+ PYTHONUNBUFFERED=1
29
+
30
+ # Installer les dependances systeme : Python3, pip, cron, Chromium deps, git
31
+ RUN apt-get update && apt-get install -y --no-install-recommends \
32
+ python3 \
33
+ python3-pip \
34
+ python3-venv \
35
+ git \
36
+ cron \
37
+ gettext-base \
38
+ tzdata \
39
+ ca-certificates \
40
+ curl \
41
+ libglib2.0-0 \
42
+ libdbus-1-3 \
43
+ libexpat1 \
44
+ libfontconfig1 \
45
+ libgtk-3-0 \
46
+ libnspr4 \
47
+ libnss3 \
48
+ libasound2 \
49
+ libflac12 \
50
+ libatk1.0-0 \
51
+ libatspi2.0-0 \
52
+ libdrm2 \
53
+ libgbm1 \
54
+ libdav1d6 \
55
+ libx11-6 \
56
+ libx11-xcb1 \
57
+ libxcomposite1 \
58
+ libxcursor1 \
59
+ libxdamage1 \
60
+ libxext6 \
61
+ libxfixes3 \
62
+ libxi6 \
63
+ libxrandr2 \
64
+ libxrender1 \
65
+ libxss1 \
66
+ libxtst6 \
67
+ libdouble-conversion3 \
68
+ && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
69
+
70
+ # Repertoire de travail pour le script Microsoft Rewards
71
+ WORKDIR /usr/src/microsoft-rewards-script
72
+
73
+ # Cloner le repo
74
+ RUN git clone https://github.com/TheNetsky/Microsoft-Rewards-Script.git /usr/src/microsoft-rewards-script
75
+
76
+ # Installer les dependances Node.js et compiler
77
+ RUN npm ci --ignore-scripts \
78
+ && npm run build \
79
+ && rm -rf node_modules \
80
+ && npm ci --omit=dev --ignore-scripts \
81
+ && npm cache clean --force
82
+
83
+ # Installer le navigateur Chromium pour Playwright
84
+ RUN npx playwright install --with-deps --only-shell chromium \
85
+ && rm -rf /root/.cache /tmp/* /var/tmp/*
86
+
87
+ # Config par defaut : headless = true pour Docker
88
+ RUN sed -i 's/"headless": false/"headless": true/' /usr/src/microsoft-rewards-script/src/config.json || true
89
+
90
+ # Repertoire pour les sessions persistantes
91
+ RUN mkdir -p /usr/src/microsoft-rewards-script/dist/sessions
92
+
93
+ # Repertoire de travail pour l'app Python
94
+ WORKDIR /app
95
+
96
+ # Installer les dependances Python (Gradio + APScheduler)
97
+ RUN pip3 install --no-cache-dir --break-system-packages \
98
+ gradio \
99
+ apscheduler
100
+
101
+ # Copier l'application Python
102
+ COPY app.py /app/app.py
103
+
104
+ # Exposer le port 7860 (requis par Hugging Face Spaces)
105
+ EXPOSE 7860
106
+
107
+ # Utilisateur non-root pour HF Spaces
108
+ RUN useradd -m -u 1000 appuser \
109
+ && chown -R appuser:appuser /app \
110
+ && chown -R appuser:appuser /usr/src/microsoft-rewards-script
111
+
112
+ USER appuser
113
+
114
+ # Lancer l'app Gradio
115
+ CMD ["python3", "/app/app.py"]
app (1).py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py - Microsoft Rewards Script pour Hugging Face Spaces
3
+
4
+ Interface Gradio qui :
5
+ - Lit les credentials depuis les HF Secrets (variables d'environnement)
6
+ - Genere le fichier accounts.json automatiquement
7
+ - Lance le script Microsoft Rewards a intervalle regulier
8
+ - Affiche les logs en temps reel
9
+ - Permet de declencher manuellement une execution
10
+
11
+ === CONFIGURATION DES SECRETS HF ===
12
+
13
+ Dans Settings > Repository secrets de votre Space, ajoutez :
14
+
15
+ Option 1 (compte unique) :
16
+ MS_EMAIL = votre_email@outlook.com
17
+ MS_PASSWORD = votre_mot_de_passe
18
+
19
+ Option 2 (multi-comptes, prevaut sur Option 1) :
20
+ MS_ACCOUNTS = [{"email":"mail1@outlook.com","password":"mdp1"},{"email":"mail2@outlook.com","password":"mdp2"}]
21
+
22
+ Optionnel :
23
+ CRON_SCHEDULE = 0 7,16,20 * * * (defaut: 3 fois par jour)
24
+ RUN_ON_START = true (defaut: true)
25
+ """
26
+
27
+ import json
28
+ import os
29
+ import subprocess
30
+ import threading
31
+ import time
32
+ import signal
33
+ import sys
34
+ from datetime import datetime
35
+ from pathlib import Path
36
+
37
+ import gradio as gr
38
+ from apscheduler.schedulers.background import BackgroundScheduler
39
+
40
+ # === Constantes ===
41
+ SCRIPT_DIR = Path("/usr/src/microsoft-rewards-script")
42
+ DIST_DIR = SCRIPT_DIR / "dist"
43
+ ACCOUNTS_FILE = DIST_DIR / "accounts.json"
44
+ CONFIG_FILE = DIST_DIR / "config.json"
45
+ LOG_FILE = Path("/app/rewards_script.log")
46
+ MAX_LOG_LINES = 500
47
+
48
+ # === Etat global ===
49
+ is_running = False
50
+ run_process = None
51
+
52
+
53
+ def log_message(msg: str):
54
+ """Ajoute un message horodate au log."""
55
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
56
+ line = f"[{timestamp}] {msg}"
57
+ print(line, flush=True)
58
+ try:
59
+ with open(LOG_FILE, "a", encoding="utf-8") as f:
60
+ f.write(line + "\n")
61
+ except Exception:
62
+ pass
63
+
64
+
65
+ def read_log() -> str:
66
+ """Lit les dernieres lignes du fichier de log."""
67
+ try:
68
+ if LOG_FILE.exists():
69
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
70
+ lines = f.readlines()
71
+ return "".join(lines[-MAX_LOG_LINES:])
72
+ return "Aucun log disponible."
73
+ except Exception as e:
74
+ return f"Erreur lecture log: {e}"
75
+
76
+
77
+ def generate_accounts_json() -> bool:
78
+ """
79
+ Genere le fichier accounts.json a partir des secrets HF.
80
+ Priorite : MS_ACCOUNTS > MS_EMAIL + MS_PASSWORD
81
+ """
82
+ ms_accounts = os.environ.get("MS_ACCOUNTS", "").strip()
83
+ ms_email = os.environ.get("MS_EMAIL", "").strip()
84
+ ms_password = os.environ.get("MS_PASSWORD", "").strip()
85
+
86
+ accounts = []
87
+
88
+ if ms_accounts:
89
+ try:
90
+ accounts = json.loads(ms_accounts)
91
+ if not isinstance(accounts, list):
92
+ log_message("ERREUR: MS_ACCOUNTS doit etre un tableau JSON.")
93
+ return False
94
+ # Valider chaque entree
95
+ for acc in accounts:
96
+ if not acc.get("email") or not acc.get("password"):
97
+ log_message(f"ERREUR: Compte invalide dans MS_ACCOUNTS: {acc}")
98
+ return False
99
+ # Ajouter proxy par defaut si absent
100
+ if "proxy" not in acc:
101
+ acc["proxy"] = {
102
+ "proxyAxios": True,
103
+ "url": "",
104
+ "port": 0,
105
+ "username": "",
106
+ "password": ""
107
+ }
108
+ log_message(f"Accounts charges depuis MS_ACCOUNTS: {len(accounts)} compte(s)")
109
+ except json.JSONDecodeError as e:
110
+ log_message(f"ERREUR: MS_ACCOUNTS n'est pas un JSON valide: {e}")
111
+ return False
112
+
113
+ elif ms_email and ms_password:
114
+ accounts = [
115
+ {
116
+ "email": ms_email,
117
+ "password": ms_password,
118
+ "proxy": {
119
+ "proxyAxios": True,
120
+ "url": "",
121
+ "port": 0,
122
+ "username": "",
123
+ "password": ""
124
+ }
125
+ }
126
+ ]
127
+ log_message(f"Account charge depuis MS_EMAIL/MS_PASSWORD: {ms_email}")
128
+
129
+ else:
130
+ log_message("ERREUR: Aucun credential trouve ! Configurez MS_ACCOUNTS ou MS_EMAIL+MS_PASSWORD dans les secrets HF.")
131
+ return False
132
+
133
+ # Ecrire le fichier accounts.json dans dist/
134
+ try:
135
+ with open(ACCOUNTS_FILE, "w", encoding="utf-8") as f:
136
+ json.dump(accounts, f, indent=4)
137
+ log_message(f"Fichier accounts.json genere: {ACCOUNTS_FILE}")
138
+ return True
139
+ except Exception as e:
140
+ log_message(f"ERREUR: Impossible d'ecrire accounts.json: {e}")
141
+ return False
142
+
143
+
144
+ def ensure_config():
145
+ """S'assure que config.json existe avec headless=true."""
146
+ try:
147
+ if CONFIG_FILE.exists():
148
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
149
+ config = json.load(f)
150
+ # Forcer headless en mode Docker
151
+ config["headless"] = True
152
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
153
+ json.dump(config, f, indent=4)
154
+ log_message("Config mise a jour: headless=true")
155
+ else:
156
+ default_config = {
157
+ "baseURL": "https://rewards.bing.com",
158
+ "sessionPath": "sessions",
159
+ "headless": True,
160
+ "parallel": False,
161
+ "runOnZeroPoints": False,
162
+ "clusters": 1,
163
+ "saveFingerprint": {"mobile": False, "desktop": False},
164
+ "workers": {
165
+ "doDailySet": True,
166
+ "doMorePromotions": True,
167
+ "doPunchCards": True,
168
+ "doDesktopSearch": True,
169
+ "doMobileSearch": True,
170
+ "doDailyCheckIn": True,
171
+ "doReadToEarn": True
172
+ },
173
+ "searchOnBingLocalQueries": False,
174
+ "globalTimeout": "30s",
175
+ "searchSettings": {
176
+ "useGeoLocaleQueries": False,
177
+ "scrollRandomResults": True,
178
+ "clickRandomResults": True,
179
+ "searchDelay": {"min": "3min", "max": "5min"},
180
+ "retryMobileSearchAmount": 2
181
+ },
182
+ "logExcludeFunc": ["SEARCH-CLOSE-TABS"],
183
+ "webhookLogExcludeFunc": ["SEARCH-CLOSE-TABS"],
184
+ "proxy": {"proxyGoogleTrends": True, "proxyBingTerms": True},
185
+ "webhook": {"enabled": False, "url": ""},
186
+ "conclusionWebhook": {"enabled": False, "url": ""}
187
+ }
188
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
189
+ json.dump(default_config, f, indent=4)
190
+ log_message("Config par defaut creee avec headless=true")
191
+ except Exception as e:
192
+ log_message(f"ERREUR config: {e}")
193
+
194
+
195
+ def run_script():
196
+ """Lance le script Microsoft Rewards et capture la sortie."""
197
+ global is_running, run_process
198
+
199
+ if is_running:
200
+ log_message("Script deja en cours d'execution, skip...")
201
+ return
202
+
203
+ is_running = True
204
+ log_message("=== Demarrage du script Microsoft Rewards ===")
205
+
206
+ # Regenerer accounts.json avant chaque run (au cas ou les secrets changent)
207
+ if not generate_accounts_json():
208
+ log_message("Impossible de generer accounts.json, abandon.")
209
+ is_running = False
210
+ return
211
+
212
+ ensure_config()
213
+
214
+ try:
215
+ env = os.environ.copy()
216
+ env["PLAYWRIGHT_BROWSERS_PATH"] = "0"
217
+ env["FORCE_HEADLESS"] = "1"
218
+
219
+ run_process = subprocess.Popen(
220
+ ["node", "./dist/index.js"],
221
+ cwd=str(SCRIPT_DIR),
222
+ stdout=subprocess.PIPE,
223
+ stderr=subprocess.STDOUT,
224
+ text=True,
225
+ env=env
226
+ )
227
+
228
+ # Lire la sortie en temps reel
229
+ for line in run_process.stdout:
230
+ line = line.strip()
231
+ if line:
232
+ log_message(f"[SCRIPT] {line}")
233
+
234
+ run_process.wait()
235
+ exit_code = run_process.returncode
236
+
237
+ if exit_code == 0:
238
+ log_message("=== Script termine avec succes ===")
239
+ else:
240
+ log_message(f"=== Script termine avec erreur (code: {exit_code}) ===")
241
+
242
+ except Exception as e:
243
+ log_message(f"ERREUR durant l'execution: {e}")
244
+ finally:
245
+ is_running = False
246
+ run_process = None
247
+
248
+
249
+ def run_script_threaded():
250
+ """Lance le script dans un thread separΓ©."""
251
+ thread = threading.Thread(target=run_script, daemon=True)
252
+ thread.start()
253
+
254
+
255
+ def manual_run():
256
+ """Declenche manuellement le script depuis l'interface."""
257
+ if is_running:
258
+ return "⚠️ Le script est deja en cours d'execution. Patientez..."
259
+ run_script_threaded()
260
+ return "βœ… Script lance ! Consultez les logs ci-dessous."
261
+
262
+
263
+ def get_status():
264
+ """Retourne le statut actuel."""
265
+ if is_running:
266
+ return "🟑 En cours d'execution..."
267
+ return "🟒 En attente (prochain run selon le planning)"
268
+
269
+
270
+ def get_next_run():
271
+ """Retourne l'heure du prochain run planifie."""
272
+ if scheduler.running:
273
+ jobs = scheduler.get_jobs()
274
+ if jobs:
275
+ next_time = jobs[0].next_run_time
276
+ if next_time:
277
+ return f"Prochain run: {next_time.strftime('%Y-%m-%d %H:%M:%S')}"
278
+ return "Planification non active"
279
+
280
+
281
+ def refresh_logs():
282
+ """Rafraichit l'affichage des logs."""
283
+ return read_log()
284
+
285
+
286
+ def refresh_status():
287
+ """Rafraichit l'affichage du statut."""
288
+ status = get_status()
289
+ next_run = get_next_run()
290
+ accounts_info = "Non configure"
291
+ if ACCOUNTS_FILE.exists():
292
+ try:
293
+ with open(ACCOUNTS_FILE, "r") as f:
294
+ accs = json.load(f)
295
+ emails = [a.get("email", "?") for a in accs]
296
+ accounts_info = f"{len(accs)} compte(s): {', '.join(emails)}"
297
+ except Exception:
298
+ accounts_info = "Erreur de lecture"
299
+
300
+ return f"{status}\n{next_run}\nComptes: {accounts_info}"
301
+
302
+
303
+ # === Parse le cron schedule ===
304
+ def parse_cron_schedule():
305
+ """Parse la variable CRON_SCHEDULE en composants APScheduler."""
306
+ schedule = os.environ.get("CRON_SCHEDULE", "0 7,16,20 * * *").strip()
307
+ parts = schedule.split()
308
+ if len(parts) != 5:
309
+ log_message(f"CRON_SCHEDULE invalide '{schedule}', utilisation du defaut")
310
+ parts = ["0", "7,16,20", "*", "*", "*"]
311
+
312
+ return {
313
+ "hour": parts[1],
314
+ "minute": parts[0],
315
+ "day": parts[2],
316
+ "month": parts[3],
317
+ "day_of_week": parts[4]
318
+ }
319
+
320
+
321
+ # === Scheduler ===
322
+ scheduler = BackgroundScheduler()
323
+
324
+ # === Interface Gradio ===
325
+ def build_ui():
326
+ with gr.Blocks(
327
+ title="Microsoft Rewards Script",
328
+ theme=gr.themes.Soft(),
329
+ css="""
330
+ .status-box { font-size: 16px; padding: 15px; border-radius: 8px; }
331
+ .log-box { font-family: monospace; font-size: 12px; }
332
+ """
333
+ ) as demo:
334
+ gr.Markdown(
335
+ """
336
+ # 🎁 Microsoft Rewards Script
337
+ Automatisation de Microsoft Rewards via [TheNetsky/Microsoft-Rewards-Script](https://github.com/TheNetsky/Microsoft-Rewards-Script)
338
+
339
+ ---
340
+ """
341
+ )
342
+
343
+ with gr.Row():
344
+ with gr.Column(scale=1):
345
+ gr.Markdown("### πŸ“Š Statut")
346
+ status_display = gr.Textbox(
347
+ value="Demarrage...",
348
+ label="Statut actuel",
349
+ interactive=False,
350
+ lines=3,
351
+ elem_classes=["status-box"]
352
+ )
353
+ refresh_status_btn = gr.Button("πŸ”„ Rafraichir le statut", variant="secondary")
354
+ refresh_status_btn.click(fn=refresh_status, outputs=status_display)
355
+
356
+ with gr.Column(scale=1):
357
+ gr.Markdown("### πŸš€ Execution manuelle")
358
+ run_btn = gr.Button("▢️ Lancer le script maintenant", variant="primary", size="lg")
359
+ run_output = gr.Textbox(label="Resultat", interactive=False)
360
+ run_btn.click(fn=manual_run, outputs=run_output)
361
+
362
+ gr.Markdown("### πŸ“œ Logs")
363
+ log_display = gr.Textbox(
364
+ value="Chargement des logs...",
365
+ label="Logs du script",
366
+ interactive=False,
367
+ lines=20,
368
+ max_lines=50,
369
+ elem_classes=["log-box"]
370
+ )
371
+
372
+ with gr.Row():
373
+ refresh_log_btn = gr.Button("πŸ”„ Rafraichir les logs", variant="secondary")
374
+ clear_log_btn = gr.Button("πŸ—‘οΈ Vider les logs")
375
+
376
+ refresh_log_btn.click(fn=refresh_logs, outputs=log_display)
377
+ clear_log_btn.click(fn=lambda: (open(LOG_FILE, "w").close() if LOG_FILE.exists() else None, "Logs effaces.")[1], outputs=log_display)
378
+
379
+ # Auto-refresh des logs toutes les 10 secondes
380
+ timer = gr.Timer(value=10)
381
+ timer.tick(fn=refresh_logs, outputs=log_display)
382
+ timer.tick(fn=refresh_status, outputs=status_display)
383
+
384
+ gr.Markdown(
385
+ """
386
+ ---
387
+ ### πŸ” Configuration des Secrets (HF Spaces > Settings > Repository secrets)
388
+
389
+ | Secret | Description | Exemple |
390
+ |--------|-------------|---------|
391
+ | `MS_EMAIL` | Email du compte Microsoft | `vous@outlook.com` |
392
+ | `MS_PASSWORD` | Mot de passe du compte | `votre_mot_de_passe` |
393
+ | `MS_ACCOUNTS` | Multi-comptes (JSON) | `[{"email":"m1@outlook.com","password":"p1"}]` |
394
+ | `CRON_SCHEDULE` | Planning cron (defaut: `0 7,16,20 * * *`) | `0 8 * * *` |
395
+ | `RUN_ON_START` | Lancer au demarrage (defaut: `true`) | `true` |
396
+
397
+ > **Note** : Si `MS_ACCOUNTS` est defini, il prevaut sur `MS_EMAIL` + `MS_PASSWORD`.
398
+ """
399
+ )
400
+
401
+ return demo
402
+
403
+
404
+ # === Point d'entree ===
405
+ if __name__ == "__main__":
406
+ log_message("=" * 50)
407
+ log_message("Microsoft Rewards Script - Hugging Face Space")
408
+ log_message("=" * 50)
409
+
410
+ # Verifier les credentials au demarrage
411
+ has_accounts = os.environ.get("MS_ACCOUNTS", "").strip()
412
+ has_email = os.environ.get("MS_EMAIL", "").strip()
413
+ has_password = os.environ.get("MS_PASSWORD", "").strip()
414
+
415
+ if has_accounts:
416
+ log_message("Mode multi-comptes detecte (MS_ACCOUNTS)")
417
+ elif has_email and has_password:
418
+ log_message(f"Mode compte unique detecte (MS_EMAIL={has_email})")
419
+ else:
420
+ log_message("⚠️ ATTENTION: Aucun credential configure !")
421
+ log_message("Ajoutez MS_EMAIL + MS_PASSWORD ou MS_ACCOUNTS dans les secrets HF.")
422
+
423
+ # Generer accounts.json au demarrage
424
+ generate_accounts_json()
425
+ ensure_config()
426
+
427
+ # Configurer le scheduler
428
+ cron_params = parse_cron_schedule()
429
+ log_message(f"Planning cron: minute={cron_params['minute']} heure={cron_params['hour']} jour={cron_params['day']} mois={cron_params['month']} jour_semaine={cron_params['day_of_week']}")
430
+
431
+ scheduler.add_job(
432
+ run_script_threaded,
433
+ "cron",
434
+ minute=cron_params["minute"],
435
+ hour=cron_params["hour"],
436
+ day=cron_params["day"],
437
+ month=cron_params["month"],
438
+ day_of_week=cron_params["day_of_week"],
439
+ id="rewards_daily_run",
440
+ name="Microsoft Rewards Daily Run"
441
+ )
442
+ scheduler.start()
443
+ log_message("Scheduler demarre")
444
+
445
+ # Lancer au demarrage si RUN_ON_START=true
446
+ if os.environ.get("RUN_ON_START", "true").lower() == "true":
447
+ log_message("RUN_ON_START=true => Lancement initial dans 30 secondes...")
448
+ # Petit delai pour laisser Gradio demarrer
449
+ threading.Timer(30, run_script_threaded).start()
450
+ else:
451
+ log_message("RUN_ON_START=false => Pas de lancement initial")
452
+
453
+ # Lancer l'interface Gradio (bloquant)
454
+ demo = build_ui()
455
+ demo.launch(
456
+ server_name="0.0.0.0",
457
+ server_port=7860,
458
+ show_error=True,
459
+ show_api=False
460
+ )