deployment, frontend updates

This commit is contained in:
buenosairesam
2026-01-26 15:11:03 -03:00
parent bf7bcbc37a
commit 3122facaba
16 changed files with 1692 additions and 113 deletions

97
dmapp/dmdb/sync.py Normal file
View File

@@ -0,0 +1,97 @@
"""
dmsync - MongoDB Change Streams sync daemon
Watches local deskmeter database and pushes changes to remote MongoDB
Requires local MongoDB to be configured as a replica set.
Uses resume tokens to continue from last position after restart.
"""
import json
import logging
import os
from pathlib import Path
from pymongo import MongoClient
from pymongo.errors import PyMongoError
logging.basicConfig(
level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s"
)
log = logging.getLogger("dmsync")
RESUME_TOKEN_FILE = Path.home() / ".dmsync-resume-token"
REMOTE_HOST = os.environ.get("DMSYNC_REMOTE_HOST", "mcrn.ar")
REMOTE_PORT = int(os.environ.get("DMSYNC_REMOTE_PORT", 27017))
COLLECTIONS = ("switch", "task", "task_history", "state")
def load_resume_token():
"""Load resume token from file if exists."""
if RESUME_TOKEN_FILE.exists():
try:
return json.loads(RESUME_TOKEN_FILE.read_text())
except (json.JSONDecodeError, IOError) as e:
log.warning(f"Failed to load resume token: {e}")
return None
def save_resume_token(token):
"""Persist resume token to file."""
try:
RESUME_TOKEN_FILE.write_text(json.dumps(token))
except IOError as e:
log.error(f"Failed to save resume token: {e}")
def sync():
"""Main sync loop using Change Streams."""
log.info(f"Connecting to local MongoDB...")
local = MongoClient()
log.info(f"Connecting to remote MongoDB at {REMOTE_HOST}:{REMOTE_PORT}...")
remote = MongoClient(REMOTE_HOST, REMOTE_PORT)
local_db = local.deskmeter
remote_db = remote.deskmeter
resume_token = load_resume_token()
if resume_token:
log.info("Resuming from saved token")
watch_kwargs = {"resume_after": resume_token} if resume_token else {}
# Watch for inserts, updates, and replaces on the database
pipeline = [{"$match": {"operationType": {"$in": ["insert", "update", "replace"]}}}]
log.info(f"Watching collections: {', '.join(COLLECTIONS)}")
try:
with local_db.watch(pipeline, **watch_kwargs) as stream:
for change in stream:
collection = change["ns"]["coll"]
if collection not in COLLECTIONS:
continue
doc = change.get("fullDocument")
if not doc:
continue
# Upsert to remote
result = remote_db[collection].replace_one(
{"_id": doc["_id"]}, doc, upsert=True
)
action = "inserted" if result.upserted_id else "updated"
log.info(f"{collection}: {action} {doc['_id']}")
save_resume_token(stream.resume_token)
except PyMongoError as e:
log.error(f"MongoDB error: {e}")
raise
if __name__ == "__main__":
log.info("Starting dmsync daemon")
sync()

View File

