somratpro commited on
Commit
7a0af95
Β·
1 Parent(s): 8d60ff8

feat: implement DNS-over-HTTPS fallback for network requests and add a web dashboard to the health server.

Browse files
Files changed (6) hide show
  1. .env.example +8 -1
  2. README.md +83 -46
  3. dns-fix.js +102 -13
  4. health-server.js +285 -10
  5. start.sh +23 -2
  6. workspace-sync.py +45 -2
.env.example CHANGED
@@ -132,7 +132,7 @@ GATEWAY_TOKEN=your_gateway_token_here
132
  # If set, users can log in with this password instead of the token
133
  # OPENCLAW_PASSWORD=your_password_here
134
 
135
- # ── OPTIONAL: Telegram Integration ──
136
  # Get bot token from: https://t.me/BotFather
137
  TELEGRAM_BOT_TOKEN=your_bot_token_here
138
 
@@ -142,6 +142,9 @@ TELEGRAM_USER_ID=123456789
142
  # Multiple user IDs (comma-separated for team access)
143
  # TELEGRAM_USER_IDS=123456789,987654321,555555555
144
 
 
 
 
145
  # ── OPTIONAL: Workspace Backup to HF Dataset ──
146
  HF_USERNAME=your_hf_username
147
  HF_TOKEN=hf_your_token_here
@@ -161,6 +164,10 @@ KEEP_ALIVE_INTERVAL=300
161
  # Workspace auto-sync interval (seconds). Default: 600.
162
  SYNC_INTERVAL=600
163
 
 
 
 
 
164
  # ── OPTIONAL: Advanced ──
165
  # Pin OpenClaw version. Default: latest
166
  OPENCLAW_VERSION=latest
 
132
  # If set, users can log in with this password instead of the token
133
  # OPENCLAW_PASSWORD=your_password_here
134
 
135
+ # ── OPTIONAL: Chat Integrations ──
136
  # Get bot token from: https://t.me/BotFather
137
  TELEGRAM_BOT_TOKEN=your_bot_token_here
138
 
 
142
  # Multiple user IDs (comma-separated for team access)
143
  # TELEGRAM_USER_IDS=123456789,987654321,555555555
144
 
145
+ # WhatsApp integration (pairing mode via CLI)
146
+ # WHATSAPP_ENABLED=true
147
+
148
  # ── OPTIONAL: Workspace Backup to HF Dataset ──
149
  HF_USERNAME=your_hf_username
150
  HF_TOKEN=hf_your_token_here
 
164
  # Workspace auto-sync interval (seconds). Default: 600.
165
  SYNC_INTERVAL=600
166
 
167
+ # ── OPTIONAL: Webhooks ──
168
+ # URL to send HTTP POST requests on startup and backup errors
169
+ # WEBHOOK_URL=https://your-webhook-endpoint.com/webhook
170
+
171
  # ── OPTIONAL: Advanced ──
172
  # Pin OpenClaw version. Default: latest
173
  OPENCLAW_VERSION=latest
