Vibe-Monitor / index.html
Doobdeedoo's picture
Add 3 files
636a5a8 verified
Raw
History Blame Contribute Delete
52.4 kB
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIBE Monitor | Privacy-Focused Activity Tracking</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="manifest" href="/manifest.json">
<style>
/* Custom styles that can't be done with Tailwind */
.chart-container {
height: 200px;
position: relative;
}
.progress-ring__circle {
transition: stroke-dashoffset 0.35s;
transform: rotate(-90deg);
transform-origin: 50% 50%;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.slide-up-enter-active, .slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter, .slide-up-leave-to {
transform: translateY(20px);
opacity: 0;
}
#batteryGauge {
width: 60px;
height: 30px;
border: 2px solid currentColor;
border-radius: 4px;
position: relative;
}
#batteryGauge::after {
content: '';
position: absolute;
right: -6px;
top: 8px;
width: 3px;
height: 10px;
background: currentColor;
border-radius: 0 2px 2px 0;
}
#batteryLevel {
height: 100%;
background: currentColor;
transition: width 0.5s;
}
</style>
</head>
<body class="h-full bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200">
<div id="app" class="min-h-full flex flex-col">
<!-- Consent Modal -->
<div id="consentModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
<div class="flex items-center mb-4">
<i class="fas fa-shield-alt text-blue-500 mr-3 text-xl"></i>
<h2 class="text-xl font-bold">Privacy-First Monitoring</h2>
</div>
<p class="mb-4 text-gray-700 dark:text-gray-300">
VIBE Monitor helps you track your device usage while respecting your privacy. All data stays on your device unless you choose to export it.
</p>
<div class="bg-blue-50 dark:bg-blue-900 dark:bg-opacity-30 p-4 rounded mb-4">
<h3 class="font-semibold text-blue-800 dark:text-blue-200 mb-2">We'll track:</h3>
<ul class="list-disc pl-5 text-sm text-blue-700 dark:text-blue-300">
<li>Screen time & app visibility</li>
<li>Device information (browser, battery)</li>
<li>Motion activity (if enabled)</li>
<li>Location (only with permission)</li>
</ul>
</div>
<div class="flex flex-col space-y-3">
<button id="acceptAll" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg font-medium transition">
Accept All Tracking
</button>
<button id="acceptEssential" class="border border-blue-600 text-blue-600 dark:text-blue-400 dark:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900 dark:hover:bg-opacity-30 py-2 px-4 rounded-lg font-medium transition">
Essential Tracking Only
</button>
<button id="rejectAll" class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 py-2 px-4 rounded-lg font-medium transition">
Reject All
</button>
</div>
</div>
</div>
<!-- Main App -->
<header class="bg-white dark:bg-gray-800 shadow-sm">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<div class="flex items-center">
<i class="fas fa-chart-line text-blue-500 mr-2"></i>
<h1 class="font-bold text-lg">VIBE Monitor</h1>
</div>
<div class="flex items-center space-x-3">
<button id="themeToggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:block"></i>
</button>
<button id="exportBtn" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700" title="Export Data">
<i class="fas fa-download"></i>
</button>
</div>
</div>
</header>
<main class="flex-grow container mx-auto px-4 py-6">
<!-- Stats Overview -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Screen Time</p>
<p id="screenTime" class="text-2xl font-bold">0h 0m</p>
</div>
<div class="text-blue-500">
<i class="fas fa-desktop text-xl"></i>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Active Sessions</p>
<p id="sessionCount" class="text-2xl font-bold">0</p>
</div>
<div class="text-green-500">
<i class="fas fa-user-clock text-xl"></i>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Battery</p>
<p id="batteryPercent" class="text-2xl font-bold">--%</p>
</div>
<div id="batteryGauge" class="ml-2">
<div id="batteryLevel" style="width: 0%"></div>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">Motion</p>
<p id="motionStatus" class="text-2xl font-bold">--</p>
</div>
<div class="text-purple-500">
<i class="fas fa-running text-xl"></i>
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Screen Time Chart -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h2 class="font-semibold mb-3 flex items-center">
<i class="fas fa-chart-pie text-blue-500 mr-2"></i>
Screen Time Distribution
</h2>
<div class="chart-container">
<canvas id="screenTimeChart"></canvas>
</div>
</div>
<!-- Activity Timeline -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h2 class="font-semibold mb-3 flex items-center">
<i class="fas fa-history text-green-500 mr-2"></i>
Activity Timeline
</h2>
<div class="chart-container">
<canvas id="activityTimeline"></canvas>
</div>
</div>
</div>
<!-- Detailed Stats -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden mb-6">
<div class="border-b border-gray-200 dark:border-gray-700 px-4 py-3 flex justify-between items-center">
<h2 class="font-semibold flex items-center">
<i class="fas fa-info-circle text-purple-500 mr-2"></i>
Device & Session Details
</h2>
<button id="refreshDetails" class="text-sm text-blue-500 hover:text-blue-700 dark:hover:text-blue-400">
<i class="fas fa-sync-alt mr-1"></i> Refresh
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-gray-200 dark:divide-gray-700">
<div class="p-4">
<h3 class="font-medium mb-2 text-gray-700 dark:text-gray-300">Device Information</h3>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">OS</span>
<span id="deviceOS" class="font-medium">--</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Browser</span>
<span id="deviceBrowser" class="font-medium">--</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Screen</span>
<span id="deviceScreen" class="font-medium">--</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Connection</span>
<span id="deviceConnection" class="font-medium">--</span>
</div>
</div>
</div>
<div class="p-4">
<h3 class="font-medium mb-2 text-gray-700 dark:text-gray-300">Current Session</h3>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Start Time</span>
<span id="sessionStart" class="font-medium">--</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Duration</span>
<span id="sessionDuration" class="font-medium">--</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Visibility</span>
<span id="sessionVisibility" class="font-medium">--</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500 dark:text-gray-400">Location</span>
<span id="sessionLocation" class="font-medium">--</span>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity Log -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div class="border-b border-gray-200 dark:border-gray-700 px-4 py-3">
<h2 class="font-semibold flex items-center">
<i class="fas fa-list-ul text-orange-500 mr-2"></i>
Recent Activity Log
</h2>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
<div id="activityLog" class="p-4 text-sm">
<p class="text-gray-500 dark:text-gray-400 text-center py-4">No activity recorded yet</p>
</div>
</div>
</div>
</main>
<!-- Export Modal -->
<div id="exportModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-bold">Export Your Data</h2>
<button id="closeExportModal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<p class="mb-4 text-gray-700 dark:text-gray-300">
Choose the format you'd like to export your activity data in. All data remains on your device until you choose to export it.
</p>
<div class="flex flex-col space-y-3">
<button id="exportJSON" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg font-medium transition flex items-center justify-center">
<i class="fas fa-file-code mr-2"></i> Export as JSON
</button>
<button id="exportCSV" class="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg font-medium transition flex items-center justify-center">
<i class="fas fa-file-csv mr-2"></i> Export as CSV
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Main Application
document.addEventListener('DOMContentLoaded', () => {
// Initialize the app
const app = new VIBEMonitor();
app.init();
});
class VIBEMonitor {
constructor() {
this.consentGiven = false;
this.trackingEnabled = false;
this.locationEnabled = false;
this.motionEnabled = false;
this.currentSession = null;
this.sessions = [];
this.activityLog = [];
this.deviceInfo = {};
this.batteryInfo = {};
this.motionData = {};
this.locationData = {};
this.charts = {};
this.theme = localStorage.getItem('theme') || 'light';
}
init() {
// Set up theme
this.setTheme(this.theme);
// Initialize UI event listeners
this.initEventListeners();
// Show consent modal if not already given
if (!localStorage.getItem('consentGiven')) {
document.getElementById('consentModal').classList.remove('hidden');
} else {
this.consentGiven = true;
this.trackingEnabled = localStorage.getItem('trackingEnabled') === 'true';
this.locationEnabled = localStorage.getItem('locationEnabled') === 'true';
this.motionEnabled = localStorage.getItem('motionEnabled') === 'true';
this.startMonitoring();
}
// Load any existing data
this.loadData();
this.updateUI();
}
initEventListeners() {
// Consent modal buttons
document.getElementById('acceptAll').addEventListener('click', () => {
this.giveConsent(true, true);
});
document.getElementById('acceptEssential').addEventListener('click', () => {
this.giveConsent(true, false);
});
document.getElementById('rejectAll').addEventListener('click', () => {
this.giveConsent(false, false);
});
// Theme toggle
document.getElementById('themeToggle').addEventListener('click', () => {
this.toggleTheme();
});
// Export buttons
document.getElementById('exportBtn').addEventListener('click', () => {
document.getElementById('exportModal').classList.remove('hidden');
});
document.getElementById('closeExportModal').addEventListener('click', () => {
document.getElementById('exportModal').classList.add('hidden');
});
document.getElementById('exportJSON').addEventListener('click', () => {
this.exportData('json');
});
document.getElementById('exportCSV').addEventListener('click', () => {
this.exportData('csv');
});
// Refresh details
document.getElementById('refreshDetails').addEventListener('click', () => {
this.updateDeviceInfo();
this.updateUI();
});
// Page visibility changes
document.addEventListener('visibilitychange', () => {
if (!this.trackingEnabled) return;
const now = new Date();
const isVisible = document.visibilityState === 'visible';
this.logActivity(isVisible ? 'Page became visible' : 'Page became hidden');
if (this.currentSession) {
if (isVisible) {
// Resume session
this.currentSession.intervals.push({
start: now,
end: null,
active: true
});
} else {
// Pause session
if (this.currentSession.intervals.length > 0) {
const lastInterval = this.currentSession.intervals[this.currentSession.intervals.length - 1];
if (lastInterval.end === null) {
lastInterval.end = now;
}
}
}
}
this.updateUI();
this.saveData();
});
}
giveConsent(tracking, location) {
this.consentGiven = true;
this.trackingEnabled = tracking;
this.locationEnabled = location && tracking;
this.motionEnabled = tracking;
localStorage.setItem('consentGiven', 'true');
localStorage.setItem('trackingEnabled', tracking);
localStorage.setItem('locationEnabled', location && tracking);
localStorage.setItem('motionEnabled', tracking);
document.getElementById('consentModal').classList.add('hidden');
if (tracking) {
this.startMonitoring();
}
this.logActivity('User gave consent for tracking');
this.updateUI();
}
startMonitoring() {
// Start a new session
this.startNewSession();
// Initialize device info
this.updateDeviceInfo();
// Set up battery monitoring
this.initBatteryMonitoring();
// Set up motion monitoring if enabled
if (this.motionEnabled) {
this.initMotionMonitoring();
}
// Set up location monitoring if enabled
if (this.locationEnabled) {
this.initLocationMonitoring();
}
// Set up periodic updates
setInterval(() => {
this.updateUI();
this.saveData();
}, 5000);
this.logActivity('Monitoring started');
}
startNewSession() {
const now = new Date();
this.currentSession = {
start: now,
intervals: [{
start: now,
end: null,
active: true
}],
deviceInfo: {},
locationData: []
};
this.sessions.push(this.currentSession);
this.logActivity('New session started');
}
updateDeviceInfo() {
const userAgent = navigator.userAgent;
let os = 'Unknown';
let browser = 'Unknown';
// Detect OS
if (userAgent.match(/android/i)) {
os = 'Android';
} else if (userAgent.match(/iphone|ipad|ipod/i)) {
os = 'iOS';
} else if (userAgent.match(/windows/i)) {
os = 'Windows';
} else if (userAgent.match(/mac/i)) {
os = 'MacOS';
} else if (userAgent.match(/linux/i)) {
os = 'Linux';
}
// Detect Browser
if (userAgent.match(/edg/i)) {
browser = 'Edge';
} else if (userAgent.match(/opr/i)) {
browser = 'Opera';
} else if (userAgent.match(/chrome/i)) {
browser = 'Chrome';
} else if (userAgent.match(/firefox/i)) {
browser = 'Firefox';
} else if (userAgent.match(/safari/i)) {
browser = 'Safari';
}
this.deviceInfo = {
os,
browser,
screen: `${window.screen.width}x${window.screen.height}`,
connection: navigator.connection ? navigator.connection.effectiveType : 'Unknown'
};
if (this.currentSession) {
this.currentSession.deviceInfo = {...this.deviceInfo};
}
}
initBatteryMonitoring() {
if ('getBattery' in navigator) {
navigator.getBattery().then(battery => {
this.batteryInfo = {
level: battery.level,
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime
};
// Update battery info when it changes
battery.addEventListener('levelchange', () => {
this.batteryInfo.level = battery.level;
this.batteryInfo.charging = battery.charging;
this.updateUI();
this.logActivity(`Battery level changed to ${Math.round(battery.level * 100)}%`);
});
battery.addEventListener('chargingchange', () => {
this.batteryInfo.charging = battery.charging;
this.updateUI();
this.logActivity(`Battery charging state changed to ${battery.charging ? 'charging' : 'not charging'}`);
});
this.updateUI();
});
} else {
this.batteryInfo = {
level: null,
charging: null,
chargingTime: null,
dischargingTime: null
};
}
}
initMotionMonitoring() {
if ('DeviceMotionEvent' in window) {
window.addEventListener('devicemotion', (event) => {
const acceleration = event.acceleration;
const accelerationIncludingGravity = event.accelerationIncludingGravity;
const rotationRate = event.rotationRate;
const interval = event.interval;
this.motionData = {
acceleration,
accelerationIncludingGravity,
rotationRate,
interval,
lastUpdated: new Date()
};
// Simple motion detection
const motionDetected = accelerationIncludingGravity &&
(Math.abs(accelerationIncludingGravity.x) > 1.5 ||
Math.abs(accelerationIncludingGravity.y) > 1.5 ||
Math.abs(accelerationIncludingGravity.z) > 1.5);
if (motionDetected) {
this.logActivity('Significant motion detected');
}
this.updateUI();
});
} else {
this.motionData = {
error: 'Device motion not supported'
};
}
}
initLocationMonitoring() {
if ('geolocation' in navigator) {
const options = {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
};
const success = (position) => {
this.locationData = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
altitudeAccuracy: position.coords.altitudeAccuracy,
heading: position.coords.heading,
speed: position.coords.speed,
timestamp: position.timestamp
};
if (this.currentSession) {
this.currentSession.locationData.push({...this.locationData});
}
this.updateUI();
this.logActivity('Location updated');
};
const error = (err) => {
this.locationData = {
error: err.message
};
this.logActivity(`Location error: ${err.message}`);
};
// Get initial position
navigator.geolocation.getCurrentPosition(success, error, options);
// Watch for position changes
this.locationWatchId = navigator.geolocation.watchPosition(success, error, options);
} else {
this.locationData = {
error: 'Geolocation not supported'
};
}
}
logActivity(message) {
const timestamp = new Date();
this.activityLog.unshift({
timestamp,
message
});
// Keep only the last 50 activities
if (this.activityLog.length > 50) {
this.activityLog.pop();
}
this.updateActivityLogUI();
}
updateUI() {
// Update screen time
this.updateScreenTime();
// Update session count
document.getElementById('sessionCount').textContent = this.sessions.length;
// Update battery info
if (this.batteryInfo.level !== null) {
const batteryPercent = Math.round(this.batteryInfo.level * 100);
document.getElementById('batteryPercent').textContent = `${batteryPercent}%`;
document.getElementById('batteryLevel').style.width = `${batteryPercent}%`;
// Change color based on battery level
const batteryGauge = document.getElementById('batteryGauge');
batteryGauge.className = '';
if (batteryPercent > 60) {
batteryGauge.classList.add('text-green-500');
} else if (batteryPercent > 20) {
batteryGauge.classList.add('text-yellow-500');
} else {
batteryGauge.classList.add('text-red-500');
}
}
// Update motion status
if (this.motionData.accelerationIncludingGravity) {
const motionDetected = Math.abs(this.motionData.accelerationIncludingGravity.x) > 1.5 ||
Math.abs(this.motionData.accelerationIncludingGravity.y) > 1.5 ||
Math.abs(this.motionData.accelerationIncludingGravity.z) > 1.5;
document.getElementById('motionStatus').textContent = motionDetected ? 'Active' : 'Idle';
document.getElementById('motionStatus').className = motionDetected ?
'text-2xl font-bold text-green-500' : 'text-2xl font-bold text-gray-500';
}
// Update device details
document.getElementById('deviceOS').textContent = this.deviceInfo.os;
document.getElementById('deviceBrowser').textContent = this.deviceInfo.browser;
document.getElementById('deviceScreen').textContent = this.deviceInfo.screen;
document.getElementById('deviceConnection').textContent = this.deviceInfo.connection;
// Update session details
if (this.currentSession) {
document.getElementById('sessionStart').textContent = this.currentSession.start.toLocaleTimeString();
// Calculate current session duration
let totalDuration = 0;
for (const interval of this.currentSession.intervals) {
const end = interval.end || new Date();
totalDuration += (end - interval.start);
}
const durationMinutes = Math.floor(totalDuration / 60000);
const durationSeconds = Math.floor((totalDuration % 60000) / 1000);
document.getElementById('sessionDuration').textContent = `${durationMinutes}m ${durationSeconds}s`;
document.getElementById('sessionVisibility').textContent =
document.visibilityState === 'visible' ? 'Visible' : 'Hidden';
if (this.locationData.latitude) {
document.getElementById('sessionLocation').textContent =
`${this.locationData.latitude.toFixed(4)}, ${this.locationData.longitude.toFixed(4)}`;
} else {
document.getElementById('sessionLocation').textContent = 'Not available';
}
}
// Update charts
this.updateCharts();
}
updateScreenTime() {
if (this.sessions.length === 0) {
document.getElementById('screenTime').textContent = '0h 0m';
return;
}
let totalScreenTime = 0;
for (const session of this.sessions) {
for (const interval of session.intervals) {
const end = interval.end || new Date();
totalScreenTime += (end - interval.start);
}
}
const hours = Math.floor(totalScreenTime / 3600000);
const minutes = Math.floor((totalScreenTime % 3600000) / 60000);
document.getElementById('screenTime').textContent = `${hours}h ${minutes}m`;
}
updateActivityLogUI() {
const activityLogElement = document.getElementById('activityLog');
if (this.activityLog.length === 0) {
activityLogElement.innerHTML = '<p class="text-gray-500 dark:text-gray-400 text-center py-4">No activity recorded yet</p>';
return;
}
let html = '';
for (const activity of this.activityLog) {
html += `
<div class="flex items-start py-2">
<div class="flex-shrink-0 h-2 w-2 rounded-full bg-blue-500 mt-1 mr-2"></div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium">${activity.message}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">${activity.timestamp.toLocaleTimeString()}</p>
</div>
</div>
`;
}
activityLogElement.innerHTML = html;
}
updateCharts() {
// Screen Time Distribution Chart (Pie)
this.updateScreenTimeChart();
// Activity Timeline Chart (Line)
this.updateActivityTimelineChart();
}
updateScreenTimeChart() {
const ctx = document.getElementById('screenTimeChart').getContext('2d');
// Group by day for the pie chart
const days = {};
const now = new Date();
for (const session of this.sessions) {
for (const interval of session.intervals) {
const end = interval.end || now;
const duration = (end - interval.start);
const dateKey = interval.start.toDateString();
if (!days[dateKey]) {
days[dateKey] = 0;
}
days[dateKey] += duration;
}
}
const labels = Object.keys(days);
const data = labels.map(date => {
// Convert milliseconds to hours
return (days[date] / 3600000).toFixed(2);
});
if (!this.charts.screenTimeChart) {
this.charts.screenTimeChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: [
'#3B82F6',
'#10B981',
'#F59E0B',
'#EF4444',
'#8B5CF6',
'#EC4899',
'#14B8A6'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
color: this.theme === 'dark' ? '#E5E7EB' : '#374151'
}
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.label}: ${context.raw} hours`;
}
}
}
}
}
});
} else {
this.charts.screenTimeChart.data.labels = labels;
this.charts.screenTimeChart.data.datasets[0].data = data;
this.charts.screenTimeChart.update();
}
}
updateActivityTimelineChart() {
const ctx = document.getElementById('activityTimeline').getContext('2d');
// Prepare data for the last 24 hours
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
// Create time slots (every 30 minutes)
const timeSlots = [];
for (let i = 0; i < 48; i++) {
const time = new Date(oneDayAgo.getTime() + i * 30 * 60 * 1000);
timeSlots.push({
time,
active: false,
duration: 0
});
}
// Mark active periods
for (const session of this.sessions) {
for (const interval of session.intervals) {
if (!interval.end) continue;
const start = interval.start;
const end = interval.end;
for (const slot of timeSlots) {
const slotStart = slot.time;
const slotEnd = new Date(slotStart.getTime() + 30 * 60 * 1000);
// Check if interval overlaps with this time slot
if (start < slotEnd && end > slotStart) {
const overlapStart = new Date(Math.max(start, slotStart));
const overlapEnd = new Date(Math.min(end, slotEnd));
const overlapDuration = overlapEnd - overlapStart;
slot.active = true;
slot.duration += overlapDuration;
}
}
}
}
const labels = timeSlots.map(slot => slot.time.getHours() + ':' + (slot.time.getMinutes() < 10 ? '0' : '') + slot.time.getMinutes());
const data = timeSlots.map(slot => slot.active ? (slot.duration / (30 * 60 * 1000)) * 100 : 0);
if (!this.charts.activityTimelineChart) {
this.charts.activityTimelineChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Activity Level',
data: data,
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderColor: 'rgba(59, 130, 246, 1)',
borderWidth: 1,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
},
color: this.theme === 'dark' ? '#E5E7EB' : '#374151'
},
grid: {
color: this.theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
}
},
x: {
ticks: {
maxRotation: 0,
autoSkip: true,
maxTicksLimit: 12,
color: this.theme === 'dark' ? '#E5E7EB' : '#374151'
},
grid: {
color: this.theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
}
}
},
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return `Activity: ${Math.round(context.raw)}%`;
}
}
}
}
}
});
} else {
this.charts.activityTimelineChart.data.labels = labels;
this.charts.activityTimelineChart.data.datasets[0].data = data;
this.charts.activityTimelineChart.options.scales.y.grid.color = this.theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
this.charts.activityTimelineChart.options.scales.x.grid.color = this.theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
this.charts.activityTimelineChart.update();
}
}
loadData() {
// Load from localStorage
const savedData = localStorage.getItem('vibeMonitorData');
if (savedData) {
try {
const data = JSON.parse(savedData);
this.sessions = data.sessions ? data.sessions.map(session => {
return {
...session,
start: new Date(session.start),
intervals: session.intervals.map(interval => {
return {
...interval,
start: new Date(interval.start),
end: interval.end ? new Date(interval.end) : null
};
})
};
}) : [];
this.activityLog = data.activityLog ? data.activityLog.map(log => {
return {
...log,
timestamp: new Date(log.timestamp)
};
}) : [];
this.logActivity('Loaded saved data from storage');
} catch (e) {
console.error('Failed to parse saved data', e);
}
}
// Set current session to the last one if it's not ended
if (this.sessions.length > 0) {
const lastSession = this.sessions[this.sessions.length - 1];
if (lastSession.intervals.some(interval => interval.end === null)) {
this.currentSession = lastSession;
}
}
}
saveData() {
if (!this.trackingEnabled) return;
const data = {
sessions: this.sessions,
activityLog: this.activityLog
};
localStorage.setItem('vibeMonitorData', JSON.stringify(data));
}
exportData(format) {
const data = {
sessions: this.sessions,
activityLog: this.activityLog,
deviceInfo: this.deviceInfo,
batteryInfo: this.batteryInfo,
motionData: this.motionData,
locationData: this.locationData,
exportedAt: new Date().toISOString()
};
let content, mimeType, extension;
if (format === 'json') {
content = JSON.stringify(data, null, 2);
mimeType = 'application/json';
extension = 'json';
} else if (format === 'csv') {
// Convert sessions to CSV
let csvContent = "Session Start,Session Duration (ms),Interval Count,Location Count\n";
for (const session of data.sessions) {
const duration = session.intervals.reduce((total, interval) => {
const end = interval.end || new Date();
return total + (end - interval.start);
}, 0);
csvContent += `"${session.start.toISOString()}",${duration},${session.intervals.length},${session.locationData ? session.locationData.length : 0}\n`;
}
// Add activity log
csvContent += "\nActivity Log\nTimestamp,Message\n";
for (const log of data.activityLog) {
csvContent += `"${log.timestamp.toISOString()}","${log.message.replace(/"/g, '""')}"\n`;
}
content = csvContent;
mimeType = 'text/csv';
extension = 'csv';
}
// Create download link
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `vibe-monitor-export-${new Date().toISOString().slice(0, 10)}.${extension}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.logActivity(`Exported data as ${format.toUpperCase()}`);
document.getElementById('exportModal').classList.add('hidden');
}
setTheme(theme) {
this.theme = theme;
document.documentElement.classList.toggle('dark', theme === 'dark');
localStorage.setItem('theme', theme);
// Update charts if they exist
if (this.charts.screenTimeChart) {
this.charts.screenTimeChart.options.plugins.legend.labels.color = theme === 'dark' ? '#E5E7EB' : '#374151';
this.charts.screenTimeChart.update();
}
if (this.charts.activityTimelineChart) {
this.charts.activityTimelineChart.options.scales.y.grid.color = theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
this.charts.activityTimelineChart.options.scales.x.grid.color = theme === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
this.charts.activityTimelineChart.update();
}
}
toggleTheme() {
this.setTheme(this.theme === 'light' ? 'dark' : 'light');
}
}
// Register service worker for PWA functionality
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then(registration => {
console.log('ServiceWorker registration successful');
}).catch(err => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Doobdeedoo/vieb-monitor" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>