@@ -1,8 +1,18 @@
from datetime import datetime, timedelta
from flask import Blueprint, render_template, jsonify
from flask import Blueprint, jsonify, render_template
from .get_period_times import get_period_totals, task_or_none, timezone, get_work_period_totals, get_current_task_info, convert_seconds, get_task_time_seconds, get_task_blocks_calendar, get_raw_switches
from .get_period_times import (
convert_seconds,
get_current_task_info,
get_period_totals,
get_raw_switches,
get_task_blocks_calendar,
get_task_time_seconds,
get_work_period_totals,
task_or_none,
timezone,
)
dmbp = Blueprint("deskmeter", __name__, url_prefix="/", template_folder="templates")
@@ -22,7 +32,7 @@ def calendar_view(scope="daily", year=None, month=None, day=None):
from flask import request
task = None
grid = int(request.args.get('grid', 1)) # Grid hours: 1, 3, or 6
grid = int(request.args.get("grid", 1)) # Grid hours: 1, 3, or 6
if grid not in [1, 3, 6]:
grid = 1
@@ -33,12 +43,16 @@ def calendar_view(scope="daily", year=None, month=None, day=None):
if not day:
day = datetime.today().day
base_date = datetime(year, month, day).replace(hour=0, minute=0, second=0, tzinfo=timezone)
base_date = datetime(year, month, day).replace(
hour=0, minute=0, second=0, tzinfo=timezone
)
if scope == "daily":
start = base_date
end = base_date.replace(hour=23, minute=59, second=59)
blocks = get_task_blocks_calendar(start, end, task, min_block_seconds=60, grid_hours=grid)
blocks = get_task_blocks_calendar(
start, end, task, min_block_seconds=60, grid_hours=grid
)
prev_date = base_date - timedelta(days=1)
next_date = base_date + timedelta(days=1)
days = [base_date]
@@ -46,7 +60,9 @@ def calendar_view(scope="daily", year=None, month=None, day=None):
elif scope == "weekly":
start = base_date - timedelta(days=base_date.weekday())
end = start + timedelta(days=6, hours=23, minutes=59, seconds=59)
blocks = get_task_blocks_calendar(start, end, task, min_block_seconds=300, grid_hours=grid)
blocks = get_task_blocks_calendar(
start, end, task, min_block_seconds=300, grid_hours=grid
)
prev_date = start - timedelta(days=7)
next_date = start + timedelta(days=7)
days = [start + timedelta(days=i) for i in range(7)]
@@ -57,7 +73,9 @@ def calendar_view(scope="daily", year=None, month=None, day=None):
end = datetime(year + 1, 1, 1, tzinfo=timezone) - timedelta(seconds=1)
else:
end = datetime(year, month + 1, 1, tzinfo=timezone) - timedelta(seconds=1)
blocks = get_task_blocks_calendar(start, end, task, min_block_seconds=600, grid_hours=grid)
blocks = get_task_blocks_calendar(
start, end, task, min_block_seconds=600, grid_hours=grid
)
if month == 1:
prev_date = datetime(year - 1, 12, 1, tzinfo=timezone)
else:
@@ -75,7 +93,9 @@ def calendar_view(scope="daily", year=None, month=None, day=None):
scope = "daily"
start = base_date
end = base_date.replace(hour=23, minute=59, second=59)
blocks = get_task_blocks_calendar(start, end, task, min_block_seconds=60, grid_hours=grid)
blocks = get_task_blocks_calendar(
start, end, task, min_block_seconds=60, grid_hours=grid
)
prev_date = base_date - timedelta(days=1)
next_date = base_date + timedelta(days=1)
days = [base_date]
@@ -91,7 +111,7 @@ def calendar_view(scope="daily", year=None, month=None, day=None):
next_date=next_date,
days=days,
grid=grid,
auto_refresh=False
auto_refresh=False,
)
@@ -111,7 +131,9 @@ def switches_view(scope="daily", year=None, month=None, day=None):
if not day:
day = datetime.today().day
base_date = datetime(year, month, day).replace(hour=0, minute=0, second=0, tzinfo=timezone)
base_date = datetime(year, month, day).replace(
hour=0, minute=0, second=0, tzinfo=timezone
)
if scope == "daily":
start = base_date
@@ -157,7 +179,7 @@ def switches_view(scope="daily", year=None, month=None, day=None):
base_date=base_date,
prev_date=prev_date,
next_date=next_date,
auto_refresh=False
auto_refresh=False,
)
@@ -176,13 +198,17 @@ def index(task=None):
# Get current task info
current_task_id, current_task_path = get_current_task_info()
current_task_time = None
if current_task_id:
total_seconds = get_task_time_seconds(start, end, current_task_id)
if total_seconds > 0:
current_task_time = convert_seconds(total_seconds)
return render_template("main.html", rows=rows, current_task_path=current_task_path, current_task_time=current_task_time, auto_refresh=True)
# Get all tasks worked on today
task_rows = get_work_period_totals(start, end)
return render_template(
"main.html",
rows=rows,
current_task_path=current_task_path,
task_rows=task_rows,
auto_refresh=True,
)
@dmbp.route("/api/current_task")
@@ -191,10 +217,9 @@ def api_current_task():
JSON API endpoint returning current task information
"""
current_task_id, current_task_path = get_current_task_info()
return jsonify({
"task_id": current_task_id,
"task_path": current_task_path or "no task"
})
return jsonify(
{"task_id": current_task_id, "task_path": current_task_path or "no task"}
)
@dmbp.route("/api/today")
@@ -212,13 +237,16 @@ def api_today(task=None):
# Get current task info
current_task_id, current_task_path = get_current_task_info()
current_task_time = None
if current_task_id:
total_seconds = get_task_time_seconds(start, end, current_task_id)
if total_seconds > 0:
current_task_time = convert_seconds(total_seconds)
return render_template("main_content.html", rows=rows, current_task_path=current_task_path, current_task_time=current_task_time)
# Get all tasks worked on today
task_rows = get_work_period_totals(start, end)
return render_template(
"main_content.html",
rows=rows,
current_task_path=current_task_path,
task_rows=task_rows,
)
@dmbp.route("/day/<int:month>/<int:day>")

View File

@@ -1,3 +1,4 @@
import os
from collections import Counter, defaultdict
from datetime import datetime, timedelta
from pathlib import Path
@@ -8,7 +9,7 @@ from pymongo import MongoClient
timezone = ZoneInfo("America/Argentina/Buenos_Aires")
utctz = ZoneInfo("UTC")
client = MongoClient()
client = MongoClient(os.environ.get("MONGODB_HOST", "localhost"))
db = client.deskmeter
switches = db.switch
tasks = db.task

View File

@@ -1,80 +1,104 @@
<html>
<head>
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='styles/dm.css') }}"> -->
{% block head %}
{% endblock %}
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='styles/dm.css') }}"> -->
{% block head %} {% endblock %}
<style>
body
{
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh; /* This ensures that the container takes the full height of the viewport */
margin: 0; /* Remove default margin */
background-color: #1a1a1a;
color: #e0e0e0;
}
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
background-color: #1a1a1a;
color: #e0e0e0;
font-family: monospace;
}
.nav-bar {
position: absolute;
top: 20px;
left: 20px;
display: flex;
gap: 15px;
}
.nav-bar {
position: absolute;
top: 20px;
left: 20px;
display: flex;
gap: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #444;
}
.nav-bar a {
text-decoration: none;
color: #6b9bd1;
font-size: 11pt;
padding: 5px 10px;
border-radius: 3px;
background-color: #2a2a2a;
}
.nav-bar a {
text-decoration: none;
color: #6b9bd1;
font-size: 12pt;
padding: 5px 10px;
border-radius: 3px;
}
.nav-bar a:hover {
background-color: #3a3a3a;
}
.nav-bar a:hover {
background-color: #2a2a2a;
}
.grey {
color: #888;
}
.grey {
color: #666;
}
.blue {
color: #6b9bd1;
}
.blue {
color: #6b9bd1;
}
table {
font-size: 84pt
}
td {
padding-right: 100px;
}
</style>
table {
font-size: clamp(14pt, 3vw, 28pt);
border-collapse: collapse;
}
{% if auto_refresh %}
<script>
function refreshData() {
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/today', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
document.getElementById('content-container').innerHTML = xhr.responseText;
}
};
xhr.send();
}
td {
padding: 0.1em 0.5em;
}
// Auto-refresh every 5 seconds using AJAX
setInterval(refreshData, 5000);
</script>
{% endif %}
td:first-child {
text-align: right;
padding-right: 0.3em;
}
td:last-child {
text-align: left;
font-variant-numeric: tabular-nums;
}
.workspace-table {
font-size: clamp(10pt, 2vw, 18pt);
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #444;
}
.workspace-table td {
padding: 0.05em 0.4em;
}
</style>
{% if auto_refresh %}
<script>
function refreshData() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/api/today", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
document.getElementById("content-container").innerHTML =
xhr.responseText;
}
};
xhr.send();
}
// Auto-refresh every 5 seconds using AJAX
setInterval(refreshData, 5000);
</script>
{% endif %}
</head>
<body>
<!-- agregar función que me diga cuanto tiempo hace que esta activo el escritorio
(calcular el delta con el ultimo switch y pasarlo a mm:ss) -->
@@ -87,9 +111,7 @@
</div>
<div id="content-container">
{% block content %}
{% include 'main_content.html' %}
{% endblock %}
{% block content %} {% include 'main_content.html' %} {% endblock %}
</div>
</body>
</html>
</html>

View File

@@ -1,23 +1,39 @@
{% if current_task_path and current_task_time %}
<div id="current-task-info" style="font-size: 48pt; margin-bottom: 40px; text-align: center;">
<div style="color: #e0e0e0;">{{ current_task_path }}</div>
<div style="color: #999; font-size: 36pt;">{{ current_task_time }}</div>
{% if current_task_path %}
<div
id="current-task-info"
style="
font-size: clamp(10pt, 2vw, 16pt);
margin-bottom: 15px;
text-align: center;
color: #666;
"
>
Current: <span style="color: #6b9bd1">{{ current_task_path }}</span>
</div>
{% endif %} {% if task_rows %}
<div id="task-list" style="margin-bottom: 25px">
<table>
<tbody>
{% for row in task_rows %}
<tr>
<td>{{ row["ws"] }}</td>
<td>{{ row["total"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<table>
<tbody>
{% for row in rows %}
{% if row["ws"] in ['Away', 'Other'] %}
{% set my_class = 'grey' %}
{% elif row["ws"] in ['Active', 'Idle'] %}
{% set my_class = 'blue' %}
{% else %}
{% set my_class = '' %}
{% endif %}
<table class="workspace-table">
<tbody>
{% for row in rows %} {% if row["ws"] in ['Away', 'Other'] %} {% set
my_class = 'grey' %} {% elif row["ws"] in ['Active', 'Idle'] %} {% set
my_class = 'blue' %} {% else %} {% set my_class = '' %} {% endif %}
<tr>
<td class="{{my_class}}" >{{ row["ws"] }}</td>
<td class="{{my_class}}" >{{ row["total"] }}</td>
<td class="{{my_class}}">{{ row["ws"] }}</td>
<td class="{{my_class}}">{{ row["total"] }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</tbody>
</table>