akenomainx4 commited on
Commit
368f964
·
verified ·
1 Parent(s): 5196cde

fix: detect rate-limit error, auto-retry once after 30s with friendly message

Browse files
Files changed (3) hide show
  1. app.js +37 -5
  2. public/app.js +13 -2
  3. public/style.css +1 -0
app.js CHANGED
@@ -107,8 +107,10 @@ function broadcast(payload) {
107
  }
108
  }
109
 
 
 
110
  function startShellular() {
111
- if (shellularProc) return;
112
 
113
  broadcast({ type: 'status', status: 'starting' });
114
 
@@ -117,8 +119,12 @@ function startShellular() {
117
  stdio: ['ignore', 'pipe', 'pipe'],
118
  });
119
 
 
 
 
120
  const handleData = (chunk) => {
121
  const text = stripAnsi(chunk.toString());
 
122
  outputBuffer += text;
123
  broadcast({ type: 'output', text });
124
  };
@@ -135,17 +141,43 @@ function startShellular() {
135
  });
136
 
137
  shellularProc.on('exit', (code, signal) => {
138
- const text = `\n[shellular exited — code=${code ?? '?'}, signal=${signal ?? 'none'}]\n`;
139
- outputBuffer += text;
140
- broadcast({ type: 'output', text });
141
  shellularProc = null;
142
- broadcast({ type: 'status', status: 'stopped' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  });
144
 
145
  broadcast({ type: 'status', status: 'running' });
146
  }
147
 
148
  function stopShellular() {
 
149
  if (!shellularProc) return;
150
  shellularProc.kill('SIGTERM');
151
  shellularProc = null;
 
107
  }
108
  }
109
 
110
+ let retryTimer = null;
111
+
112
  function startShellular() {
113
+ if (shellularProc || retryTimer) return;
114
 
115
  broadcast({ type: 'status', status: 'starting' });
116
 
 
119
  stdio: ['ignore', 'pipe', 'pipe'],
120
  });
121
 
122
+ // Accumulate stdout/stderr so we can detect the error type on exit
123
+ let procOutput = '';
124
+
125
  const handleData = (chunk) => {
126
  const text = stripAnsi(chunk.toString());
127
+ procOutput += text;
128
  outputBuffer += text;
129
  broadcast({ type: 'output', text });
130
  };
 
141
  });
142
 
143
  shellularProc.on('exit', (code, signal) => {
 
 
 
144
  shellularProc = null;
145
+
146
+ // Detect rate-limit / registration failure (exit code 1, no signal)
147
+ const isRegError = code === 1 && !signal &&
148
+ (procOutput.includes('invalid_union') || procOutput.includes('Too many requests') ||
149
+ procOutput.includes('host registration'));
150
+
151
+ if (isRegError) {
152
+ const WAIT = 30;
153
+ const msg = `\n⚠ Registration rate-limited by shellular API.\n` +
154
+ ` Retrying automatically in ${WAIT}s — please wait…\n`;
155
+ outputBuffer += msg;
156
+ broadcast({ type: 'output', text: msg });
157
+ broadcast({ type: 'status', status: 'retrying' });
158
+
159
+ retryTimer = setTimeout(() => {
160
+ retryTimer = null;
161
+ const msg2 = '\n[Retrying registration…]\n';
162
+ outputBuffer += msg2;
163
+ broadcast({ type: 'output', text: msg2 });
164
+ startShellular();
165
+ }, WAIT * 1000);
166
+ } else {
167
+ const text = code !== 0
168
+ ? `\n[shellular exited — code=${code ?? '?'}, signal=${signal ?? 'none'}]\n`
169
+ : '\n[shellular disconnected]\n';
170
+ outputBuffer += text;
171
+ broadcast({ type: 'output', text });
172
+ broadcast({ type: 'status', status: 'stopped' });
173
+ }
174
  });
175
 
176
  broadcast({ type: 'status', status: 'running' });
177
  }
178
 
179
  function stopShellular() {
180
+ if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }
181
  if (!shellularProc) return;
182
  shellularProc.kill('SIGTERM');
183
  shellularProc = null;
public/app.js CHANGED
@@ -184,9 +184,20 @@ function handleEvent(payload) {
184
  function updateStatus(status) {
185
  shellStatus = status;
186
 
187
- const labels = { running: 'Running', starting: 'Starting', stopped: 'Stopped', error: 'Error' };
 
 
 
 
 
 
188
  statusBadge.textContent = labels[status] || status;
189
- statusBadge.className = `badge badge-${status}`;
 
 
 
 
 
190
 
191
  if (status === 'stopped' || status === 'error') {
192
  if (!qrRendered) {
 
184
  function updateStatus(status) {
185
  shellStatus = status;
186
 
187
+ const labels = {
188
+ running: 'Running',
189
+ starting: 'Starting',
190
+ retrying: 'Retrying…',
191
+ stopped: 'Stopped',
192
+ error: 'Error',
193
+ };
194
  statusBadge.textContent = labels[status] || status;
195
+ statusBadge.className = `badge badge-${status}`;
196
+
197
+ if (status === 'retrying') {
198
+ if (!qrRendered) setQrState('loading');
199
+ return; // keep showing the spinner while we wait
200
+ }
201
 
202
  if (status === 'stopped' || status === 'error') {
203
  if (!qrRendered) {
public/style.css CHANGED
@@ -242,6 +242,7 @@ html, body {
242
  }
243
  .badge-running { background: rgba(74,222,128,.15); color: var(--success); }
244
  .badge-starting { background: rgba(250,204,21,.15); color: var(--warning); }
 
245
  .badge-stopped { background: rgba(255,95,109,.12); color: var(--error); }
246
  .badge-error { background: rgba(255,95,109,.12); color: var(--error); }
247
 
 
242
  }
243
  .badge-running { background: rgba(74,222,128,.15); color: var(--success); }
244
  .badge-starting { background: rgba(250,204,21,.15); color: var(--warning); }
245
+ .badge-retrying { background: rgba(250,204,21,.15); color: var(--warning); }
246
  .badge-stopped { background: rgba(255,95,109,.12); color: var(--error); }
247
  .badge-error { background: rgba(255,95,109,.12); color: var(--error); }
248