MS Teams integrations
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# DepreCore
|
# DepreCore
|
||||||
|
|
||||||
DepreCore is a fixed asset management and depreciation SPA for small and mid-sized organizations. This build includes an Express API, Vue/Vuetify frontend, SQLite persistence, role-aware login, asset register, templates, JSON tax-rule sets, depreciation reports, import/export, and admin configuration.
|
DepreCore is a fixed asset management and depreciation platform for small and mid-sized organizations. This build includes an Express API, Vue/Vuetify frontend, SQLite persistence, role-aware login, asset register, templates, JSON tax-rule sets, depreciation reports, import/export, and admin configuration.
|
||||||
|
|
||||||
Each asset carries six independent books (GAAP, Federal, State, AMT, ACE, User-defined), each with its own cost, depreciation method, useful life, §179 amount, bonus percentage, and convention. The asset workbench also includes a File Cabinet (invoices, photos, manuals), warranty/service-agreement tracking, a per-asset task list, and a running notes timeline.
|
Each asset carries six independent books (GAAP, Federal, State, AMT, ACE, User-defined), each with its own cost, depreciation method, useful life, §179 amount, bonus percentage, and convention. The asset workbench also includes a File Cabinet (invoices, photos, manuals), warranty/service-agreement tracking, a per-asset task list, and a running notes timeline.
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const profileRoutes = require('./routes/profile');
|
|||||||
const referenceRoutes = require('./routes/reference');
|
const referenceRoutes = require('./routes/reference');
|
||||||
const reportRoutes = require('./routes/reports');
|
const reportRoutes = require('./routes/reports');
|
||||||
const serviceNowRoutes = require('./routes/servicenow');
|
const serviceNowRoutes = require('./routes/servicenow');
|
||||||
|
const teamsRoutes = require('./routes/teams');
|
||||||
const taxRuleRoutes = require('./routes/taxRules');
|
const taxRuleRoutes = require('./routes/taxRules');
|
||||||
const templateRoutes = require('./routes/templates');
|
const templateRoutes = require('./routes/templates');
|
||||||
const workdayRoutes = require('./routes/workday');
|
const workdayRoutes = require('./routes/workday');
|
||||||
@@ -61,6 +62,7 @@ function createApp() {
|
|||||||
app.use('/api', taxRuleRoutes);
|
app.use('/api', taxRuleRoutes);
|
||||||
app.use('/api', reportRoutes);
|
app.use('/api', reportRoutes);
|
||||||
app.use('/api', serviceNowRoutes);
|
app.use('/api', serviceNowRoutes);
|
||||||
|
app.use('/api', teamsRoutes);
|
||||||
app.use('/api', dataPortabilityRoutes);
|
app.use('/api', dataPortabilityRoutes);
|
||||||
app.use('/api', adminRoutes);
|
app.use('/api', adminRoutes);
|
||||||
app.use('/api', workdayRoutes);
|
app.use('/api', workdayRoutes);
|
||||||
|
|||||||
23
server/routes/teams.js
Normal file
23
server/routes/teams.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { requireCapability } = require('../middleware/auth');
|
||||||
|
const { publicConfig, saveConfig, sendTest } = require('../services/teams');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/teams/settings', requireCapability('admin.integrations'), (req, res) => {
|
||||||
|
res.json({ settings: publicConfig() });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/teams/settings', requireCapability('admin.integrations'), (req, res) => {
|
||||||
|
res.json({ settings: saveConfig(req.body, req.user.id) });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/teams/test', requireCapability('admin.integrations'), async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
res.json(await sendTest(req.user.id));
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -2,6 +2,7 @@ const { all, audit, now, one, run } = require('../db');
|
|||||||
const { getConfig, sendMail } = require('./notifications');
|
const { getConfig, sendMail } = require('./notifications');
|
||||||
const { deliverAlerts } = require('./webhooks');
|
const { deliverAlerts } = require('./webhooks');
|
||||||
const servicenow = require('./servicenow');
|
const servicenow = require('./servicenow');
|
||||||
|
const teams = require('./teams');
|
||||||
const { pmAlertCandidates } = require('./pm');
|
const { pmAlertCandidates } = require('./pm');
|
||||||
|
|
||||||
function todayStr() {
|
function todayStr() {
|
||||||
@@ -171,10 +172,12 @@ async function runAlertCycle(userId) {
|
|||||||
const result = { created: created.length, emailed: false, webhooked: false };
|
const result = { created: created.length, emailed: false, webhooked: false };
|
||||||
|
|
||||||
const snCfg = servicenow.getConfig();
|
const snCfg = servicenow.getConfig();
|
||||||
|
const teamsCfg = teams.getConfig();
|
||||||
const emailReady = cfg.enabled && cfg.host && cfg.recipients.length;
|
const emailReady = cfg.enabled && cfg.host && cfg.recipients.length;
|
||||||
const webhookReady = cfg.webhookEnabled && cfg.webhookUrl;
|
const webhookReady = cfg.webhookEnabled && cfg.webhookUrl;
|
||||||
const ticketReady = snCfg.ticketEnabled && snCfg.autoTicket && snCfg.instanceUrl;
|
const ticketReady = snCfg.ticketEnabled && snCfg.autoTicket && snCfg.instanceUrl;
|
||||||
if (!emailReady && !webhookReady && !ticketReady) return result;
|
const teamsReady = teamsCfg.enabled && teamsCfg.webhookUrl;
|
||||||
|
if (!emailReady && !webhookReady && !ticketReady && !teamsReady) return result;
|
||||||
|
|
||||||
const pending = all(
|
const pending = all(
|
||||||
`SELECT al.*, a.asset_id AS asset_code
|
`SELECT al.*, a.asset_id AS asset_code
|
||||||
@@ -205,6 +208,13 @@ async function runAlertCycle(userId) {
|
|||||||
result.ticketed = tickets.created;
|
result.ticketed = tickets.created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (teamsReady) {
|
||||||
|
const teamsDelivery = await teams.deliverAlerts(pending);
|
||||||
|
result.teamsPosted = teamsDelivery.attempted;
|
||||||
|
result.teamsDelivered = teamsDelivery.delivered;
|
||||||
|
result.teamsFailed = teamsDelivery.failed;
|
||||||
|
}
|
||||||
|
|
||||||
const ts = now();
|
const ts = now();
|
||||||
for (const alert of pending) run('UPDATE alerts SET notified_at = ? WHERE id = ?', [ts, alert.id]);
|
for (const alert of pending) run('UPDATE alerts SET notified_at = ? WHERE id = ?', [ts, alert.id]);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
187
server/services/teams.js
Normal file
187
server/services/teams.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// Microsoft Teams integration: posts alerts to a Teams channel/chat via an incoming webhook URL.
|
||||||
|
// Supports the modern Power Automate "Workflows" webhook (Adaptive Card envelope) and the legacy
|
||||||
|
// Office 365 connector ("MessageCard"). Best-effort delivery — failures are counted, never thrown,
|
||||||
|
// so a bad Teams endpoint can't break the alert cycle.
|
||||||
|
const { all, audit, now, run } = require('../db');
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 10000;
|
||||||
|
const FORMATS = ['adaptive', 'messagecard'];
|
||||||
|
const SEVERITY_RANK = { info: 0, warning: 1, critical: 2 };
|
||||||
|
|
||||||
|
function readSettings() {
|
||||||
|
const rows = all("SELECT key, value FROM app_settings WHERE key LIKE 'teams_%'");
|
||||||
|
return Object.fromEntries(rows.map((r) => [r.key, r.value]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw config used for delivery.
|
||||||
|
function getConfig() {
|
||||||
|
const s = readSettings();
|
||||||
|
return {
|
||||||
|
enabled: s.teams_enabled === 'true',
|
||||||
|
webhookUrl: s.teams_webhook_url || '',
|
||||||
|
format: FORMATS.includes(s.teams_format) ? s.teams_format : 'adaptive',
|
||||||
|
minSeverity: SEVERITY_RANK[s.teams_min_severity] !== undefined ? s.teams_min_severity : 'warning'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safe config for the API/UI.
|
||||||
|
function publicConfig() {
|
||||||
|
const c = getConfig();
|
||||||
|
return {
|
||||||
|
teams_enabled: c.enabled,
|
||||||
|
teams_webhook_url: c.webhookUrl,
|
||||||
|
teams_format: c.format,
|
||||||
|
teams_min_severity: c.minSeverity,
|
||||||
|
teams_configured: Boolean(c.webhookUrl)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValue(key, value) {
|
||||||
|
run(
|
||||||
|
'INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at',
|
||||||
|
[key, String(value), now()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig(body, userId) {
|
||||||
|
if (body.teams_enabled !== undefined) setValue('teams_enabled', body.teams_enabled ? 'true' : 'false');
|
||||||
|
if (body.teams_webhook_url !== undefined) setValue('teams_webhook_url', body.teams_webhook_url || '');
|
||||||
|
if (body.teams_format !== undefined) setValue('teams_format', FORMATS.includes(body.teams_format) ? body.teams_format : 'adaptive');
|
||||||
|
if (body.teams_min_severity !== undefined) setValue('teams_min_severity', SEVERITY_RANK[body.teams_min_severity] !== undefined ? body.teams_min_severity : 'warning');
|
||||||
|
audit(userId, 'update', 'teams_settings', null, null, body);
|
||||||
|
return publicConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Card builders ---------------------------------------------------------
|
||||||
|
|
||||||
|
function adaptiveColor(severity) {
|
||||||
|
if (severity === 'critical') return 'attention';
|
||||||
|
if (severity === 'warning') return 'warning';
|
||||||
|
return 'accent';
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageCardColor(severity) {
|
||||||
|
if (severity === 'critical') return 'b3261e';
|
||||||
|
if (severity === 'warning') return 'a56a00';
|
||||||
|
return '2457a6';
|
||||||
|
}
|
||||||
|
|
||||||
|
function highestSeverity(alerts) {
|
||||||
|
return alerts.reduce((top, a) => (SEVERITY_RANK[a.severity] > SEVERITY_RANK[top] ? a.severity : top), 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
function alertFacts(alert) {
|
||||||
|
const facts = [];
|
||||||
|
if (alert.due_date) facts.push({ title: 'Due', value: String(alert.due_date), name: 'Due' });
|
||||||
|
if (alert.asset_code) facts.push({ title: 'Asset', value: String(alert.asset_code), name: 'Asset' });
|
||||||
|
if (alert.type) facts.push({ title: 'Type', value: String(alert.type), name: 'Type' });
|
||||||
|
return facts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adaptive Card (Power Automate Workflows webhook). One card summarizing all alerts.
|
||||||
|
function buildAdaptiveCard(alerts, heading, intro) {
|
||||||
|
const body = [
|
||||||
|
{ type: 'TextBlock', size: 'Large', weight: 'Bolder', text: heading },
|
||||||
|
{ type: 'TextBlock', text: intro, isSubtle: true, wrap: true, spacing: 'None' }
|
||||||
|
];
|
||||||
|
for (const alert of alerts) {
|
||||||
|
body.push({
|
||||||
|
type: 'TextBlock', weight: 'Bolder', color: adaptiveColor(alert.severity),
|
||||||
|
text: `${String(alert.severity || 'info').toUpperCase()} · ${alert.title}`, wrap: true, separator: true
|
||||||
|
});
|
||||||
|
if (alert.message) body.push({ type: 'TextBlock', text: alert.message, wrap: true, spacing: 'None' });
|
||||||
|
const facts = alertFacts(alert);
|
||||||
|
if (facts.length) body.push({ type: 'FactSet', facts: facts.map((f) => ({ title: f.title, value: f.value })) });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'message',
|
||||||
|
attachments: [{
|
||||||
|
contentType: 'application/vnd.microsoft.card.adaptive',
|
||||||
|
content: {
|
||||||
|
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
|
||||||
|
type: 'AdaptiveCard',
|
||||||
|
version: '1.4',
|
||||||
|
msteams: { width: 'Full' },
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy MessageCard (Office 365 connector).
|
||||||
|
function buildMessageCard(alerts, heading, intro) {
|
||||||
|
return {
|
||||||
|
'@type': 'MessageCard',
|
||||||
|
'@context': 'http://schema.org/extensions',
|
||||||
|
themeColor: messageCardColor(highestSeverity(alerts)),
|
||||||
|
summary: heading,
|
||||||
|
title: `${heading} — ${intro}`,
|
||||||
|
sections: alerts.map((alert) => ({
|
||||||
|
activityTitle: `${String(alert.severity || 'info').toUpperCase()} · ${alert.title}`,
|
||||||
|
text: alert.message || '',
|
||||||
|
facts: alertFacts(alert).map((f) => ({ name: f.name, value: f.value }))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(alerts, config, { heading = 'DepreCore alerts', intro } = {}) {
|
||||||
|
const text = intro || `${alerts.length} item(s) need attention.`;
|
||||||
|
return config.format === 'messagecard'
|
||||||
|
? buildMessageCard(alerts, heading, text)
|
||||||
|
: buildAdaptiveCard(alerts, heading, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Delivery --------------------------------------------------------------
|
||||||
|
|
||||||
|
async function postPayload(url, payload) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
return { ok: response.ok, status: response.status };
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post the pending alerts as a single digest card. Honors the configured minimum severity so the
|
||||||
|
// channel isn't flooded with low-priority items.
|
||||||
|
async function deliverAlerts(alerts) {
|
||||||
|
const c = getConfig();
|
||||||
|
if (!c.enabled || !c.webhookUrl || !alerts.length) {
|
||||||
|
return { attempted: false, delivered: 0, failed: 0 };
|
||||||
|
}
|
||||||
|
const threshold = SEVERITY_RANK[c.minSeverity] ?? 1;
|
||||||
|
const relevant = alerts.filter((a) => (SEVERITY_RANK[a.severity] ?? 0) >= threshold);
|
||||||
|
if (!relevant.length) return { attempted: false, delivered: 0, failed: 0 };
|
||||||
|
try {
|
||||||
|
const result = await postPayload(c.webhookUrl, buildPayload(relevant, c));
|
||||||
|
return { attempted: true, delivered: result.ok ? 1 : 0, failed: result.ok ? 0 : 1, status: result.status, count: relevant.length };
|
||||||
|
} catch {
|
||||||
|
return { attempted: true, delivered: 0, failed: 1 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post a test card to verify the webhook URL.
|
||||||
|
async function sendTest(userId) {
|
||||||
|
const c = getConfig();
|
||||||
|
if (!c.webhookUrl) throw Object.assign(new Error('A Teams webhook URL is not configured'), { status: 400 });
|
||||||
|
const sample = [{ severity: 'info', title: 'DepreCore Teams test', message: 'If you can see this card in your channel, the Teams integration is configured correctly.', type: 'test' }];
|
||||||
|
const payload = buildPayload(sample, c, { heading: 'DepreCore Teams test', intro: 'Connection test' });
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await postPayload(c.webhookUrl, payload);
|
||||||
|
} catch (error) {
|
||||||
|
throw Object.assign(new Error(`Teams request failed: ${error.message}`), { status: 400 });
|
||||||
|
}
|
||||||
|
audit(userId, 'send', 'teams_test', null, null, { status: result.status });
|
||||||
|
if (!result.ok) throw Object.assign(new Error(`Teams endpoint returned HTTP ${result.status}`), { status: 400 });
|
||||||
|
return { ok: true, status: result.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { buildPayload, deliverAlerts, getConfig, publicConfig, saveConfig, sendTest };
|
||||||
@@ -1387,6 +1387,61 @@ async function main() {
|
|||||||
await new Promise((resolve) => hookServer.close(resolve));
|
await new Promise((resolve) => hookServer.close(resolve));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Microsoft Teams integration: posts alert digests to a Teams webhook (Adaptive Card / MessageCard).
|
||||||
|
const teamsHits = [];
|
||||||
|
const teamsServer = http.createServer((req, res) => {
|
||||||
|
let raw = '';
|
||||||
|
req.on('data', (chunk) => { raw += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
teamsHits.push({ raw, body: JSON.parse(raw || '{}') });
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => teamsServer.listen(4300, '127.0.0.1', resolve));
|
||||||
|
try {
|
||||||
|
const teamsUrl = 'http://127.0.0.1:4300/teams';
|
||||||
|
const savedTeams = await request('/api/teams/settings', {
|
||||||
|
method: 'PUT', headers: auth,
|
||||||
|
body: JSON.stringify({ teams_enabled: true, teams_webhook_url: teamsUrl, teams_format: 'adaptive', teams_min_severity: 'info' })
|
||||||
|
});
|
||||||
|
if (savedTeams.settings.teams_webhook_url !== teamsUrl || savedTeams.settings.teams_format !== 'adaptive' || !savedTeams.settings.teams_enabled) {
|
||||||
|
throw new Error('Teams settings did not persist');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test card uses the Adaptive Card (Workflows) envelope.
|
||||||
|
const teamsTest = await request('/api/teams/test', { method: 'POST', headers: auth, body: JSON.stringify({}) });
|
||||||
|
if (!teamsTest.ok) throw new Error('Teams test card was not delivered');
|
||||||
|
const adaptiveHit = teamsHits.find((h) => h.body.type === 'message');
|
||||||
|
if (!adaptiveHit || !Array.isArray(adaptiveHit.body.attachments) ||
|
||||||
|
adaptiveHit.body.attachments[0].contentType !== 'application/vnd.microsoft.card.adaptive' ||
|
||||||
|
adaptiveHit.body.attachments[0].content.type !== 'AdaptiveCard') {
|
||||||
|
throw new Error('Teams adaptive card envelope was malformed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the (now-closed) webhook so the next cycle is handled by Teams, then raise a fresh alert.
|
||||||
|
await request('/api/notifications/settings', { method: 'PUT', headers: auth, body: JSON.stringify({ webhook_enabled: false }) });
|
||||||
|
await request(`/api/assets/${assetId}/tasks`, {
|
||||||
|
method: 'POST', headers: auth,
|
||||||
|
body: JSON.stringify({ title: `Teams overdue ${suffix}`, type: 'inspection', due_date: '2019-02-02' })
|
||||||
|
});
|
||||||
|
const teamsCycle = await request('/api/alerts/run', { method: 'POST', headers: auth });
|
||||||
|
if (!teamsCycle.teamsPosted || !(teamsCycle.teamsDelivered > 0)) throw new Error('Alert cycle did not post alerts to Teams');
|
||||||
|
const digestHit = teamsHits.find((h) => h.body.type === 'message' && JSON.stringify(h.body).includes(`Teams overdue ${suffix}`));
|
||||||
|
if (!digestHit) throw new Error('Teams did not receive the alert digest card');
|
||||||
|
|
||||||
|
// Legacy MessageCard format produces a different envelope.
|
||||||
|
await request('/api/teams/settings', { method: 'PUT', headers: auth, body: JSON.stringify({ teams_format: 'messagecard' }) });
|
||||||
|
const mcTest = await request('/api/teams/test', { method: 'POST', headers: auth, body: JSON.stringify({}) });
|
||||||
|
if (!mcTest.ok) throw new Error('Teams MessageCard test was not delivered');
|
||||||
|
if (!teamsHits.some((h) => h.body['@type'] === 'MessageCard')) throw new Error('Teams MessageCard payload was not produced');
|
||||||
|
|
||||||
|
// Disable Teams so it doesn't interfere with later alert assertions.
|
||||||
|
await request('/api/teams/settings', { method: 'PUT', headers: auth, body: JSON.stringify({ teams_enabled: false }) });
|
||||||
|
} finally {
|
||||||
|
await new Promise((resolve) => teamsServer.close(resolve));
|
||||||
|
}
|
||||||
|
|
||||||
// ServiceNow integration: incident push (Table API) + CMDB asset pull, against a mock instance.
|
// ServiceNow integration: incident push (Table API) + CMDB asset pull, against a mock instance.
|
||||||
const snHits = [];
|
const snHits = [];
|
||||||
const snServer = http.createServer((req, res) => {
|
const snServer = http.createServer((req, res) => {
|
||||||
|
|||||||
133
src/components/TeamsSettings.vue
Normal file
133
src/components/TeamsSettings.vue
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="span-12 panel-pad">
|
||||||
|
<h2 class="section-title mb-1">Microsoft Teams</h2>
|
||||||
|
<p class="quiet text-body-2 mb-3">
|
||||||
|
Post newly raised alerts to a Teams channel or chat via an incoming webhook. One digest card is sent per alert
|
||||||
|
cycle, alongside (or instead of) email. Create the webhook in Teams with a
|
||||||
|
<strong>Workflows</strong> "post to a channel when a webhook request is received" flow (recommended), or a legacy
|
||||||
|
<strong>Incoming Webhook</strong> connector, then paste the URL below.
|
||||||
|
</p>
|
||||||
|
<div class="form-grid">
|
||||||
|
<v-switch v-model="form.teams_enabled" label="Enable Teams delivery" color="primary" density="compact" hide-details />
|
||||||
|
<div />
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.teams_webhook_url"
|
||||||
|
class="full"
|
||||||
|
label="Teams webhook URL"
|
||||||
|
placeholder="https://prod-00.westus.logic.azure.com:443/workflows/…/triggers/manual/paths/invoke?…"
|
||||||
|
type="url"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="form.teams_format"
|
||||||
|
:items="formatItems"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
label="Card format"
|
||||||
|
hint="Adaptive Card for Workflows webhooks; MessageCard for legacy connectors"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="form.teams_min_severity"
|
||||||
|
:items="severityItems"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
label="Minimum severity to post"
|
||||||
|
hint="Lower-severity alerts are skipped to keep the channel focused"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-row mt-3">
|
||||||
|
<v-btn variant="tonal" prepend-icon="mdi-microsoft-teams" :loading="testing" :disabled="!form.teams_webhook_url" @click="sendTest">Send test card</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn color="primary" prepend-icon="mdi-content-save" :loading="saving" @click="save">Save Teams settings</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-alert v-if="status" class="mt-3" density="compact" :type="statusType">{{ status }}</v-alert>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { apiRequest } from '../services/api';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
token: { type: String, required: true }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
teams_enabled: false,
|
||||||
|
teams_webhook_url: '',
|
||||||
|
teams_format: 'adaptive',
|
||||||
|
teams_min_severity: 'warning'
|
||||||
|
},
|
||||||
|
formatItems: [
|
||||||
|
{ title: 'Adaptive Card (Workflows)', value: 'adaptive' },
|
||||||
|
{ title: 'MessageCard (legacy connector)', value: 'messagecard' }
|
||||||
|
],
|
||||||
|
severityItems: [
|
||||||
|
{ title: 'Info and above (all alerts)', value: 'info' },
|
||||||
|
{ title: 'Warning and above', value: 'warning' },
|
||||||
|
{ title: 'Critical only', value: 'critical' }
|
||||||
|
],
|
||||||
|
saving: false,
|
||||||
|
testing: false,
|
||||||
|
status: '',
|
||||||
|
statusType: 'success'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.load();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async api(path, options = {}) {
|
||||||
|
return apiRequest(path, this.token, options);
|
||||||
|
},
|
||||||
|
async load() {
|
||||||
|
const data = await (await this.api('/api/teams/settings')).json();
|
||||||
|
this.form = {
|
||||||
|
teams_enabled: data.settings.teams_enabled,
|
||||||
|
teams_webhook_url: data.settings.teams_webhook_url,
|
||||||
|
teams_format: data.settings.teams_format,
|
||||||
|
teams_min_severity: data.settings.teams_min_severity
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
this.saving = true;
|
||||||
|
this.status = '';
|
||||||
|
try {
|
||||||
|
const data = await (await this.api('/api/teams/settings', { method: 'PUT', body: JSON.stringify(this.form) })).json();
|
||||||
|
this.form = {
|
||||||
|
teams_enabled: data.settings.teams_enabled,
|
||||||
|
teams_webhook_url: data.settings.teams_webhook_url,
|
||||||
|
teams_format: data.settings.teams_format,
|
||||||
|
teams_min_severity: data.settings.teams_min_severity
|
||||||
|
};
|
||||||
|
this.statusType = 'success';
|
||||||
|
this.status = 'Teams settings saved.';
|
||||||
|
} catch (error) {
|
||||||
|
this.statusType = 'error';
|
||||||
|
this.status = error.message;
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async sendTest() {
|
||||||
|
this.testing = true;
|
||||||
|
this.status = '';
|
||||||
|
try {
|
||||||
|
// Persist the current URL/format first so the test hits what the user typed.
|
||||||
|
await this.save();
|
||||||
|
const result = await (await this.api('/api/teams/test', { method: 'POST', body: JSON.stringify({}) })).json();
|
||||||
|
this.statusType = 'success';
|
||||||
|
this.status = `Test card posted to Teams (HTTP ${result.status}).`;
|
||||||
|
} catch (error) {
|
||||||
|
this.statusType = 'error';
|
||||||
|
this.status = `Test failed: ${error.message}`;
|
||||||
|
} finally {
|
||||||
|
this.testing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -215,6 +215,8 @@
|
|||||||
|
|
||||||
<WebhookSettings :token="token" />
|
<WebhookSettings :token="token" />
|
||||||
|
|
||||||
|
<TeamsSettings :token="token" />
|
||||||
|
|
||||||
<ServiceNowSettings :token="token" />
|
<ServiceNowSettings :token="token" />
|
||||||
|
|
||||||
<v-card class="span-12 panel-pad">
|
<v-card class="span-12 panel-pad">
|
||||||
@@ -304,10 +306,11 @@ import PartCategorySettings from '../components/PartCategorySettings.vue';
|
|||||||
import PmCategorySettings from '../components/PmCategorySettings.vue';
|
import PmCategorySettings from '../components/PmCategorySettings.vue';
|
||||||
import PmSettings from '../components/PmSettings.vue';
|
import PmSettings from '../components/PmSettings.vue';
|
||||||
import ServiceNowSettings from '../components/ServiceNowSettings.vue';
|
import ServiceNowSettings from '../components/ServiceNowSettings.vue';
|
||||||
|
import TeamsSettings from '../components/TeamsSettings.vue';
|
||||||
import WebhookSettings from '../components/WebhookSettings.vue';
|
import WebhookSettings from '../components/WebhookSettings.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { AssetClassSettings, AssetIdTemplateSettings, AuditTrailSettings, CategorySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PasswordPolicySettings, PmCategorySettings, PmSettings, Section179LimitSettings, ServiceNowSettings, WebhookSettings },
|
components: { AssetClassSettings, AssetIdTemplateSettings, AuditTrailSettings, CategorySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PasswordPolicySettings, PmCategorySettings, PmSettings, Section179LimitSettings, ServiceNowSettings, TeamsSettings, WebhookSettings },
|
||||||
props: {
|
props: {
|
||||||
token: { type: String, default: '' },
|
token: { type: String, default: '' },
|
||||||
section: { type: String, default: 'admin-users' },
|
section: { type: String, default: 'admin-users' },
|
||||||
|
|||||||
Reference in New Issue
Block a user