README.md CHANGED
@@ -15,7 +15,7 @@ license: mit
15
  [![HF Space](https://img.shields.io/badge/πŸ€—%20HuggingFace-Space-blue?style=flat-square)](https://huggingface.co/spaces)
16
  [![OpenClaw](https://img.shields.io/badge/OpenClaw-Gateway-red?style=flat-square)](https://github.com/openclaw/openclaw)
17
 
18
- **Your always-on AI assistant β€” free, no server needed.** HuggingClaw runs [OpenClaw](https://openclaw.ai) on HuggingFace Spaces, giving you a 24/7 AI chat assistant Telegram. It works with *any* large language model (LLM) – Claude, ChatGPT, Gemini, etc. – and even supports custom models via [OpenRouter](https://openrouter.ai). Deploy in minutes on the free HF Spaces tier (2 vCPU, 16GB RAM, 50GB) with automatic workspace backup to a HuggingFace Dataset so your chat history and settings persist across restarts.
19
 
20
  ## Table of Contents
21
 
@@ -23,7 +23,10 @@ license: mit
23
  - [πŸŽ₯ Video Tutorial](#-video-tutorial)
24
  - [πŸš€ Quick Start](#-quick-start)
25
  - [πŸ“± Telegram Setup *(Optional)*](#-telegram-setup-optional)
 
26
  - [πŸ’Ύ Workspace Backup *(Optional)*](#-workspace-backup-optional)
 
 
27
  - [βš™οΈ Full Configuration Reference](#-full-configuration-reference)
28
  - [πŸ€– LLM Providers](#-llm-providers)
29
  - [πŸ’» Local Development](#-local-development)
@@ -42,7 +45,9 @@ license: mit
42
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
43
  - πŸ’Ύ **Workspace Backup:** Chats and settings sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
44
  - πŸ’“ **Always-On:** Built-in keep-alive pings prevent the HF Space from sleeping, so the assistant is always online.
45
- - πŸ‘₯ **Multi-User Telegram:** Configure one or more user IDs to control who can message the bot.
 
 
46
  - πŸ” **Flexible Auth:** Secure the Control UI with either a gateway token or password.
47
  - 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
48
 
@@ -86,6 +91,23 @@ To chat via Telegram:
86
 
87
  After restarting, the bot should appear online on Telegram.
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  ## πŸ’Ύ Workspace Backup *(Optional)*
90
 
91
  For persistent chat history and configuration:
@@ -95,73 +117,88 @@ For persistent chat history and configuration:
95
 
96
  Optionally set `BACKUP_DATASET_NAME` (default: `huggingclaw-backup`) to choose the HF Dataset name. On first run, HuggingClaw will create (or use) the private Dataset repo `HF_USERNAME/SPACE-backup`, then restore your workspace on startup and sync changes every 10 minutes. The workspace is also saved on graceful shutdown. This ensures your data survives restarts.
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  ## βš™οΈ Full Configuration Reference
99
 
100
  See `.env.example` for complete settings. Key environment variables:
101
 
102
  ### Core
103
 
104
- | Variable | Description |
105
- |----------------|---------------------------------------|
106
- | `LLM_API_KEY` | LLM provider API key (e.g. OpenAI, Anthropic, etc.) |
107
- | `LLM_MODEL` | Model ID (prefix `<provider>/`, auto-detected from prefix) |
108
- | `GATEWAY_TOKEN`| Gateway token for Control UI access (required) |
109
 
110
  ### Background Services
111
 
112
- | Variable | Default | Description |
113
- |----------------------|---------|-----------------------------------|
114
- | `KEEP_ALIVE_INTERVAL`| `300` | Self-ping interval in seconds (0 to disable) |
115
- | `SYNC_INTERVAL` | `600` | Workspace sync interval (sec.) to HF Dataset |
116
 
117
  ### Security
118
 
119
- | Variable | Description |
120
- |---------------------|---------------------------------------------------|
121
- | `OPENCLAW_PASSWORD` | (optional) Enable simple password auth instead of token |
122
- | `TRUSTED_PROXIES` | Comma-separated IPs of HF proxies (for reverse-proxy fixes) |
123
- | `ALLOWED_ORIGINS` | Comma-separated allowed origins for Control UI (e.g. `https://your-space.hf.space`) |
124
 
125
  ### Workspace Backup
126
 
127
- | Variable | Default | Description |
128
- |---------------------|-------------------|------------------------------------------------|
129
- | `HF_USERNAME` | β€” | Your HuggingFace username |
130
- | `HF_TOKEN` | β€” | HF token with write access (for backups) |
131
- | `BACKUP_DATASET_NAME`| `huggingclaw-backup`| Dataset name for backup repo (auto-created) |
132
- | `WORKSPACE_GIT_USER`| `openclaw@example.com` | Git commit email for workspace commits |
133
- | `WORKSPACE_GIT_NAME`| `OpenClaw Bot` | Git commit name for workspace commits |
134
 
135
  ### Advanced
136
 
137
- | Variable | Default | Description |
138
- |---------------------|-----------|-------------------------------------|
139
- | `OPENCLAW_VERSION` | `latest` | Pin a specific OpenClaw version |
140
- | `HEALTH_PORT` | `7861` | Internal health endpoint port |
141
 
142
  ## πŸ€– LLM Providers
143
 
144
  HuggingClaw supports **all providers** from OpenClaw. Set `LLM_MODEL=<provider/model>` and the provider is auto-detected. For example:
145
 
146
- | Provider | Prefix | Example Model | API Key Source |
147
- |---------------|---------------|--------------------------------|-----------------------------------|
148
- | **Anthropic** | `anthropic/` | `anthropic/claude-sonnet-4-6` | [Anthropic Console](https://console.anthropic.com/) |
149
- | **OpenAI** | `openai/` | `openai/gpt-5.4` | [OpenAI Platform](https://platform.openai.com/) |
150
- | **Google** | `google/` | `google/gemini-2.5-flash` | [AI Studio](https://ai.google.dev/) |
151
- | **DeepSeek** | `deepseek/` | `deepseek/deepseek-v3.2` | [DeepSeek](https://platform.deepseek.com) |
152
- | **xAI (Grok)**| `xai/` | `xai/grok-4` | [xAI](https://console.x.ai) |
153
- | **Mistral** | `mistral/` | `mistral/mistral-large-latest` | [Mistral Console](https://console.mistral.ai) |
154
- | **Moonshot** | `moonshot/` | `moonshot/kimi-k2.5` | [Moonshot](https://platform.moonshot.cn) |
155
- | **Cohere** | `cohere/` | `cohere/command-a` | [Cohere Dashboard](https://dashboard.cohere.com) |
156
- | **Groq** | `groq/` | `groq/mixtral-8x7b-32768` | [Groq](https://console.groq.com) |
157
- | **MiniMax** | `minimax/` | `minimax/minimax-m2.7` | [MiniMax](https://platform.minimax.io) |
158
- | **NVIDIA** | `nvidia/` | `nvidia/nemotron-3-super-120b-a12b` | [NVIDIA API](https://api.nvidia.com) |
159
- | **Z.ai (GLM)**| `zai/` | `zai/glm-5` | [Z.ai](https://z.ai) |
160
- | **Volcengine**| `volcengine/` | `volcengine/doubao-seed-1-8-251228` | [Volcengine](https://www.volcengine.com) |
161
- | **HuggingFace**| `huggingface/`| `huggingface/deepseek-ai/DeepSeek-R1` | [HF Tokens](https://huggingface.co/settings/tokens) |
162
- | **OpenCode Zen**| `opencode/` | `opencode/claude-opus-4-6` | [OpenCode.ai](https://opencode.ai/auth) |
163
- | **OpenCode Go**| `opencode-go/`| `opencode-go/kimi-k2.5` | [OpenCode.ai](https://opencode.ai/auth) |
164
- | **Kilo Gateway**| `kilocode/` | `kilocode/anthropic/claude-opus-4.6` | [Kilo.ai](https://kilo.ai) |
165
 
166
  ### OpenRouter – 200+ Models with One Key
167
 
 
15
  [![HF Space](https://img.shields.io/badge/πŸ€—%20HuggingFace-Space-blue?style=flat-square)](https://huggingface.co/spaces)
16
  [![OpenClaw](https://img.shields.io/badge/OpenClaw-Gateway-red?style=flat-square)](https://github.com/openclaw/openclaw)
17
 
18
+ **Your always-on AI assistant β€” free, no server needed.** HuggingClaw runs [OpenClaw](https://openclaw.ai) on HuggingFace Spaces, giving you a 24/7 AI chat assistant on Telegram and WhatsApp. It works with *any* large language model (LLM) – Claude, ChatGPT, Gemini, etc. – and even supports custom models via [OpenRouter](https://openrouter.ai). Deploy in minutes on the free HF Spaces tier (2 vCPU, 16GB RAM, 50GB) with automatic workspace backup to a HuggingFace Dataset so your chat history and settings persist across restarts.
19
 
20
  ## Table of Contents
21
 
 
23
  - [πŸŽ₯ Video Tutorial](#-video-tutorial)
24
  - [πŸš€ Quick Start](#-quick-start)
25
  - [πŸ“± Telegram Setup *(Optional)*](#-telegram-setup-optional)
26
+ - [πŸ’¬ WhatsApp Setup *(Optional)*](#-whatsapp-setup-optional)
27
  - [πŸ’Ύ Workspace Backup *(Optional)*](#-workspace-backup-optional)
28
+ - [πŸ“Š Dashboard & Monitoring](#-dashboard--monitoring)
29
+ - [πŸ”” Webhooks *(Optional)*](#-webhooks-optional)
30
  - [βš™οΈ Full Configuration Reference](#-full-configuration-reference)
31
  - [πŸ€– LLM Providers](#-llm-providers)
32
  - [πŸ’» Local Development](#-local-development)
 
45
  - 🐳 **Fast Builds:** Uses a pre-built OpenClaw Docker image to deploy in minutes.
46
  - πŸ’Ύ **Workspace Backup:** Chats and settings sync to a private HF Dataset via the `huggingface_hub` (Git fallback), preserving data automatically.
47
  - πŸ’“ **Always-On:** Built-in keep-alive pings prevent the HF Space from sleeping, so the assistant is always online.
48
+ - πŸ‘₯ **Multi-User Messaging:** Support for Telegram (multi-user) and WhatsApp (pairing).
49
+ - πŸ“Š **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
50
+ - πŸ”” **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
51
  - πŸ” **Flexible Auth:** Secure the Control UI with either a gateway token or password.
52
  - 🏠 **100% HF-Native:** Runs entirely on HuggingFace’s free infrastructure (2 vCPU, 16GB RAM).
53
 
 
91
 
92
  After restarting, the bot should appear online on Telegram.
93
 
94
+ ## πŸ’¬ WhatsApp Setup *(Optional)*
95
+
96
+ To use WhatsApp:
97
+
98
+ 1. Add the secret `WHATSAPP_ENABLED` and set it to `true`.
99
+ 2. Once the Space restarts, use the OpenClaw CLI to link your account:
100
+
101
+ ```bash
102
+ npm install -g openclaw@latest
103
+ openclaw channels login --gateway https://YOUR_SPACE_URL.hf.space --channel whatsapp
104
+ ```
105
+
106
+ 3. Scan the QR code with your phone (WhatsApp β†’ Linked Devices).
107
+
108
+ > [!NOTE]
109
+ > HuggingClaw uses dynamic DNS-over-HTTPS probing to ensure reliability on HuggingFace Spaces.
110
+
111
  ## πŸ’Ύ Workspace Backup *(Optional)*
112
 
113
  For persistent chat history and configuration:
 
117
 
118
  Optionally set `BACKUP_DATASET_NAME` (default: `huggingclaw-backup`) to choose the HF Dataset name. On first run, HuggingClaw will create (or use) the private Dataset repo `HF_USERNAME/SPACE-backup`, then restore your workspace on startup and sync changes every 10 minutes. The workspace is also saved on graceful shutdown. This ensures your data survives restarts.
119
 
120
+ ## πŸ“Š Dashboard & Monitoring
121
+
122
+ HuggingClaw now features a beautiful web dashboard. Access it by visiting your Space's URL on port 7861 or the internal health endpoint:
123
+
124
+ - **Uptime Tracking:** Real-time uptime monitoring.
125
+ - **Sync Status:** Visual indicators for workspace backup operations.
126
+ - **Model Info:** See which LLM is currently powering your assistant.
127
+
128
+ ## πŸ”” Webhooks *(Optional)*
129
+
130
+ Get notified when your Space restarts or if a backup fails:
131
+
132
+ - Set `WEBHOOK_URL` to your endpoint (e.g., Make.com, IFTTT, Discord Webhook).
133
+ - HuggingClaw sends a POST JSON payload with event details.
134
+
135
  ## βš™οΈ Full Configuration Reference
136
 
137
  See `.env.example` for complete settings. Key environment variables:
138
 
139
  ### Core
140
 
141
+ | Variable | Description |
142
+ |-----------------|-------------------------------------------------------------|
143
+ | `LLM_API_KEY` | LLM provider API key (e.g. OpenAI, Anthropic, etc.) |
144
+ | `LLM_MODEL` | Model ID (prefix `<provider>/`, auto-detected from prefix) |
145
+ | `GATEWAY_TOKEN` | Gateway token for Control UI access (required) |
146
 
147
  ### Background Services
148
 
149
+ | Variable | Default | Description |
150
+ |-----------------------|---------|---------------------------------------------|
151
+ | `KEEP_ALIVE_INTERVAL` | `300` | Self-ping interval in seconds (0 to disable)|
152
+ | `SYNC_INTERVAL` | `600` | Workspace sync interval (sec.) to HF Dataset|
153
 
154
  ### Security
155
 
156
+ | Variable | Description |
157
+ |----------------------|---------------------------------------------------------|
158
+ | `OPENCLAW_PASSWORD` | (optional) Enable simple password auth instead of token |
159
+ | `TRUSTED_PROXIES` | Comma-separated IPs of HF proxies |
160
+ | `ALLOWED_ORIGINS` | Comma-separated allowed origins for Control UI |
161
 
162
  ### Workspace Backup
163
 
164
+ | Variable | Default | Description |
165
+ |-----------------------|----------------------|--------------------------------------|
166
+ | `HF_USERNAME` | β€” | Your HuggingFace username |
167
+ | `HF_TOKEN` | β€” | HF token with write access |
168
+ | `BACKUP_DATASET_NAME` | `huggingclaw-backup` | Dataset name for backup repo |
169
+ | `WORKSPACE_GIT_USER` | `openclaw@example.com`| Git commit email for workspace sync |
170
+ | `WORKSPACE_GIT_NAME` | `OpenClaw Bot` | Git commit name for workspace sync |
171
 
172
  ### Advanced
173
 
174
+ | Variable | Default | Description |
175
+ |--------------------|----------|-------------------------------------|
176
+ | `OPENCLAW_VERSION` | `latest` | Pin a specific OpenClaw version |
177
+ | `HEALTH_PORT` | `7861` | Internal health endpoint port |
178
 
179
  ## πŸ€– LLM Providers
180
 
181
  HuggingClaw supports **all providers** from OpenClaw. Set `LLM_MODEL=<provider/model>` and the provider is auto-detected. For example:
182
 
183
+ | Provider | Prefix | Example Model | API Key Source |
184
+ |------------------|-----------------|---------------------------------------|------------------------------------------------------|
185
+ | **Anthropic** | `anthropic/` | `anthropic/claude-sonnet-4-6` | [Anthropic Console](https://console.anthropic.com/) |
186
+ | **OpenAI** | `openai/` | `openai/gpt-5.4` | [OpenAI Platform](https://platform.openai.com/) |
187
+ | **Google** | `google/` | `google/gemini-2.5-flash` | [AI Studio](https://ai.google.dev/) |
188
+ | **DeepSeek** | `deepseek/` | `deepseek/deepseek-v3.2` | [DeepSeek](https://platform.deepseek.com) |
189
+ | **xAI (Grok)** | `xai/` | `xai/grok-4` | [xAI](https://console.x.ai) |
190
+ | **Mistral** | `mistral/` | `mistral/mistral-large-latest` | [Mistral Console](https://console.mistral.ai) |
191
+ | **Moonshot** | `moonshot/` | `moonshot/kimi-k2.5` | [Moonshot](https://platform.moonshot.cn) |
192
+ | **Cohere** | `cohere/` | `cohere/command-a` | [Cohere Dashboard](https://dashboard.cohere.com) |
193
+ | **Groq** | `groq/` | `groq/mixtral-8x7b-32768` | [Groq](https://console.groq.com) |
194
+ | **MiniMax** | `minimax/` | `minimax/minimax-m2.7` | [MiniMax](https://platform.minimax.io) |
195
+ | **NVIDIA** | `nvidia/` | `nvidia/nemotron-3-super-120b-a12b` | [NVIDIA API](https://api.nvidia.com) |
196
+ | **Z.ai (GLM)** | `zai/` | `zai/glm-5` | [Z.ai](https://z.ai) |
197
+ | **Volcengine** | `volcengine/` | `volcengine/doubao-seed-1-8-251228` | [Volcengine](https://www.volcengine.com) |
198
+ | **HuggingFace** | `huggingface/` | `huggingface/deepseek-ai/DeepSeek-R1` | [HF Tokens](https://huggingface.co/settings/tokens) |
199
+ | **OpenCode Zen** | `opencode/` | `opencode/claude-opus-4-6` | [OpenCode.ai](https://opencode.ai/auth) |
200
+ | **OpenCode Go** | `opencode-go/` | `opencode-go/kimi-k2.5` | [OpenCode.ai](https://opencode.ai/auth) |
201
+ | **Kilo Gateway** | `kilocode/` | `kilocode/anthropic/claude-opus-4.6` | [Kilo.ai](https://kilo.ai) |
202
 
203
  ### OpenRouter – 200+ Models with One Key
204
 
dns-fix.js CHANGED
@@ -1,19 +1,108 @@
1
- // Fix HF Spaces DNS: internal resolver can't resolve discord.com / api.telegram.org
2
- // Override dns.lookup (used by http/https) to use Google/Cloudflare DNS
3
- const dns = require('dns');
4
- const { Resolver } = dns;
5
- const resolver = new Resolver();
6
- resolver.setServers(['8.8.8.8', '1.1.1.1']);
 
 
 
 
 
 
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  const origLookup = dns.lookup;
9
- dns.lookup = function(hostname, options, callback) {
10
- if (typeof options === 'function') { callback = options; options = { family: 0 }; }
11
- resolver.resolve4(hostname, (err, addresses) => {
12
- if (err || !addresses || !addresses.length) return origLookup.call(dns, hostname, options, callback);
13
- if (options && options.all) {
14
- callback(null, addresses.map(a => ({ address: a, family: 4 })));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  } else {
16
- callback(null, addresses[0], 4);
 
17
  }
18
  });
19
  };
 
1
+ /**
2
+ * DNS fix preload script for HF Spaces.
3
+ *
4
+ * Patches Node.js dns.lookup to:
5
+ * 1. Try system DNS first
6
+ * 2. Fall back to DNS-over-HTTPS (Cloudflare) if system DNS fails
7
+ * (This is needed because HF Spaces intercepts/blocks some domains like
8
+ * WhatsApp web or Telegram API via standard UDP DNS).
9
+ *
10
+ * Loaded via: NODE_OPTIONS="--require /opt/dns-fix.js"
11
+ */
12
+ "use strict";
13
 
14
+ const dns = require("dns");
15
+ const https = require("https");
16
+
17
+ // In-memory cache for runtime DoH resolutions
18
+ const runtimeCache = new Map(); // hostname -> { ip, expiry }
19
+
20
+ // DNS-over-HTTPS resolver
21
+ function dohResolve(hostname, callback) {
22
+ // Check runtime cache
23
+ const cached = runtimeCache.get(hostname);
24
+ if (cached && cached.expiry > Date.now()) {
25
+ return callback(null, cached.ip);
26
+ }
27
+
28
+ const url = `https://1.1.1.1/dns-query?name=${encodeURIComponent(hostname)}&type=A`;
29
+ const req = https.get(
30
+ url,
31
+ { headers: { Accept: "application/dns-json" }, timeout: 15000 },
32
+ (res) => {
33
+ let body = "";
34
+ res.on("data", (c) => (body += c));
35
+ res.on("end", () => {
36
+ try {
37
+ const data = JSON.parse(body);
38
+ const aRecords = (data.Answer || []).filter((a) => a.type === 1);
39
+ if (aRecords.length === 0) {
40
+ return callback(new Error(`DoH: no A record for ${hostname}`));
41
+ }
42
+ const ip = aRecords[0].data;
43
+ const ttl = Math.max((aRecords[0].TTL || 300) * 1000, 60000);
44
+ runtimeCache.set(hostname, { ip, expiry: Date.now() + ttl });
45
+ callback(null, ip);
46
+ } catch (e) {
47
+ callback(new Error(`DoH parse error: ${e.message}`));
48
+ }
49
+ });
50
+ }
51
+ );
52
+ req.on("error", (e) => callback(new Error(`DoH request failed: ${e.message}`)));
53
+ req.on("timeout", () => {
54
+ req.destroy();
55
+ callback(new Error("DoH request timed out"));
56
+ });
57
+ }
58
+
59
+ // Monkey-patch dns.lookup
60
  const origLookup = dns.lookup;
61
+
62
+ dns.lookup = function patchedLookup(hostname, options, callback) {
63
+ // Normalize arguments (options is optional, can be number or object)
64
+ if (typeof options === "function") {
65
+ callback = options;
66
+ options = {};
67
+ }
68
+ if (typeof options === "number") {
69
+ options = { family: options };
70
+ }
71
+ options = options || {};
72
+
73
+ // Skip patching for localhost, IPs, and internal domains
74
+ if (
75
+ !hostname ||
76
+ hostname === "localhost" ||
77
+ hostname === "0.0.0.0" ||
78
+ hostname === "127.0.0.1" ||
79
+ hostname === "::1" ||
80
+ /^\d+\.\d+\.\d+\.\d+$/.test(hostname) ||
81
+ /^::/.test(hostname)
82
+ ) {
83
+ return origLookup.call(dns, hostname, options, callback);
84
+ }
85
+
86
+ // 1) Try system DNS first
87
+ origLookup.call(dns, hostname, options, (err, address, family) => {
88
+ if (!err && address) {
89
+ return callback(null, address, family);
90
+ }
91
+
92
+ // 2) System DNS failed with ENOTFOUND or EAI_AGAIN β€” fall back to DoH
93
+ if (err && (err.code === "ENOTFOUND" || err.code === "EAI_AGAIN")) {
94
+ dohResolve(hostname, (dohErr, ip) => {
95
+ if (dohErr || !ip) {
96
+ return callback(err); // Return original error
97
+ }
98
+ if (options.all) {
99
+ return callback(null, [{ address: ip, family: 4 }]);
100
+ }
101
+ callback(null, ip, 4);
102
+ });
103
  } else {
104
+ // Other DNS errors β€” pass through
105
+ callback(err, address, family);
106
  }
107
  });
108
  };
health-server.js CHANGED
@@ -1,27 +1,302 @@
1
- // Lightweight health endpoint on port 7861
2
- // OpenClaw runs on 7860, this runs alongside it
3
- // Returns 200 OK for keep-alive pings and external monitoring
4
  const http = require('http');
 
 
5
 
6
  const PORT = process.env.HEALTH_PORT || 7861;
7
  const startTime = Date.now();
 
 
 
8
 
9
  const server = http.createServer((req, res) => {
10
- if (req.url === '/health' || req.url === '/') {
11
- const uptime = Math.floor((Date.now() - startTime) / 1000);
 
 
12
  res.writeHead(200, { 'Content-Type': 'application/json' });
13
  res.end(JSON.stringify({
14
  status: 'ok',
15
  uptime: uptime,
16
- uptimeHuman: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
17
  timestamp: new Date().toISOString()
18
  }));
19
- } else {
20
- res.writeHead(404);
21
- res.end();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  });
24
 
25
  server.listen(PORT, '0.0.0.0', () => {
26
- console.log(`πŸ₯ Health server listening on port ${PORT}`);
27
  });
 
1
+ // Lightweight health server and dashboard on port 7861
 
 
2
  const http = require('http');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
 
6
  const PORT = process.env.HEALTH_PORT || 7861;
7
  const startTime = Date.now();
8
+ const LLM_MODEL = process.env.LLM_MODEL || 'Not Set';
9
+ const WHATSAPP_ENABLED = process.env.WHATSAPP_ENABLED === 'true' || process.env.WHATSAPP_ENABLED === '1';
10
+ const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
11
 
12
  const server = http.createServer((req, res) => {
13
+ const uptime = Math.floor((Date.now() - startTime) / 1000);
14
+ const uptimeHuman = `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`;
15
+
16
+ if (req.url === '/health') {
17
  res.writeHead(200, { 'Content-Type': 'application/json' });
18
  res.end(JSON.stringify({
19
  status: 'ok',
20
  uptime: uptime,
21
+ uptimeHuman: uptimeHuman,
22
  timestamp: new Date().toISOString()
23
  }));
24
+ return;
25
+ }
26
+
27
+ if (req.url === '/status') {
28
+ let syncStatus = { status: 'unknown', message: 'No sync data yet' };
29
+ try {
30
+ if (fs.existsSync('/tmp/sync-status.json')) {
31
+ syncStatus = JSON.parse(fs.readFileSync('/tmp/sync-status.json', 'utf8'));
32
+ }
33
+ } catch (e) {}
34
+
35
+ res.writeHead(200, { 'Content-Type': 'application/json' });
36
+ res.end(JSON.stringify({
37
+ model: LLM_MODEL,
38
+ whatsapp: WHATSAPP_ENABLED,
39
+ telegram: TELEGRAM_ENABLED,
40
+ sync: syncStatus,
41
+ uptime: uptimeHuman
42
+ }));
43
+ return;
44
  }
45
+
46
+ if (req.url === '/') {
47
+ res.writeHead(200, { 'Content-Type': 'text/html' });
48
+ res.end(`
49
+ <!DOCTYPE html>
50
+ <html lang="en">
51
+ <head>
52
+ <meta charset="UTF-8">
53
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
54
+ <title>HuggingClaw Dashboard</title>
55
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
56
+ <style>
57
+ :root {
58
+ --bg: #0f172a;
59
+ --card-bg: rgba(30, 41, 59, 0.7);
60
+ --accent: linear-gradient(135deg, #3b82f6, #8b5cf6);
61
+ --text: #f8fafc;
62
+ --text-dim: #94a3b8;
63
+ --success: #10b981;
64
+ --error: #ef4444;
65
+ --warning: #f59e0b;
66
+ }
67
+
68
+ * { box-sizing: border-box; margin: 0; padding: 0; }
69
+
70
+ body {
71
+ font-family: 'Outfit', sans-serif;
72
+ background-color: var(--bg);
73
+ color: var(--text);
74
+ display: flex;
75
+ justify-content: center;
76
+ align-items: center;
77
+ min-height: 100vh;
78
+ overflow: hidden;
79
+ background-image:
80
+ radial-gradient(at 0% 0%, rgba(59, 130, 246, 0.15) 0px, transparent 50%),
81
+ radial-gradient(at 100% 0%, rgba(139, 92, 246, 0.15) 0px, transparent 50%);
82
+ }
83
+
84
+ .dashboard {
85
+ width: 90%;
86
+ max-width: 600px;
87
+ background: var(--card-bg);
88
+ backdrop-filter: blur(12px);
89
+ border: 1px solid rgba(255, 255, 255, 0.1);
90
+ border-radius: 24px;
91
+ padding: 40px;
92
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
93
+ animation: fadeIn 0.8s ease-out;
94
+ }
95
+
96
+ @keyframes fadeIn {
97
+ from { opacity: 0; transform: translateY(20px); }
98
+ to { opacity: 1; transform: translateY(0); }
99
+ }
100
+
101
+ header {
102
+ text-align: center;
103
+ margin-bottom: 40px;
104
+ }
105
+
106
+ h1 {
107
+ font-size: 2.5rem;
108
+ margin-bottom: 8px;
109
+ background: var(--accent);
110
+ -webkit-background-clip: text;
111
+ -webkit-text-fill-color: transparent;
112
+ font-weight: 600;
113
+ }
114
+
115
+ .subtitle {
116
+ color: var(--text-dim);
117
+ font-size: 0.9rem;
118
+ letter-spacing: 1px;
119
+ text-transform: uppercase;
120
+ }
121
+
122
+ .stats-grid {
123
+ display: grid;
124
+ grid-template-columns: repeat(2, 1fr);
125
+ gap: 20px;
126
+ margin-bottom: 30px;
127
+ }
128
+
129
+ .stat-card {
130
+ background: rgba(255, 255, 255, 0.03);
131
+ border: 1px solid rgba(255, 255, 255, 0.05);
132
+ padding: 20px;
133
+ border-radius: 16px;
134
+ transition: transform 0.3s ease, border-color 0.3s ease;
135
+ }
136
+
137
+ .stat-card:hover {
138
+ transform: translateY(-5px);
139
+ border-color: rgba(59, 130, 246, 0.3);
140
+ }
141
+
142
+ .stat-label {
143
+ color: var(--text-dim);
144
+ font-size: 0.75rem;
145
+ text-transform: uppercase;
146
+ margin-bottom: 8px;
147
+ display: block;
148
+ }
149
+
150
+ .stat-value {
151
+ font-size: 1.1rem;
152
+ font-weight: 600;
153
+ word-break: break-all;
154
+ }
155
+
156
+ .status-badge {
157
+ display: inline-flex;
158
+ align-items: center;
159
+ gap: 6px;
160
+ padding: 4px 12px;
161
+ border-radius: 20px;
162
+ font-size: 0.8rem;
163
+ font-weight: 600;
164
+ }
165
+
166
+ .status-online { background: rgba(16, 185, 129, 0.1); color: var(--success); }
167
+ .status-offline { background: rgba(239, 68, 68, 0.1); color: var(--error); }
168
+ .status-syncing { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
169
+
170
+ .pulse {
171
+ width: 8px;
172
+ height: 8px;
173
+ border-radius: 50%;
174
+ background: currentColor;
175
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
176
+ animation: pulse 2s infinite;
177
+ }
178
+
179
+ @keyframes pulse {
180
+ 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
181
+ 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
182
+ 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
183
+ }
184
+
185
+ .footer {
186
+ text-align: center;
187
+ color: var(--text-dim);
188
+ font-size: 0.8rem;
189
+ margin-top: 20px;
190
+ }
191
+
192
+ .sync-info {
193
+ background: rgba(255, 255, 255, 0.02);
194
+ padding: 15px;
195
+ border-radius: 12px;
196
+ font-size: 0.85rem;
197
+ color: var(--text-dim);
198
+ margin-top: 10px;
199
+ }
200
+
201
+ #sync-msg { color: var(--text); display: block; margin-top: 4px; }
202
+
203
+ </style>
204
+ </head>
205
+ <body>
206
+ <div class="dashboard">
207
+ <header>
208
+ <h1>🦞 HuggingClaw</h1>
209
+ <p class="subtitle">Space Control Panel</p>
210
+ </header>
211
+
212
+ <div class="stats-grid">
213
+ <div class="stat-card">
214
+ <span class="stat-label">Model</span>
215
+ <span class="stat-value" id="model-id">Loading...</span>
216
+ </div>
217
+ <div class="stat-card">
218
+ <span class="stat-label">Uptime</span>
219
+ <span class="stat-value" id="uptime">Loading...</span>
220
+ </div>
221
+ <div class="stat-card">
222
+ <span class="stat-label">WhatsApp</span>
223
+ <span id="wa-status">Loading...</span>
224
+ </div>
225
+ <div class="stat-card">
226
+ <span class="stat-label">Telegram</span>
227
+ <span id="tg-status">Loading...</span>
228
+ </div>
229
+ </div>
230
+
231
+ <div class="stat-card" style="width: 100%;">
232
+ <span class="stat-label">Workspace Sync Status</span>
233
+ <div id="sync-badge-container"></div>
234
+ <div class="sync-info">
235
+ Last Sync Activity: <span id="sync-time">Never</span>
236
+ <span id="sync-msg">Initializing synchronization...</span>
237
+ </div>
238
+ </div>
239
+
240
+ <div class="footer">
241
+ &bull; Live updates every 10s &bull;
242
+ </div>
243
+ </div>
244
+
245
+ <script>
246
+ async function updateStats() {
247
+ try {
248
+ const res = await fetch('/status');
249
+ const data = await res.json();
250
+
251
+ document.getElementById('model-id').textContent = data.model;
252
+ document.getElementById('uptime').textContent = data.uptime;
253
+
254
+ document.getElementById('wa-status').innerHTML = data.whatsapp
255
+ ? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
256
+ : '<div class="status-badge status-offline">Disabled</div>';
257
+
258
+ document.getElementById('tg-status').innerHTML = data.telegram
259
+ ? '<div class="status-badge status-online"><div class="pulse"></div>Active</div>'
260
+ : '<div class="status-badge status-offline">Disabled</div>';
261
+
262
+ const syncData = data.sync;
263
+ let badgeClass = 'status-offline';
264
+ let pulseHtml = '';
265
+
266
+ if (syncData.status === 'success') {
267
+ badgeClass = 'status-online';
268
+ pulseHtml = '<div class="pulse"></div>';
269
+ } else if (syncData.status === 'syncing') {
270
+ badgeClass = 'status-syncing';
271
+ pulseHtml = '<div class="pulse" style="background:#3b82f6"></div>';
272
+ } else if (syncData.status === 'error') {
273
+ badgeClass = 'status-offline';
274
+ }
275
+
276
+ document.getElementById('sync-badge-container').innerHTML =
277
+ '<div class="status-badge ' + badgeClass + '">' + pulseHtml + syncData.status.toUpperCase() + '</div>';
278
+
279
+ document.getElementById('sync-time').textContent = syncData.timestamp || 'Never';
280
+ document.getElementById('sync-msg').textContent = syncData.message || 'Waiting for first sync...';
281
+
282
+ } catch (e) {
283
+ console.error("Failed to fetch status", e);
284
+ }
285
+ }
286
+
287
+ updateStats();
288
+ setInterval(updateStats, 10000);
289
+ </script>
290
+ </body>
291
+ </html>
292
+ `);
293
+ return;
294
+ }
295
+
296
+ res.writeHead(404);
297
+ res.end();
298
  });
299
 
300
  server.listen(PORT, '0.0.0.0', () => {
301
+ console.log(`πŸ₯ Health server & Dashboard listening on port ${PORT}`);
302
  });
start.sh CHANGED
@@ -234,6 +234,12 @@ if [ -n "$TELEGRAM_BOT_TOKEN" ]; then
234
  fi
235
  fi
236
 
 
 
 
 
 
 
237
  # Write config
238
  echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
239
  chmod 600 /home/node/.openclaw/openclaw.json
@@ -249,6 +255,11 @@ printf " β”‚ %-40s β”‚\n" "Telegram: βœ… enabled"
249
  else
250
  printf " β”‚ %-40s β”‚\n" "Telegram: ❌ not configured"
251
  fi
 
 
 
 
 
252
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
253
  printf " β”‚ %-40s β”‚\n" "Backup: βœ… ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
254
  else
@@ -270,9 +281,20 @@ if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
270
  SYNC_STATUS="βœ… every ${SYNC_INTERVAL:-600}s"
271
  fi
272
  printf " β”‚ %-40s β”‚\n" "Auto-sync: $SYNC_STATUS"
 
 
 
273
  echo " β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜"
274
  echo ""
275
 
 
 
 
 
 
 
 
 
276
  # ── Trap SIGTERM for graceful shutdown ──
277
  graceful_shutdown() {
278
  echo ""
@@ -300,6 +322,7 @@ graceful_shutdown() {
300
  trap graceful_shutdown SIGTERM SIGINT
301
 
302
  # ── Start background services ──
 
303
  node /home/node/app/health-server.js &
304
  /home/node/app/keep-alive.sh &
305
 
@@ -308,8 +331,6 @@ python3 /home/node/app/workspace-sync.py &
308
  # ── Launch gateway ──
309
  echo "πŸš€ Launching OpenClaw gateway on port 7860..."
310
  echo ""
311
- # Set model via environment for the gateway
312
- export LLM_MODEL="$LLM_MODEL"
313
 
314
 
315
 
 
234
  fi
235
  fi
236
 
237
+ # WhatsApp
238
+ if [ "$WHATSAPP_ENABLED" = "true" ] || [ "$WHATSAPP_ENABLED" = "1" ]; then
239
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.whatsapp = {"enabled": true}')
240
+ CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.channels.whatsapp = {"dmPolicy": "pairing"}')
241
+ fi
242
+
243
  # Write config
244
  echo "$CONFIG_JSON" > "/home/node/.openclaw/openclaw.json"
245
  chmod 600 /home/node/.openclaw/openclaw.json
 
255
  else
256
  printf " β”‚ %-40s β”‚\n" "Telegram: ❌ not configured"
257
  fi
258
+ if [ "$WHATSAPP_ENABLED" = "true" ] || [ "$WHATSAPP_ENABLED" = "1" ]; then
259
+ printf " β”‚ %-40s β”‚\n" "WhatsApp: βœ… enabled"
260
+ else
261
+ printf " β”‚ %-40s β”‚\n" "WhatsApp: ❌ not configured"
262
+ fi
263
  if [ -n "$HF_USERNAME" ] && [ -n "$HF_TOKEN" ]; then
264
  printf " β”‚ %-40s β”‚\n" "Backup: βœ… ${HF_USERNAME}/${BACKUP_DATASET:-huggingclaw-backup}"
265
  else
 
281
  SYNC_STATUS="βœ… every ${SYNC_INTERVAL:-600}s"
282
  fi
283
  printf " β”‚ %-40s β”‚\n" "Auto-sync: $SYNC_STATUS"
284
+ if [ -n "$WEBHOOK_URL" ]; then
285
+ printf " β”‚ %-40s β”‚\n" "Webhooks: βœ… enabled"
286
+ fi
287
  echo " β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜"
288
  echo ""
289
 
290
+ # ── Trigger Webhook on Restart ──
291
+ if [ -n "$WEBHOOK_URL" ]; then
292
+ echo "πŸ”” Sending restart webhook..."
293
+ curl -s -X POST "$WEBHOOK_URL" \
294
+ -H "Content-Type: application/json" \
295
+ -d '{"event":"restart", "status":"success", "message":"HuggingClaw gateway has started/restarted.", "model": "'"$LLM_MODEL"'"}' >/dev/null 2>&1 &
296
+ fi
297
+
298
  # ── Trap SIGTERM for graceful shutdown ──
299
  graceful_shutdown() {
300
  echo ""
 
322
  trap graceful_shutdown SIGTERM SIGINT
323
 
324
  # ── Start background services ──
325
+ export LLM_MODEL="$LLM_MODEL"
326
  node /home/node/app/health-server.js &
327
  /home/node/app/keep-alive.sh &
328
 
 
331
  # ── Launch gateway ──
332
  echo "πŸš€ Launching OpenClaw gateway on port 7860..."
333
  echo ""
 
 
334
 
335
 
336
 
workspace-sync.py CHANGED
@@ -19,6 +19,7 @@ INTERVAL = int(os.environ.get("SYNC_INTERVAL", "600"))
19
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
20
  HF_USERNAME = os.environ.get("HF_USERNAME", "")
21
  BACKUP_DATASET = os.environ.get("BACKUP_DATASET_NAME", "huggingclaw-backup")
 
22
 
23
  running = True
24
 
@@ -42,6 +43,37 @@ def has_changes():
42
  except Exception:
43
  return False
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  def sync_with_hf_hub():
47
  """Sync workspace using huggingface_hub library."""
@@ -128,21 +160,32 @@ def main():
128
  continue
129
 
130
  ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
 
 
131
 
132
  if use_hf_hub:
133
  if sync_with_hf_hub():
134
  print(f"πŸ”„ Workspace sync (hf_hub): pushed changes ({ts})")
 
135
  else:
136
  # Fallback to git
137
  if sync_with_git():
138
  print(f"πŸ”„ Workspace sync (git fallback): pushed changes ({ts})")
 
139
  else:
140
- print(f"πŸ”„ Workspace sync: failed ({ts}), will retry")
 
 
 
141
  else:
142
  if sync_with_git():
143
  print(f"πŸ”„ Workspace sync (git): pushed changes ({ts})")
 
144
  else:
145
- print(f"πŸ”„ Workspace sync: push failed ({ts}), will retry")
 
 
 
146
 
147
 
148
  if __name__ == "__main__":
 
19
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
20
  HF_USERNAME = os.environ.get("HF_USERNAME", "")
21
  BACKUP_DATASET = os.environ.get("BACKUP_DATASET_NAME", "huggingclaw-backup")
22
+ WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
23
 
24
  running = True
25
 
 
43
  except Exception:
44
  return False
45
 
46
+ def write_sync_status(status, message=""):
47
+ """Write sync status to file for the health server dashboard."""
48
+ try:
49
+ import json
50
+ data = {
51
+ "status": status,
52
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
53
+ "message": message
54
+ }
55
+ with open("/tmp/sync-status.json", "w") as f:
56
+ json.dump(data, f)
57
+ except Exception as e:
58
+ print(f" ⚠️ Could not write sync status: {e}")
59
+
60
+ def trigger_webhook(event, status, message):
61
+ """Trigger webhook notification."""
62
+ if not WEBHOOK_URL:
63
+ return
64
+ try:
65
+ import urllib.request
66
+ import json
67
+ data = json.dumps({
68
+ "event": event,
69
+ "status": status,
70
+ "message": message,
71
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
72
+ }).encode('utf-8')
73
+ req = urllib.request.Request(WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
74
+ urllib.request.urlopen(req, timeout=10)
75
+ except Exception as e:
76
+ print(f" ⚠️ Webhook delivery failed: {e}")
77
 
78
  def sync_with_hf_hub():
79
  """Sync workspace using huggingface_hub library."""
 
160
  continue
161
 
162
  ts = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
163
+
164
+ write_sync_status("syncing", f"Starting sync at {ts}")
165
 
166
  if use_hf_hub:
167
  if sync_with_hf_hub():
168
  print(f"πŸ”„ Workspace sync (hf_hub): pushed changes ({ts})")
169
+ write_sync_status("success", "Successfully pushed to HF Hub")
170
  else:
171
  # Fallback to git
172
  if sync_with_git():
173
  print(f"πŸ”„ Workspace sync (git fallback): pushed changes ({ts})")
174
+ write_sync_status("success", "Successfully pushed via git fallback")
175
  else:
176
+ msg = f"Workspace sync: failed ({ts}), will retry"
177
+ print(f"πŸ”„ {msg}")
178
+ write_sync_status("error", msg)
179
+ trigger_webhook("sync", "error", msg)
180
  else:
181
  if sync_with_git():
182
  print(f"πŸ”„ Workspace sync (git): pushed changes ({ts})")
183
+ write_sync_status("success", "Successfully pushed via git")
184
  else:
185
+ msg = f"Workspace sync: push failed ({ts}), will retry"
186
+ print(f"πŸ”„ {msg}")
187
+ write_sync_status("error", msg)
188
+ trigger_webhook("sync", "error", msg)
189
 
190
 
191
  if __name__ == "__main__":