@yemi33/minions 0.1.812 → 0.1.814
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.
- package/CHANGELOG.md +5 -3
- package/dashboard/js/command-center.js +7 -4
- package/dashboard/js/render-prd.js +16 -1
- package/dashboard.js +111 -0
- package/docs/teams-production.md +368 -0
- package/docs/teams-setup.md +300 -0
- package/engine/ado.js +7 -0
- package/engine/cli.js +24 -4
- package/engine/github.js +7 -0
- package/engine/lifecycle.js +35 -13
- package/engine/preflight.js +13 -0
- package/engine/queries.js +1 -0
- package/engine/shared.js +9 -0
- package/engine/teams-cards.js +137 -0
- package/engine/teams.js +589 -0
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
3
|
+
## 0.1.814 (2026-04-11)
|
|
4
4
|
|
|
5
5
|
### Features
|
|
6
|
+
- Bidirectional Teams integration for Command Center (#764)
|
|
7
|
+
- show branch strategy on PRD view (#762)
|
|
6
8
|
- Restructure decompose.md sub-task output format (#655)
|
|
7
9
|
- CC race condition fixes, doc-chat expand, settings save fix, metric rate
|
|
8
10
|
- configurable ignored comment authors — auto-filtered, never trigger fixes
|
|
@@ -11,6 +13,8 @@
|
|
|
11
13
|
- cap review→fix cycles at evalMaxIterations (default 3)
|
|
12
14
|
|
|
13
15
|
### Fixes
|
|
16
|
+
- add smoke test for checkTimeouts ReferenceError regression (closes #775) (#822)
|
|
17
|
+
- replace safeWrite with mutateDispatch in CLI kill handler (#654)
|
|
14
18
|
- CC queue drain uses combined flush, not one-at-a-time while loop (#818)
|
|
15
19
|
- allow agents to explain rationale instead of blindly fixing review feedback
|
|
16
20
|
- don't URL-encode ADO branchName filter (refs/heads/ format expected raw)
|
|
@@ -29,8 +33,6 @@
|
|
|
29
33
|
- never downgrade reviewStatus from 'approved' unless explicitly rejected
|
|
30
34
|
- warn on invalid pipeline JSON instead of silently dropping + add test
|
|
31
35
|
- repair invalid JSON in daily-arch-improvement pipeline
|
|
32
|
-
- flush queued CC messages as single combined request
|
|
33
|
-
- human feedback fixes are never capped (only minion review→fix loop is)
|
|
34
36
|
|
|
35
37
|
## 0.1.782 (2026-04-10)
|
|
36
38
|
|
|
@@ -378,11 +378,14 @@ async function ccSend() {
|
|
|
378
378
|
}
|
|
379
379
|
var wasAborted = await _ccDoSend(message);
|
|
380
380
|
|
|
381
|
-
// Flush queued messages
|
|
382
|
-
|
|
383
|
-
|
|
381
|
+
// Flush queued messages one at a time, pausing after abort to let server release ccInFlight
|
|
382
|
+
while (tab._queue && tab._queue.length > 0) {
|
|
383
|
+
if (wasAborted) {
|
|
384
|
+
await new Promise(function(r) { setTimeout(r, 1500); });
|
|
385
|
+
}
|
|
386
|
+
var next = tab._queue.shift();
|
|
384
387
|
_renderQueueIndicator();
|
|
385
|
-
await _ccDoSend(
|
|
388
|
+
wasAborted = await _ccDoSend(next);
|
|
386
389
|
}
|
|
387
390
|
}
|
|
388
391
|
|
|
@@ -171,6 +171,12 @@ function renderPrdProgress(prog) {
|
|
|
171
171
|
const agentLabel = wiAgent ? '<span style="font-size:9px;color:var(--muted)" title="' + escHtml(wiAgent.name || wi.dispatched_to) + '">' +
|
|
172
172
|
(wiAgent.emoji || '') + ' ' + escHtml(wiAgent.name || wi.dispatched_to) + '</span>' : '';
|
|
173
173
|
|
|
174
|
+
// Branch label — show the target branch for this item
|
|
175
|
+
const wiBranch = wi ? (wi.branch || wi.featureBranch || (wi._artifacts && wi._artifacts.branch) || '') : '';
|
|
176
|
+
const branchLabel = wiBranch
|
|
177
|
+
? '<span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:3px;border:1px solid var(--border);font-family:monospace;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle" title="Branch: ' + escHtml(wiBranch) + '">🌿 ' + escHtml(wiBranch) + '</span>'
|
|
178
|
+
: '';
|
|
179
|
+
|
|
174
180
|
// Requeue button for failed items or PRD items with no work item (orphaned/deleted)
|
|
175
181
|
const canRequeue = (wi && (wi.status === 'failed' || i.status === 'failed')) ||
|
|
176
182
|
(!wi && i.status && i.status !== 'missing' && i.status !== 'done' && i.status !== 'planned');
|
|
@@ -195,6 +201,7 @@ function renderPrdProgress(prog) {
|
|
|
195
201
|
requeueBtn +
|
|
196
202
|
(projBadges ? '<span>' + projBadges + '</span>' : '') +
|
|
197
203
|
(prLinks ? '<span>' + prLinks + '</span>' : '') +
|
|
204
|
+
branchLabel +
|
|
198
205
|
'<span class="prd-item-priority ' + (i.priority || '') + '">' + escHtml(i.priority || '') + '</span>' +
|
|
199
206
|
'<span onclick="event.stopPropagation();prdItemRemove(\'' + src + '\',\'' + iid + '\')" style="color:var(--red);cursor:pointer;font-size:10px;padding:0 4px" title="Remove item">x</span>' +
|
|
200
207
|
(i.description ? '<div style="width:100%;font-size:11px;color:var(--muted);padding:2px 0 2px 42px;line-height:1.4">' + renderMd(i.description) + '</div>' : '') +
|
|
@@ -283,7 +290,9 @@ function renderPrdProgress(prog) {
|
|
|
283
290
|
return '<div style="font-size:11px;font-weight:600;color:var(--blue);margin-bottom:4px;padding:6px 8px;background:var(--surface2);border-radius:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
|
|
284
291
|
(g._projects.length > 0 ? g._projects.map(function(p) { return '<span class="prd-project-badge">' + escHtml(p) + '</span>'; }).join(' ') : '') +
|
|
285
292
|
'<span style="color:var(--text)">' + escHtml(summary || g.file) + '</span>' +
|
|
286
|
-
(g.branchStrategy === 'shared-branch'
|
|
293
|
+
(g.branchStrategy === 'shared-branch'
|
|
294
|
+
? '<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:rgba(210,153,34,0.15);color:var(--yellow);font-weight:400" title="All items commit to a single shared feature branch">🌳 shared branch</span>'
|
|
295
|
+
: '<span style="font-size:9px;padding:1px 5px;border-radius:3px;background:rgba(56,139,253,0.12);color:var(--blue);font-weight:400" title="Each item gets its own branch (work/P-xxx) and PR">🔀 parallel branches</span>') +
|
|
287
296
|
pausedLabel +
|
|
288
297
|
staleLabel +
|
|
289
298
|
'<span style="font-weight:700;font-size:11px;color:' + (done === g.items.length && g.items.length > 0 ? 'var(--green)' : 'var(--text)') + '">' + (g.items.length > 0 ? Math.round((done / g.items.length) * 100) : 0) + '%</span>' +
|
|
@@ -400,6 +409,12 @@ function renderPrdProgress(prog) {
|
|
|
400
409
|
return '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(rqId) + '\',\'' + escHtml(w ? (w._source || '') : '') + '\',\'' + escHtml(i.source || '') + '\')" style="font-size:8px;color:var(--green);cursor:pointer;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">retry</span>';
|
|
401
410
|
})() +
|
|
402
411
|
(deps ? '<span style="font-size:8px;color:var(--muted)" title="Depends on: ' + escHtml(deps) + '">deps: ' + escHtml(deps) + '</span>' : '') +
|
|
412
|
+
(function() {
|
|
413
|
+
var w = wi[i.id];
|
|
414
|
+
var b = w ? (w.branch || w.featureBranch || (w._artifacts && w._artifacts.branch) || '') : '';
|
|
415
|
+
if (!b) return '';
|
|
416
|
+
return '<span style="font-size:8px;color:var(--muted);font-family:monospace;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle" title="Branch: ' + escHtml(b) + '">🌿 ' + escHtml(b) + '</span>';
|
|
417
|
+
})() +
|
|
403
418
|
'</div>' +
|
|
404
419
|
((i.prs || []).length ? '<div style="margin-top:3px">' + (i.prs || []).map(function(pr) { return _renderPrLink(pr, { size: '9px' }); }).join(' ') + '</div>' : '') +
|
|
405
420
|
(i.status === 'decomposed' ? (function() {
|
package/dashboard.js
CHANGED
|
@@ -20,6 +20,7 @@ const _dashboardVersion = {
|
|
|
20
20
|
};
|
|
21
21
|
const shared = require('./engine/shared');
|
|
22
22
|
const queries = require('./engine/queries');
|
|
23
|
+
const teams = require('./engine/teams');
|
|
23
24
|
const os = require('os');
|
|
24
25
|
|
|
25
26
|
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS } = shared;
|
|
@@ -38,6 +39,7 @@ function reloadConfig() {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
|
|
42
|
+
const TEAMS_INBOX_PATH = path.join(ENGINE_DIR, 'teams-inbox.json');
|
|
41
43
|
|
|
42
44
|
// Resolve a plan/PRD file path: .json files live in prd/, .md files in plans/
|
|
43
45
|
// Validates that the file stays within the expected directory to prevent path traversal.
|
|
@@ -2214,6 +2216,9 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2214
2216
|
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
2215
2217
|
}
|
|
2216
2218
|
|
|
2219
|
+
// Teams notification for plan approval — non-blocking
|
|
2220
|
+
try { teams.teamsNotifyPlanEvent({ name: plan.plan_summary || body.file, file: body.file }, 'plan-approved').catch(() => {}); } catch {}
|
|
2221
|
+
|
|
2217
2222
|
invalidateStatusCache();
|
|
2218
2223
|
return jsonReply(res, 200, { ok: true, status: 'approved', resumedWorkItems: resumed });
|
|
2219
2224
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
@@ -2422,6 +2427,10 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
2422
2427
|
plan.rejectedBy = body.rejectedBy || os.userInfo().username;
|
|
2423
2428
|
if (body.reason) plan.rejectionReason = body.reason;
|
|
2424
2429
|
safeWrite(planPath, plan);
|
|
2430
|
+
|
|
2431
|
+
// Teams notification for plan rejection — non-blocking
|
|
2432
|
+
try { teams.teamsNotifyPlanEvent({ name: plan.plan_summary || body.file, file: body.file }, 'plan-rejected').catch(() => {}); } catch {}
|
|
2433
|
+
|
|
2425
2434
|
return jsonReply(res, 200, { ok: true, status: 'rejected' });
|
|
2426
2435
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
2427
2436
|
}
|
|
@@ -3442,6 +3451,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3442
3451
|
});
|
|
3443
3452
|
}
|
|
3444
3453
|
|
|
3454
|
+
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
3455
|
+
if (!tabId.startsWith('teams-')) {
|
|
3456
|
+
teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3445
3459
|
const reply = { ...parseCCActions(result.text), sessionId: ccSession.sessionId, newSession: !wasResume };
|
|
3446
3460
|
if (sessionReset) reply.sessionReset = true;
|
|
3447
3461
|
return jsonReply(res, 200, reply);
|
|
@@ -3555,6 +3569,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3555
3569
|
const donePayload = { type: 'done', text: displayText, actions, sessionId: responseSessionId, newSession: !wasResume };
|
|
3556
3570
|
if (sessionReset) donePayload.sessionReset = true;
|
|
3557
3571
|
res.write('data: ' + JSON.stringify(donePayload) + '\n\n');
|
|
3572
|
+
|
|
3573
|
+
// Mirror CC response to Teams (non-blocking, skip Teams-originated)
|
|
3574
|
+
const _streamTabId = body.tabId || 'default';
|
|
3575
|
+
if (!_streamTabId.startsWith('teams-')) {
|
|
3576
|
+
teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3558
3579
|
_ccStreamEnded = true; res.end();
|
|
3559
3580
|
} finally {
|
|
3560
3581
|
ccInFlightTabs.delete(tabId);
|
|
@@ -3850,6 +3871,93 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
3850
3871
|
}
|
|
3851
3872
|
}
|
|
3852
3873
|
|
|
3874
|
+
// ── Teams Bot Handler ─────────────────────────────────────────────────────
|
|
3875
|
+
|
|
3876
|
+
async function handleTeamsBot(req, res) {
|
|
3877
|
+
if (!teams.isTeamsEnabled()) {
|
|
3878
|
+
return jsonReply(res, 503, { error: 'Teams integration disabled' }, req);
|
|
3879
|
+
}
|
|
3880
|
+
const adapter = teams.createAdapter();
|
|
3881
|
+
if (!adapter) {
|
|
3882
|
+
return jsonReply(res, 503, { error: 'Teams adapter unavailable' }, req);
|
|
3883
|
+
}
|
|
3884
|
+
try {
|
|
3885
|
+
await adapter.process(req, res, async (context) => {
|
|
3886
|
+
const activity = context.activity;
|
|
3887
|
+
const cfg = teams.getTeamsConfig();
|
|
3888
|
+
|
|
3889
|
+
// Save conversation reference on install/member events
|
|
3890
|
+
if (activity.type === 'conversationUpdate' && activity.membersAdded?.length) {
|
|
3891
|
+
const ref = context.activity.conversation?.id;
|
|
3892
|
+
if (ref) {
|
|
3893
|
+
const convRef = {
|
|
3894
|
+
activityId: activity.id,
|
|
3895
|
+
user: activity.from,
|
|
3896
|
+
bot: activity.recipient,
|
|
3897
|
+
conversation: activity.conversation,
|
|
3898
|
+
channelId: activity.channelId,
|
|
3899
|
+
locale: activity.locale,
|
|
3900
|
+
serviceUrl: activity.serviceUrl,
|
|
3901
|
+
};
|
|
3902
|
+
teams.saveConversationRef(activity.conversation.id, convRef);
|
|
3903
|
+
shared.log('info', `Teams conversationUpdate: saved ref for ${activity.conversation.id}`);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
|
|
3907
|
+
if (activity.type === 'installationUpdate') {
|
|
3908
|
+
const convRef = {
|
|
3909
|
+
activityId: activity.id,
|
|
3910
|
+
user: activity.from,
|
|
3911
|
+
bot: activity.recipient,
|
|
3912
|
+
conversation: activity.conversation,
|
|
3913
|
+
channelId: activity.channelId,
|
|
3914
|
+
locale: activity.locale,
|
|
3915
|
+
serviceUrl: activity.serviceUrl,
|
|
3916
|
+
};
|
|
3917
|
+
if (activity.conversation?.id) {
|
|
3918
|
+
teams.saveConversationRef(activity.conversation.id, convRef);
|
|
3919
|
+
shared.log('info', `Teams installationUpdate: saved ref for ${activity.conversation.id}`);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
// Handle incoming messages
|
|
3924
|
+
if (activity.type === 'message' && activity.text) {
|
|
3925
|
+
// Filter bot's own echo messages
|
|
3926
|
+
if (activity.from?.id === cfg.appId) return;
|
|
3927
|
+
|
|
3928
|
+
const msgId = `teams-${Date.now()}-${shared.uid()}`;
|
|
3929
|
+
const convRef = {
|
|
3930
|
+
activityId: activity.id,
|
|
3931
|
+
user: activity.from,
|
|
3932
|
+
bot: activity.recipient,
|
|
3933
|
+
conversation: activity.conversation,
|
|
3934
|
+
channelId: activity.channelId,
|
|
3935
|
+
locale: activity.locale,
|
|
3936
|
+
serviceUrl: activity.serviceUrl,
|
|
3937
|
+
};
|
|
3938
|
+
mutateJsonFileLocked(TEAMS_INBOX_PATH, (inbox) => {
|
|
3939
|
+
if (!Array.isArray(inbox)) inbox = [];
|
|
3940
|
+
inbox.push({
|
|
3941
|
+
id: msgId,
|
|
3942
|
+
text: activity.text,
|
|
3943
|
+
from: activity.from?.name || activity.from?.id || 'unknown',
|
|
3944
|
+
conversationRef: convRef,
|
|
3945
|
+
receivedAt: new Date().toISOString(),
|
|
3946
|
+
_processedAt: null,
|
|
3947
|
+
});
|
|
3948
|
+
return inbox;
|
|
3949
|
+
}, { defaultValue: [] });
|
|
3950
|
+
shared.log('info', `Teams message received from ${activity.from?.name || 'unknown'}: ${activity.text.slice(0, 80)}`);
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3953
|
+
} catch (err) {
|
|
3954
|
+
shared.log('warn', `Teams bot handler error: ${err.message}`);
|
|
3955
|
+
if (!res.headersSent) {
|
|
3956
|
+
return jsonReply(res, 500, { error: 'Bot processing failed' }, req);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3853
3961
|
// ── Route Registry ──────────────────────────────────────────────────────────
|
|
3854
3962
|
// Order matters: specific routes before general ones (e.g., /api/plans/approve before /api/plans/:file)
|
|
3855
3963
|
|
|
@@ -4453,6 +4561,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4453
4561
|
{ method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent config', params: 'engine?, claude?, agents?', handler: handleSettingsUpdate },
|
|
4454
4562
|
{ method: 'POST', path: '/api/settings/routing', desc: 'Update routing.md', params: 'content', handler: handleSettingsRouting },
|
|
4455
4563
|
{ method: 'POST', path: '/api/settings/reset', desc: 'Reset engine + claude + agent settings to defaults', handler: handleSettingsReset },
|
|
4564
|
+
|
|
4565
|
+
// Teams Bot Framework webhook
|
|
4566
|
+
{ method: 'POST', path: '/api/bot', desc: 'Bot Framework webhook for Teams integration', handler: handleTeamsBot },
|
|
4456
4567
|
];
|
|
4457
4568
|
|
|
4458
4569
|
// ── Route Dispatcher ────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# Teams Production Endpoint Migration
|
|
2
|
+
|
|
3
|
+
Guide for migrating the Minions Teams bot from a Dev Tunnel to a stable public HTTPS endpoint for production use. Choose one of the three deployment options below based on your infrastructure.
|
|
4
|
+
|
|
5
|
+
**Key fact:** The Azure Bot messaging endpoint URL can be changed at any time in the Azure Portal — it takes effect immediately. No bot reinstallation is needed in Teams. This means you can switch between Dev Tunnel and production endpoints freely.
|
|
6
|
+
|
|
7
|
+
**Prerequisites:**
|
|
8
|
+
|
|
9
|
+
- A working Teams integration via Dev Tunnel (see [docs/teams-setup.md](teams-setup.md))
|
|
10
|
+
- Azure CLI installed (`az --version`) for Options 1 and 2
|
|
11
|
+
- A public-facing server or VM for Option 3
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Option 1: Azure App Service
|
|
16
|
+
|
|
17
|
+
Deploy the Minions dashboard as an Azure App Service with a stable FQDN.
|
|
18
|
+
|
|
19
|
+
### Steps
|
|
20
|
+
|
|
21
|
+
1. **Create an App Service Plan** (skip if you have one):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
az appservice plan create \
|
|
25
|
+
--name minions-plan \
|
|
26
|
+
--resource-group rg-minions \
|
|
27
|
+
--sku B1 \
|
|
28
|
+
--is-linux
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. **Create the Web App:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
az webapp create \
|
|
35
|
+
--name minions-dashboard \
|
|
36
|
+
--resource-group rg-minions \
|
|
37
|
+
--plan minions-plan \
|
|
38
|
+
--runtime "NODE:20-lts"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This creates a publicly accessible URL: `https://minions-dashboard.azurewebsites.net`
|
|
42
|
+
|
|
43
|
+
3. **Configure environment variables:**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
az webapp config appsettings set \
|
|
47
|
+
--name minions-dashboard \
|
|
48
|
+
--resource-group rg-minions \
|
|
49
|
+
--settings \
|
|
50
|
+
PORT=8080 \
|
|
51
|
+
NODE_ENV=production
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> Azure App Service routes external port 443 (HTTPS) to your app's internal port (default 8080). Set `PORT=8080` so the dashboard listens on the expected port.
|
|
55
|
+
|
|
56
|
+
4. **Deploy the code:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# From the minions repository root
|
|
60
|
+
az webapp deploy \
|
|
61
|
+
--name minions-dashboard \
|
|
62
|
+
--resource-group rg-minions \
|
|
63
|
+
--src-path . \
|
|
64
|
+
--type zip
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Alternatively, configure continuous deployment from your Git repository:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
az webapp deployment source config \
|
|
71
|
+
--name minions-dashboard \
|
|
72
|
+
--resource-group rg-minions \
|
|
73
|
+
--repo-url https://github.com/your-org/minions \
|
|
74
|
+
--branch master \
|
|
75
|
+
--manual-integration
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
5. **Copy `config.json` to the App Service.** The simplest approach is to use the Kudu console or App Service Editor to upload your `config.json` to the application root. Alternatively, mount an Azure File Share containing your config.
|
|
79
|
+
|
|
80
|
+
6. **Update the Azure Bot messaging endpoint:**
|
|
81
|
+
|
|
82
|
+
- Open the [Azure Portal](https://portal.azure.com) > your Azure Bot resource > **Configuration**.
|
|
83
|
+
- Change the **Messaging endpoint** to:
|
|
84
|
+
```
|
|
85
|
+
https://minions-dashboard.azurewebsites.net/api/bot
|
|
86
|
+
```
|
|
87
|
+
- Click **Apply**. The change takes effect immediately.
|
|
88
|
+
|
|
89
|
+
### Verify
|
|
90
|
+
|
|
91
|
+
1. Open `https://minions-dashboard.azurewebsites.net/api/routes` in a browser — you should see the API route list.
|
|
92
|
+
2. In the Azure Bot resource, click **Test in Web Chat** and send a message.
|
|
93
|
+
3. Send a message in Teams to the bot — confirm it receives and responds.
|
|
94
|
+
|
|
95
|
+
### Rollback
|
|
96
|
+
|
|
97
|
+
To revert to Dev Tunnel:
|
|
98
|
+
|
|
99
|
+
1. Start your local Dev Tunnel: `devtunnel host -p 7331 --allow-anonymous`
|
|
100
|
+
2. Update the Azure Bot messaging endpoint back to your tunnel URL: `https://<tunnel>.devtunnels.ms/api/bot`
|
|
101
|
+
3. Click **Apply**. Traffic returns to your local machine immediately.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Option 2: Azure Container App
|
|
106
|
+
|
|
107
|
+
Containerize the dashboard and deploy to Azure Container Apps with a stable FQDN.
|
|
108
|
+
|
|
109
|
+
### Steps
|
|
110
|
+
|
|
111
|
+
1. **Create a Dockerfile** in the repository root:
|
|
112
|
+
|
|
113
|
+
```dockerfile
|
|
114
|
+
FROM node:20-slim
|
|
115
|
+
WORKDIR /app
|
|
116
|
+
COPY package*.json ./
|
|
117
|
+
RUN npm ci --omit=dev 2>/dev/null || true
|
|
118
|
+
COPY . .
|
|
119
|
+
EXPOSE 7331
|
|
120
|
+
CMD ["node", "dashboard.js"]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
> Minions has zero npm dependencies beyond Node.js built-ins, so `npm ci` may be a no-op. The botbuilder package (added by this feature branch) is the exception.
|
|
124
|
+
|
|
125
|
+
2. **Build and push to Azure Container Registry:**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Create a container registry (skip if you have one)
|
|
129
|
+
az acr create \
|
|
130
|
+
--name minionsacr \
|
|
131
|
+
--resource-group rg-minions \
|
|
132
|
+
--sku Basic
|
|
133
|
+
|
|
134
|
+
# Build and push
|
|
135
|
+
az acr build \
|
|
136
|
+
--registry minionsacr \
|
|
137
|
+
--image minions-dashboard:latest .
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
3. **Create a Container Apps environment** (skip if you have one):
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
az containerapp env create \
|
|
144
|
+
--name minions-env \
|
|
145
|
+
--resource-group rg-minions \
|
|
146
|
+
--location eastus
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
4. **Deploy the container:**
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
az containerapp create \
|
|
153
|
+
--name minions-dashboard \
|
|
154
|
+
--resource-group rg-minions \
|
|
155
|
+
--environment minions-env \
|
|
156
|
+
--image minionsacr.azurecr.io/minions-dashboard:latest \
|
|
157
|
+
--registry-server minionsacr.azurecr.io \
|
|
158
|
+
--target-port 7331 \
|
|
159
|
+
--ingress external \
|
|
160
|
+
--min-replicas 1 \
|
|
161
|
+
--max-replicas 1 \
|
|
162
|
+
--env-vars NODE_ENV=production
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
> Use `--min-replicas 1 --max-replicas 1` because the Minions engine uses file-based state that doesn't support multiple replicas.
|
|
166
|
+
|
|
167
|
+
5. **Get the FQDN:**
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
az containerapp show \
|
|
171
|
+
--name minions-dashboard \
|
|
172
|
+
--resource-group rg-minions \
|
|
173
|
+
--query "properties.configuration.ingress.fqdn" \
|
|
174
|
+
--output tsv
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This returns something like: `minions-dashboard.happyfield-abc123.eastus.azurecontainerapps.io`
|
|
178
|
+
|
|
179
|
+
6. **Update the Azure Bot messaging endpoint:**
|
|
180
|
+
|
|
181
|
+
- Open the [Azure Portal](https://portal.azure.com) > your Azure Bot resource > **Configuration**.
|
|
182
|
+
- Change the **Messaging endpoint** to:
|
|
183
|
+
```
|
|
184
|
+
https://minions-dashboard.happyfield-abc123.eastus.azurecontainerapps.io/api/bot
|
|
185
|
+
```
|
|
186
|
+
- Click **Apply**. The change takes effect immediately.
|
|
187
|
+
|
|
188
|
+
### Verify
|
|
189
|
+
|
|
190
|
+
1. Open `https://<your-fqdn>/api/routes` in a browser.
|
|
191
|
+
2. Test via Azure Bot **Test in Web Chat**.
|
|
192
|
+
3. Send a message in Teams — confirm end-to-end flow works.
|
|
193
|
+
|
|
194
|
+
### Rollback
|
|
195
|
+
|
|
196
|
+
To revert to Dev Tunnel:
|
|
197
|
+
|
|
198
|
+
1. Start your local Dev Tunnel: `devtunnel host -p 7331 --allow-anonymous`
|
|
199
|
+
2. Update the Azure Bot messaging endpoint back to your tunnel URL.
|
|
200
|
+
3. Click **Apply**. Immediate switchover.
|
|
201
|
+
|
|
202
|
+
Optionally stop the container to save costs:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
az containerapp update \
|
|
206
|
+
--name minions-dashboard \
|
|
207
|
+
--resource-group rg-minions \
|
|
208
|
+
--min-replicas 0 --max-replicas 0
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Option 3: Reverse Proxy (nginx / Caddy)
|
|
214
|
+
|
|
215
|
+
For servers with a public IP address or an existing reverse proxy setup.
|
|
216
|
+
|
|
217
|
+
### Steps (Caddy — recommended for simplicity)
|
|
218
|
+
|
|
219
|
+
Caddy automatically provisions and renews TLS certificates via Let's Encrypt.
|
|
220
|
+
|
|
221
|
+
1. **Install Caddy:**
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Debian/Ubuntu
|
|
225
|
+
sudo apt install -y caddy
|
|
226
|
+
|
|
227
|
+
# macOS
|
|
228
|
+
brew install caddy
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
2. **Configure Caddy.** Create or edit `/etc/caddy/Caddyfile`:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
minions.yourdomain.com {
|
|
235
|
+
reverse_proxy localhost:7331
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
> Replace `minions.yourdomain.com` with your actual domain. Ensure a DNS A record points this domain to your server's public IP.
|
|
240
|
+
|
|
241
|
+
3. **Start Caddy:**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
sudo systemctl enable --now caddy
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Caddy automatically obtains a Let's Encrypt TLS certificate for your domain.
|
|
248
|
+
|
|
249
|
+
4. **Start the Minions dashboard:**
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
minions dash
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Or run it as a systemd service for persistence:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# /etc/systemd/system/minions-dashboard.service
|
|
259
|
+
[Unit]
|
|
260
|
+
Description=Minions Dashboard
|
|
261
|
+
After=network.target
|
|
262
|
+
|
|
263
|
+
[Service]
|
|
264
|
+
Type=simple
|
|
265
|
+
User=your-user
|
|
266
|
+
WorkingDirectory=/path/to/minions
|
|
267
|
+
ExecStart=/usr/bin/node dashboard.js
|
|
268
|
+
Restart=on-failure
|
|
269
|
+
|
|
270
|
+
[Install]
|
|
271
|
+
WantedBy=multi-user.target
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
sudo systemctl enable --now minions-dashboard
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
5. **Update the Azure Bot messaging endpoint:**
|
|
279
|
+
|
|
280
|
+
- Open the Azure Portal > your Azure Bot resource > **Configuration**.
|
|
281
|
+
- Change the **Messaging endpoint** to:
|
|
282
|
+
```
|
|
283
|
+
https://minions.yourdomain.com/api/bot
|
|
284
|
+
```
|
|
285
|
+
- Click **Apply**. The change takes effect immediately.
|
|
286
|
+
|
|
287
|
+
### Steps (nginx)
|
|
288
|
+
|
|
289
|
+
1. **Install nginx and certbot:**
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
sudo apt install -y nginx certbot python3-certbot-nginx
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
2. **Configure nginx.** Create `/etc/nginx/sites-available/minions`:
|
|
296
|
+
|
|
297
|
+
```nginx
|
|
298
|
+
server {
|
|
299
|
+
listen 80;
|
|
300
|
+
server_name minions.yourdomain.com;
|
|
301
|
+
|
|
302
|
+
location / {
|
|
303
|
+
proxy_pass http://localhost:7331;
|
|
304
|
+
proxy_http_version 1.1;
|
|
305
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
306
|
+
proxy_set_header Connection 'upgrade';
|
|
307
|
+
proxy_set_header Host $host;
|
|
308
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
309
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
310
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
sudo ln -s /etc/nginx/sites-available/minions /etc/nginx/sites-enabled/
|
|
317
|
+
sudo nginx -t && sudo systemctl reload nginx
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
3. **Obtain a TLS certificate:**
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
sudo certbot --nginx -d minions.yourdomain.com
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Certbot modifies the nginx config to add TLS and sets up auto-renewal.
|
|
327
|
+
|
|
328
|
+
4. **Start the Minions dashboard** (same as Caddy option above).
|
|
329
|
+
|
|
330
|
+
5. **Update the Azure Bot messaging endpoint** (same as Caddy option above).
|
|
331
|
+
|
|
332
|
+
### Verify
|
|
333
|
+
|
|
334
|
+
1. Open `https://minions.yourdomain.com/api/routes` in a browser — confirm the route list loads over HTTPS.
|
|
335
|
+
2. Check the TLS certificate: `curl -vI https://minions.yourdomain.com 2>&1 | grep "SSL certificate"`.
|
|
336
|
+
3. Test via Azure Bot **Test in Web Chat**.
|
|
337
|
+
4. Send a message in Teams — confirm the bot responds.
|
|
338
|
+
|
|
339
|
+
### Rollback
|
|
340
|
+
|
|
341
|
+
To revert to Dev Tunnel:
|
|
342
|
+
|
|
343
|
+
1. Start your local Dev Tunnel: `devtunnel host -p 7331 --allow-anonymous`
|
|
344
|
+
2. Update the Azure Bot messaging endpoint back to your tunnel URL.
|
|
345
|
+
3. Click **Apply**. Immediate switchover.
|
|
346
|
+
|
|
347
|
+
The reverse proxy can remain running — it just won't receive Bot Framework traffic until the endpoint is pointed back.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Choosing an Option
|
|
352
|
+
|
|
353
|
+
| Criteria | App Service | Container App | Reverse Proxy |
|
|
354
|
+
|----------|-------------|---------------|---------------|
|
|
355
|
+
| Setup complexity | Medium | Medium | Low (Caddy) / Medium (nginx) |
|
|
356
|
+
| TLS management | Automatic | Automatic | Automatic (Caddy/certbot) |
|
|
357
|
+
| Cost | ~$13/mo (B1) | Pay-per-use | Free (your server + Let's Encrypt) |
|
|
358
|
+
| Custom domain | Supported | Supported | Required |
|
|
359
|
+
| Scaling | Supported but not needed | Supported but not needed | Manual |
|
|
360
|
+
| Best for | Azure-native teams | Container workflows | Existing servers |
|
|
361
|
+
|
|
362
|
+
> **Note on replicas:** Minions uses file-based state (`engine/*.json`). Do not run multiple replicas — use exactly 1 instance. All three options above default to single-instance deployment.
|
|
363
|
+
|
|
364
|
+
## Common Notes
|
|
365
|
+
|
|
366
|
+
- **Endpoint changes are immediate.** When you update the messaging endpoint in the Azure Bot Configuration, it takes effect right away. No bot reinstallation, no downtime, no user-visible change in Teams.
|
|
367
|
+
- **No deprecated webhooks.** This guide uses Azure Bot Framework exclusively. Do not use deprecated O365 Connector webhooks or Power Automate flows — they are being removed by Microsoft.
|
|
368
|
+
- **Config portability.** The same `config.json` works across all environments. Just ensure the `teams.appId` and `teams.appPassword` are correct for the bot registration that points to your production URL.
|