Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
141 lines
5 KiB
JavaScript
141 lines
5 KiB
JavaScript
/**
|
|
* Briefing — Constants & Helpers
|
|
*
|
|
* Shared constants, option cache, formatting utilities
|
|
*/
|
|
|
|
import { dbAll, safeJsonParse } from '../../database/connection.js';
|
|
import { apiLogger } from '../../utils/logger.js';
|
|
|
|
const log = apiLogger.child({ module: 'briefing-service' });
|
|
export { log };
|
|
|
|
// =============================================================================
|
|
// CONSTANTS
|
|
// =============================================================================
|
|
|
|
export const DEFAULT_CHAT_ID = '423753027';
|
|
export const VIKA_CHAT_ID = '400602216';
|
|
|
|
// All household members who receive briefings
|
|
export const ALL_CHAT_IDS = [DEFAULT_CHAT_ID, VIKA_CHAT_ID];
|
|
|
|
// All notifications are routed to forum topics — no DMs.
|
|
// Topic mapping is handled by TelegramService.sendToTopic().
|
|
// Table IDs in GOD CRM (GERAVIKA space)
|
|
export const TABLE_IDS = {
|
|
CALENDAR_EVENTS: 2671,
|
|
SPRINT_TASKS: 2649,
|
|
HEALTH_METRICS: 2643,
|
|
MEDICATIONS: 2658,
|
|
};
|
|
|
|
export const TIMEZONE = 'Europe/Moscow';
|
|
|
|
export const PRIORITY_ICONS = {
|
|
p1: '\u{1F534}', p0: '\u{1F534}', high: '\u{1F534}', urgent: '\u{1F534}',
|
|
p2: '\u{1F7E1}', medium: '\u{1F7E1}', normal: '\u{1F7E1}',
|
|
p3: '\u{1F7E2}', low: '\u{1F7E2}',
|
|
p4: '\u26AA',
|
|
};
|
|
|
|
export const DONE_STATES = ['Done', 'Archive', 'done', 'archive', 'completed', 'Completed'];
|
|
|
|
// Wellness schedule table IDs
|
|
export const WELLNESS_SCHEDULE_TABLE = 2934;
|
|
export const ATOMIC_HABITS_TABLE = 2933;
|
|
|
|
// =============================================================================
|
|
// OPTION CACHE
|
|
// =============================================================================
|
|
|
|
// Lookup tables for select columns in Family Task Board (table 2649)
|
|
// Related table IDs: state->2646, priority->2648, type->2647
|
|
let _optionCache = null;
|
|
|
|
export async function getOptionLabels() {
|
|
if (_optionCache) return _optionCache;
|
|
try {
|
|
const stateRows = await dbAll('SELECT id, data FROM table_rows WHERE table_id = 2646');
|
|
const priorityRows = await dbAll('SELECT id, data FROM table_rows WHERE table_id = 2648');
|
|
const typeRows = await dbAll('SELECT id, data FROM table_rows WHERE table_id = 2647');
|
|
|
|
const stateMap = {};
|
|
const priorityMap = {};
|
|
const typeMap = {};
|
|
const doneStateIds = new Set();
|
|
|
|
for (const r of stateRows) {
|
|
const d = safeJsonParse(r.data) || {};
|
|
stateMap[r.id] = d.name || String(r.id);
|
|
if (d.is_final) doneStateIds.add(r.id);
|
|
}
|
|
for (const r of priorityRows) {
|
|
const d = safeJsonParse(r.data) || {};
|
|
priorityMap[r.id] = d.name || String(r.id);
|
|
}
|
|
for (const r of typeRows) {
|
|
const d = safeJsonParse(r.data) || {};
|
|
typeMap[r.id] = d.name || String(r.id);
|
|
}
|
|
|
|
_optionCache = { stateMap, priorityMap, typeMap, doneStateIds };
|
|
return _optionCache;
|
|
} catch (err) {
|
|
log.error({ err }, 'Failed to load option labels');
|
|
return { stateMap: {}, priorityMap: {}, typeMap: {}, doneStateIds: new Set() };
|
|
}
|
|
}
|
|
|
|
export function resolveTaskFields(data, opts) {
|
|
const stateRaw = data.state || data.status || '';
|
|
const priorityRaw = data.priority || '';
|
|
const stateLabel = opts.stateMap[stateRaw] || String(stateRaw);
|
|
const priorityLabel = opts.priorityMap[priorityRaw] || String(priorityRaw);
|
|
const isDone = opts.doneStateIds.has(Number(stateRaw)) || DONE_STATES.includes(stateLabel);
|
|
const title = data.what || data.title || data.name || '\u0411\u0435\u0437 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f';
|
|
return { title, stateLabel, priorityLabel, isDone };
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPERS
|
|
// =============================================================================
|
|
|
|
export function getTodayBounds() {
|
|
const now = new Date();
|
|
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: TIMEZONE, year: 'numeric', month: '2-digit', day: '2-digit',
|
|
});
|
|
const todayStr = formatter.format(now);
|
|
|
|
const labelFormatter = new Intl.DateTimeFormat('ru-RU', {
|
|
timeZone: TIMEZONE, weekday: 'long', day: '2-digit', month: 'long', year: 'numeric',
|
|
});
|
|
const todayLabel = labelFormatter.format(now);
|
|
|
|
return { todayStr, todayStart: `${todayStr}T00:00:00`, todayEnd: `${todayStr}T23:59:59`, todayLabel };
|
|
}
|
|
|
|
export function extractTime(datetime) {
|
|
if (!datetime) return '\u0432\u0435\u0441\u044c \u0434\u0435\u043d\u044c';
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(datetime)) return '\u0432\u0435\u0441\u044c \u0434\u0435\u043d\u044c';
|
|
try {
|
|
const d = new Date(datetime);
|
|
if (isNaN(d.getTime())) return '\u0432\u0435\u0441\u044c \u0434\u0435\u043d\u044c';
|
|
return new Intl.DateTimeFormat('en-GB', {
|
|
timeZone: TIMEZONE, hour: '2-digit', minute: '2-digit', hour12: false,
|
|
}).format(d);
|
|
} catch {
|
|
return '\u0432\u0435\u0441\u044c \u0434\u0435\u043d\u044c';
|
|
}
|
|
}
|
|
|
|
export function getPriorityIcon(priority) {
|
|
if (!priority) return '\u26AA';
|
|
return PRIORITY_ICONS[String(priority).toLowerCase().trim()] || '\u26AA';
|
|
}
|
|
|
|
export function escTg(text) {
|
|
if (!text) return '';
|
|
return String(text).replace(/([_*`\[])/g, '\\$1');
|
|
}
|