Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions client/components/activity/activity.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<template name="activity">
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">ActivityWatch Integration</h1>
{{#if isLoading}}
<div class="alert alert-info mb-6">
<span class="loading loading-spinner loading-sm"></span>
<span>Loading activity data...</span>
</div>
{{/if}}
<!-- API Key Section -->
<div class="card bg-base-200 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">🔑 Your API Key</h2>
<p class="text-sm text-gray-600 mb-4">
Use this API key in the browser extension to sync your ActivityWatch data.
</p>

{{#if apiKey}}
<div class="form-control mb-4">
<label class="label">
<span class="label-text font-semibold">API Key</span>
</label>
<div class="flex gap-2">
<input
type="{{#if showApiKey}}text{{else}}password{{/if}}"
value="{{apiKey}}"
readonly
class="input input-bordered flex-1 font-mono text-sm bg-white"
/>
<button class="btn btn-outline toggle-api-key">
{{#if showApiKey}}👁️ Hide{{else}}👁️ Show{{/if}}
</button>
<button class="btn btn-primary copy-api-key">
{{#if copiedMessage}}
✓ Copied!
{{else}}
📋 Copy
{{/if}}
</button>
</div>
</div>

<button class="btn btn-warning btn-sm generate-api-key {{#if generatingKey}}loading{{/if}}">
{{#if generatingKey}}
Generating...
{{else}}
🔄 Regenerate API Key
{{/if}}
</button>
{{else}}
<button class="btn btn-primary generate-api-key {{#if generatingKey}}loading{{/if}}">
{{#if generatingKey}}
Generating...
{{else}}
Generate API Key
{{/if}}
</button>
{{/if}}

<div class="alert alert-info mt-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div>
<h3 class="font-bold">Setup Instructions:</h3>
<ol class="list-decimal list-inside text-sm mt-2">
<li>Download the TimeHarbor browser extension</li>
<li>Copy your API key above</li>
<li>Configure the extension with your API key</li>
<li>Your data will sync automatically every 60 seconds</li>
</ol>
</div>
</div>
</div>
</div>

<!-- Sync Status -->
<div class="stats shadow mb-6 w-full">
<div class="stat">
<div class="stat-title">Last Sync</div>
<div class="stat-value text-2xl">{{lastSync}}</div>
<div class="stat-desc">
{{#if activityData}}
✅ Connected
{{else}}
⏳ Waiting for data...
{{/if}}
</div>
</div>

<div class="stat">
<div class="stat-title">Active Buckets</div>
<div class="stat-value text-2xl">{{buckets.length}}</div>
<div class="stat-desc">Tracking sources</div>
</div>

{{#if activityData}}
<div class="stat">
<div class="stat-title">Hostname</div>
<div class="stat-value text-xl">{{activityData.hostname}}</div>
<div class="stat-desc">Device name</div>
</div>
{{/if}}
</div>

<!-- Buckets Table -->
{{#if buckets.length}}
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">📊 Activity Buckets</h2>

<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>Bucket ID</th>
<th>Hostname</th>
<th>Type</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
{{#each buckets}}
<tr>
<td class="font-mono text-sm">{{id}}</td>
<td>{{hostname}}</td>
<td><span class="badge badge-primary">{{type}}</span></td>
<td>{{formatTimestamp last_updated}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{else}}
<div class="alert alert-warning shadow-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<div>
<h3 class="font-bold">No activity data yet</h3>
<div class="text-sm">Make sure the browser extension is installed and configured with your API key.</div>
</div>
</div>
{{/if}}
</div>
</template>
138 changes: 138 additions & 0 deletions client/components/activity/activity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { Meteor } from 'meteor/meteor';
import { ActivityData } from '../../../collections.js';
import './activity.html';

Template.activity.onCreated(function() {
const instance = this;

// Reactive variables
instance.apiKey = new ReactiveVar(null);
instance.showApiKey = new ReactiveVar(false);
instance.copiedMessage = new ReactiveVar(false);
instance.generatingKey = new ReactiveVar(false);

instance.autorun(() => {
instance.subscribe('myActivityData');
});

// Get user's API key on load
Meteor.call('activitywatch.getApiKey', (error, apiKey) => {
if (error) {
console.error('Error getting API key:', error);
} else {
console.log('Got API key:', apiKey);
instance.apiKey.set(apiKey);
}
});
});

Template.activity.helpers({

isLoading() {
return !Template.instance().subscriptionsReady();
},
// Get activity data from MongoDB (not from localhost:5600)
activityData() {
return ActivityData.findOne({ userId: Meteor.userId() });
},

// Convert buckets object to array for display
buckets() {
const data = ActivityData.findOne({ userId: Meteor.userId() });
if (!data || !data.buckets) return [];

return Object.keys(data.buckets).map(key => ({
id: key,
...data.buckets[key]
}));
},

// Show when data was last synced
lastSync() {
const data = ActivityData.findOne({ userId: Meteor.userId() });
if (!data || !data.lastSync) return 'Never';

const date = new Date(data.lastSync);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);

if (diffInSeconds < 60) return `${diffInSeconds}s ago`;
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
return date.toLocaleString();
},

apiKey() {
return Template.instance().apiKey.get();
},

showApiKey() {
return Template.instance().showApiKey.get();
},

copiedMessage() {
return Template.instance().copiedMessage.get();
},

generatingKey() {
return Template.instance().generatingKey.get();
},

formatTimestamp(timestamp) {
if (!timestamp) return 'Never';

const date = new Date(timestamp);
const now = new Date();
const diffInSeconds = Math.floor((now - date) / 1000);

if (diffInSeconds < 60) return `${diffInSeconds}s ago`;
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
return `${Math.floor(diffInSeconds / 86400)}d ago`;
}
});

Template.activity.events({
'click .generate-api-key'(event, instance) {
event.preventDefault();

if (instance.generatingKey.get()) return;

instance.generatingKey.set(true);

Meteor.call('activitywatch.generateApiKey', (error, apiKey) => {
instance.generatingKey.set(false);

if (error) {
console.error('Error generating API key:', error);
alert('Error generating API key: ' + error.reason);
} else {
console.log('Generated API key:', apiKey);
instance.apiKey.set(apiKey);
alert('New API key generated successfully!');
}
});
},

'click .toggle-api-key'(event, instance) {
event.preventDefault();
instance.showApiKey.set(!instance.showApiKey.get());
},

'click .copy-api-key'(event, instance) {
event.preventDefault();
const apiKey = instance.apiKey.get();

if (apiKey) {
navigator.clipboard.writeText(apiKey).then(() => {
instance.copiedMessage.set(true);
setTimeout(() => instance.copiedMessage.set(false), 2000);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy API key');
});
}
}
});
1 change: 1 addition & 0 deletions client/components/layout/MainLayout.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ <h2 class="text-2xl font-bold">TimeHarbor</h2>
<a href="/" class="btn btn-ghost hover:bg-primary-focus">Home</a>
<a href="/teams" class="btn btn-ghost hover:bg-primary-focus">Teams</a>
<a href="/tickets" class="btn btn-ghost hover:bg-primary-focus">Tickets</a>
<a href="/activity" class="btn btn-ghost hover:bg-primary-focus">Activity</a>
<a href="/calendar" class="btn btn-ghost hover:bg-primary-focus">Calendar</a>
<a href="/admin" class="btn btn-ghost hover:bg-primary-focus">Admin Review</a>
</nav>
Expand Down
3 changes: 2 additions & 1 deletion client/components/layout/MainLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ if (Template.mainLayout) {
} else if (href === '/admin' || target === 'admin') {
FlowRouter.go('/admin');
} else {
FlowRouter.go('/');
// For any other href (e.g. /activity), forward directly to FlowRouter
FlowRouter.go(href);
}
},
'click #logoutBtn'(event, instance) {
Expand Down
10 changes: 8 additions & 2 deletions client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import './components/calendar/CalendarPage.html';
import './components/admin/admin.html';
import './components/timesheet/TimesheetPage.html';
import './components/guide/UserGuide.html';

import './components/activity/activity.html';
// Import component JS files
import './components/auth/AuthPage.js';
import './components/layout/MainLayout.js';
Expand All @@ -22,9 +22,15 @@ import './components/calendar/CalendarPage.js';
import './components/admin/admin.js';
import './components/timesheet/TimesheetPage.js';
import './components/guide/UserGuide.js';
import './components/activity/activity.js';

// Import routing configuration
import './routes.js';

// Import utilities
import './utils/DateUtils.js';
import './utils/DateUtils.js';

import { ActivityData } from '../collections.js';

// Make available in console for debugging
window.ActivityData = ActivityData;
10 changes: 10 additions & 0 deletions client/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ FlowRouter.route('/admin', {
}
});

// Activity route (ActivityWatch)
FlowRouter.route('/activity', {
name: 'activity',
action() {
requireAuth(this, FlowRouter.go);
currentScreen.set('mainLayout');
currentRouteTemplate.set('activity');
}
});

// Individual Timesheet route - NEW!
FlowRouter.route('/timesheet/:userId', {
name: 'timesheet',
Expand Down
1 change: 1 addition & 0 deletions collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export const Teams = new Mongo.Collection('teams');
export const Sessions = new Mongo.Collection('sessions');

export const ClockEvents = new Mongo.Collection('clockevents');
export const ActivityData = new Mongo.Collection('activityData');
Loading