## New Modules - StorageModule: SQLite persistence for sessions, app usage, conversations - MonitoringModule: Cross-platform window tracking (Win32/X11) - AIModule: Multi-provider LLM integration with agentic tool loop - VoiceModule: TTS/STT coordination with speak queue ## Shared Libraries - AissiaLLM: ILLMProvider abstraction (Claude + OpenAI providers) - AissiaPlatform: IWindowTracker abstraction (Win32 + X11) - AissiaAudio: ITTSEngine (SAPI/espeak) + ISTTEngine (Whisper API) - HttpClient: Header-only HTTP client with OpenSSL ## Configuration - Added JSON configs for all modules (storage, monitoring, ai, voice) - Multi-provider LLM config with Claude and OpenAI support ## Dependencies - SQLite3, OpenSSL, cpp-httplib (FetchContent) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
223 lines
8.1 KiB
C++
223 lines
8.1 KiB
C++
#include "MonitoringModule.h"
|
|
#include <grove/JsonDataNode.h>
|
|
#include <algorithm>
|
|
|
|
namespace aissia {
|
|
|
|
MonitoringModule::MonitoringModule() {
|
|
m_logger = spdlog::get("MonitoringModule");
|
|
if (!m_logger) {
|
|
m_logger = spdlog::stdout_color_mt("MonitoringModule");
|
|
}
|
|
m_config = std::make_unique<grove::JsonDataNode>("config");
|
|
}
|
|
|
|
void MonitoringModule::setConfiguration(const grove::IDataNode& configNode,
|
|
grove::IIO* io,
|
|
grove::ITaskScheduler* scheduler) {
|
|
m_io = io;
|
|
m_config = std::make_unique<grove::JsonDataNode>("config");
|
|
|
|
m_pollIntervalMs = configNode.getInt("poll_interval_ms", 1000);
|
|
m_idleThresholdSeconds = configNode.getInt("idle_threshold_seconds", 300);
|
|
m_enabled = configNode.getBool("enabled", true);
|
|
|
|
// Load productive apps list
|
|
m_productiveApps.clear();
|
|
auto* prodNode = configNode.getChildReadOnly("productive_apps");
|
|
if (prodNode) {
|
|
for (const auto& name : prodNode->getChildNames()) {
|
|
m_productiveApps.insert(prodNode->getString(name, ""));
|
|
}
|
|
}
|
|
// Default productive apps
|
|
if (m_productiveApps.empty()) {
|
|
m_productiveApps = {"Code", "code", "CLion", "clion", "Visual Studio",
|
|
"devenv", "rider", "idea", "pycharm"};
|
|
}
|
|
|
|
// Load distracting apps list
|
|
m_distractingApps.clear();
|
|
auto* distNode = configNode.getChildReadOnly("distracting_apps");
|
|
if (distNode) {
|
|
for (const auto& name : distNode->getChildNames()) {
|
|
m_distractingApps.insert(distNode->getString(name, ""));
|
|
}
|
|
}
|
|
if (m_distractingApps.empty()) {
|
|
m_distractingApps = {"Discord", "discord", "Steam", "steam",
|
|
"firefox", "chrome", "YouTube"};
|
|
}
|
|
|
|
// Create window tracker
|
|
m_tracker = WindowTrackerFactory::create();
|
|
|
|
m_logger->info("MonitoringModule configure: poll={}ms, idle={}s, platform={}",
|
|
m_pollIntervalMs, m_idleThresholdSeconds,
|
|
m_tracker ? m_tracker->getPlatformName() : "none");
|
|
}
|
|
|
|
const grove::IDataNode& MonitoringModule::getConfiguration() {
|
|
return *m_config;
|
|
}
|
|
|
|
void MonitoringModule::process(const grove::IDataNode& input) {
|
|
if (!m_enabled || !m_tracker || !m_tracker->isAvailable()) return;
|
|
|
|
float currentTime = input.getDouble("gameTime", 0.0);
|
|
|
|
// Poll based on interval
|
|
float pollIntervalSec = m_pollIntervalMs / 1000.0f;
|
|
if (currentTime - m_lastPollTime < pollIntervalSec) return;
|
|
m_lastPollTime = currentTime;
|
|
|
|
checkCurrentApp(currentTime);
|
|
checkIdleState(currentTime);
|
|
}
|
|
|
|
void MonitoringModule::checkCurrentApp(float currentTime) {
|
|
std::string newApp = m_tracker->getCurrentAppName();
|
|
std::string newTitle = m_tracker->getCurrentWindowTitle();
|
|
|
|
if (newApp != m_currentApp) {
|
|
// App changed
|
|
int duration = static_cast<int>(currentTime - m_appStartTime);
|
|
|
|
if (!m_currentApp.empty() && duration > 0) {
|
|
m_appDurations[m_currentApp] += duration;
|
|
|
|
// Update productivity counters
|
|
if (isProductiveApp(m_currentApp)) {
|
|
m_totalProductiveSeconds += duration;
|
|
} else if (isDistractingApp(m_currentApp)) {
|
|
m_totalDistractingSeconds += duration;
|
|
}
|
|
|
|
publishAppChanged(m_currentApp, newApp, duration);
|
|
}
|
|
|
|
m_currentApp = newApp;
|
|
m_currentWindowTitle = newTitle;
|
|
m_appStartTime = currentTime;
|
|
|
|
m_logger->debug("App: {} - {}", m_currentApp, m_currentWindowTitle.substr(0, 50));
|
|
}
|
|
}
|
|
|
|
void MonitoringModule::checkIdleState(float currentTime) {
|
|
bool wasIdle = m_isIdle;
|
|
m_isIdle = m_tracker->isUserIdle(m_idleThresholdSeconds);
|
|
|
|
if (m_isIdle && !wasIdle) {
|
|
m_logger->info("Utilisateur inactif ({}s)", m_idleThresholdSeconds);
|
|
|
|
if (m_io) {
|
|
auto event = std::make_unique<grove::JsonDataNode>("idle");
|
|
event->setString("type", "idle_detected");
|
|
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
|
|
m_io->publish("monitoring:idle_detected", std::move(event));
|
|
}
|
|
}
|
|
else if (!m_isIdle && wasIdle) {
|
|
m_logger->info("Activite reprise");
|
|
|
|
if (m_io) {
|
|
auto event = std::make_unique<grove::JsonDataNode>("active");
|
|
event->setString("type", "activity_resumed");
|
|
m_io->publish("monitoring:activity_resumed", std::move(event));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool MonitoringModule::isProductiveApp(const std::string& appName) const {
|
|
// Check exact match
|
|
if (m_productiveApps.count(appName)) return true;
|
|
|
|
// Check if app name contains productive keyword
|
|
std::string lowerApp = appName;
|
|
std::transform(lowerApp.begin(), lowerApp.end(), lowerApp.begin(), ::tolower);
|
|
|
|
for (const auto& prod : m_productiveApps) {
|
|
std::string lowerProd = prod;
|
|
std::transform(lowerProd.begin(), lowerProd.end(), lowerProd.begin(), ::tolower);
|
|
if (lowerApp.find(lowerProd) != std::string::npos) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool MonitoringModule::isDistractingApp(const std::string& appName) const {
|
|
if (m_distractingApps.count(appName)) return true;
|
|
|
|
std::string lowerApp = appName;
|
|
std::transform(lowerApp.begin(), lowerApp.end(), lowerApp.begin(), ::tolower);
|
|
|
|
for (const auto& dist : m_distractingApps) {
|
|
std::string lowerDist = dist;
|
|
std::transform(lowerDist.begin(), lowerDist.end(), lowerDist.begin(), ::tolower);
|
|
if (lowerApp.find(lowerDist) != std::string::npos) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void MonitoringModule::publishAppChanged(const std::string& oldApp, const std::string& newApp, int duration) {
|
|
if (!m_io) return;
|
|
|
|
auto event = std::make_unique<grove::JsonDataNode>("app_changed");
|
|
event->setString("oldApp", oldApp);
|
|
event->setString("newApp", newApp);
|
|
event->setInt("duration", duration);
|
|
event->setBool("wasProductive", isProductiveApp(oldApp));
|
|
event->setBool("wasDistracting", isDistractingApp(oldApp));
|
|
m_io->publish("monitoring:app_changed", std::move(event));
|
|
}
|
|
|
|
std::unique_ptr<grove::IDataNode> MonitoringModule::getHealthStatus() {
|
|
auto status = std::make_unique<grove::JsonDataNode>("status");
|
|
status->setString("status", m_enabled ? "running" : "disabled");
|
|
status->setString("currentApp", m_currentApp);
|
|
status->setBool("isIdle", m_isIdle);
|
|
status->setInt("totalProductiveSeconds", m_totalProductiveSeconds);
|
|
status->setInt("totalDistractingSeconds", m_totalDistractingSeconds);
|
|
status->setString("platform", m_tracker ? m_tracker->getPlatformName() : "none");
|
|
return status;
|
|
}
|
|
|
|
void MonitoringModule::shutdown() {
|
|
m_logger->info("MonitoringModule arrete. Productif: {}s, Distrait: {}s",
|
|
m_totalProductiveSeconds, m_totalDistractingSeconds);
|
|
}
|
|
|
|
std::unique_ptr<grove::IDataNode> MonitoringModule::getState() {
|
|
auto state = std::make_unique<grove::JsonDataNode>("state");
|
|
state->setString("currentApp", m_currentApp);
|
|
state->setDouble("appStartTime", m_appStartTime);
|
|
state->setBool("isIdle", m_isIdle);
|
|
state->setInt("totalProductiveSeconds", m_totalProductiveSeconds);
|
|
state->setInt("totalDistractingSeconds", m_totalDistractingSeconds);
|
|
return state;
|
|
}
|
|
|
|
void MonitoringModule::setState(const grove::IDataNode& state) {
|
|
m_currentApp = state.getString("currentApp", "");
|
|
m_appStartTime = state.getDouble("appStartTime", 0.0);
|
|
m_isIdle = state.getBool("isIdle", false);
|
|
m_totalProductiveSeconds = state.getInt("totalProductiveSeconds", 0);
|
|
m_totalDistractingSeconds = state.getInt("totalDistractingSeconds", 0);
|
|
|
|
m_logger->info("Etat restore: app={}, productif={}s", m_currentApp, m_totalProductiveSeconds);
|
|
}
|
|
|
|
} // namespace aissia
|
|
|
|
extern "C" {
|
|
|
|
grove::IModule* createModule() {
|
|
return new aissia::MonitoringModule();
|
|
}
|
|
|
|
void destroyModule(grove::IModule* module) {
|
|
delete module;
|
|
}
|
|
|
|
}
|