fix(worktime): use Europe/Moscow day boundary for aw-rus today reports
This commit is contained in:
@@ -294,9 +294,36 @@
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
||||
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
||||
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
||||
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
||||
|
||||
- name: Установить скрипт AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py"
|
||||
dest: /usr/local/bin/aw-worktime-api.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Установить systemd unit AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service"
|
||||
dest: /etc/systemd/system/aw-worktime-api.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Перезагрузить systemd после установки AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Включить и перезапустить AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-api.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
|
||||
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
|
||||
ansible.builtin.command:
|
||||
cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh"
|
||||
|
||||
@@ -9,6 +9,7 @@ aw_server_log_dir: "/var/log/activitywatch"
|
||||
aw_server_user: "activitywatch"
|
||||
aw_server_group: "activitywatch"
|
||||
aw_worktime_report_base: "http://10.10.10.13:5610"
|
||||
aw_worktime_timezone: "Europe/Moscow"
|
||||
|
||||
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ aw_server_db_path: "/var/lib/activitywatch/aw-server-rust/sqlite.db"
|
||||
aw_server_user: "activitywatch"
|
||||
aw_server_group: "activitywatch"
|
||||
aw_worktime_report_base: "http://10.10.10.13:5610"
|
||||
aw_worktime_timezone: "Europe/Moscow"
|
||||
|
||||
aw_repo_root: "/mnt/usb_hdd2/Projects/ActivityWatch-Russian"
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
- install_aw_server.sh
|
||||
- apply_webui_ru_patch.sh
|
||||
- activitywatch-server.service
|
||||
- aw-worktime-api.py
|
||||
- aw-worktime-api.service
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
- install_aw_server.sh
|
||||
- apply_webui_ru_patch.sh
|
||||
- activitywatch-server.service
|
||||
- aw-worktime-api.py
|
||||
- aw-worktime-api.service
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
||||
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
||||
no_log: true
|
||||
|
||||
- name: Передать AW server env внутрь CT
|
||||
|
||||
@@ -10,3 +10,4 @@ AW_SERVER_LOG_DIR=/var/log/activitywatch
|
||||
AW_SERVER_USER=activitywatch
|
||||
AW_SERVER_GROUP=activitywatch
|
||||
AW_WORKTIME_REPORT_BASE=http://10.10.10.13:5610
|
||||
AW_WORKTIME_TZ=Europe/Moscow
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
AW = "http://127.0.0.1:5600/api/0"
|
||||
REPORT_TZ = ZoneInfo(os.environ.get("AW_WORKTIME_TZ", "Europe/Moscow"))
|
||||
|
||||
|
||||
def get(u):
|
||||
with urllib.request.urlopen(u, timeout=30) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def pts(s):
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
|
||||
|
||||
def report_today():
|
||||
now_local = datetime.now(REPORT_TZ)
|
||||
start_local = datetime(now_local.year, now_local.month, now_local.day, tzinfo=REPORT_TZ)
|
||||
end_local = start_local + timedelta(days=1) - timedelta(seconds=1)
|
||||
start = start_local.astimezone(timezone.utc)
|
||||
end = end_local.astimezone(timezone.utc)
|
||||
b = get(AW + "/buckets")
|
||||
sb = next((k for k in b if k.startswith("aw-worktime-sessions_")), None)
|
||||
if not sb:
|
||||
return []
|
||||
ev = get(f"{AW}/buckets/{sb}/events?limit=50000")
|
||||
by = {}
|
||||
for e in ev:
|
||||
ts = pts(e.get("timestamp"))
|
||||
if ts < start or ts > end:
|
||||
continue
|
||||
d = e.get("data") or {}
|
||||
user = (d.get("username") or "").strip()
|
||||
if not user:
|
||||
continue
|
||||
state = (d.get("state") or "").lower()
|
||||
active = ("актив" in state) or (state == "active")
|
||||
row = by.setdefault(user, {"active": set(), "first": None, "last": None, "rows": 0})
|
||||
row["rows"] += 1
|
||||
if active:
|
||||
second = ts.replace(microsecond=0)
|
||||
row["active"].add(second)
|
||||
row["first"] = second if row["first"] is None or second < row["first"] else row["first"]
|
||||
row["last"] = second if row["last"] is None or second > row["last"] else row["last"]
|
||||
rows = []
|
||||
full = int((end_local - start_local).total_seconds())
|
||||
for user in sorted(by):
|
||||
row = by[user]
|
||||
active_seconds = len(row["active"])
|
||||
rows.append({
|
||||
"user": user,
|
||||
"active_seconds": active_seconds,
|
||||
"active_hhmm": "%02d:%02d" % (active_seconds // 3600, (active_seconds % 3600) // 60),
|
||||
"first_activity": row["first"].isoformat().replace("+00:00", "Z") if row["first"] else "",
|
||||
"last_activity": row["last"].isoformat().replace("+00:00", "Z") if row["last"] else "",
|
||||
"idle_seconds": max(0, full - active_seconds),
|
||||
"sessions_count": row["rows"],
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if not self.path.startswith("/reports/worktime/today"):
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
fmt = "json"
|
||||
if "format=csv" in self.path:
|
||||
fmt = "csv"
|
||||
rows = report_today()
|
||||
if fmt == "csv":
|
||||
out = io.StringIO()
|
||||
writer = csv.DictWriter(
|
||||
out,
|
||||
fieldnames=[
|
||||
"user",
|
||||
"active_seconds",
|
||||
"active_hhmm",
|
||||
"first_activity",
|
||||
"last_activity",
|
||||
"idle_seconds",
|
||||
"sessions_count",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
data = out.getvalue().encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/csv; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
return
|
||||
obj = {
|
||||
"generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
"report_timezone": str(REPORT_TZ),
|
||||
"rows": rows,
|
||||
}
|
||||
data = json.dumps(obj, ensure_ascii=False, indent=2).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
|
||||
HTTPServer(("0.0.0.0", 5610), H).serve_forever()
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=AW Worktime Report API
|
||||
After=network.target activitywatch-server.service
|
||||
Wants=activitywatch-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/activitywatch/aw-server.env
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/aw-worktime-api.py
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -24,6 +24,8 @@ required_vars=(
|
||||
BOOTSTRAP_DIR="/root/bootstrap"
|
||||
VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
||||
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
||||
|
||||
for var_name in "${required_vars[@]}"; do
|
||||
if [[ -z "${!var_name:-}" ]]; then
|
||||
@@ -89,6 +91,18 @@ systemctl enable activitywatch-server.service
|
||||
systemctl restart activitywatch-server.service
|
||||
systemctl --no-pager --full status activitywatch-server.service || true
|
||||
|
||||
if [[ -f "$WORKTIME_API_SRC" ]]; then
|
||||
install -m 0755 "$WORKTIME_API_SRC" /usr/local/bin/aw-worktime-api.py
|
||||
fi
|
||||
|
||||
if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then
|
||||
install -m 0644 "$WORKTIME_API_SERVICE_SRC" /etc/systemd/system/aw-worktime-api.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable aw-worktime-api.service
|
||||
systemctl restart aw-worktime-api.service
|
||||
systemctl --no-pager --full status aw-worktime-api.service || true
|
||||
fi
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
||||
break
|
||||
|
||||
@@ -234,9 +234,36 @@
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
||||
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
||||
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
||||
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
||||
|
||||
- name: Установить скрипт AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py"
|
||||
dest: /usr/local/bin/aw-worktime-api.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Установить systemd unit AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service"
|
||||
dest: /etc/systemd/system/aw-worktime-api.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Перезагрузить systemd после установки AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Включить и перезапустить AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-api.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
|
||||
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
|
||||
ansible.builtin.command:
|
||||
cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh"
|
||||
|
||||
@@ -8,6 +8,7 @@ aw_server_log_dir: "/var/log/activitywatch"
|
||||
aw_server_user: "activitywatch"
|
||||
aw_server_group: "activitywatch"
|
||||
aw_worktime_report_base: "http://10.10.10.13:5610"
|
||||
aw_worktime_timezone: "Europe/Moscow"
|
||||
|
||||
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
- install_aw_server.sh
|
||||
- apply_webui_ru_patch.sh
|
||||
- activitywatch-server.service
|
||||
- aw-worktime-api.py
|
||||
- aw-worktime-api.service
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
|
||||
+2
@@ -9,6 +9,8 @@
|
||||
- install_aw_server.sh
|
||||
- apply_webui_ru_patch.sh
|
||||
- activitywatch-server.service
|
||||
- aw-worktime-api.py
|
||||
- aw-worktime-api.service
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
- aw-sw-cleanup.js
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
||||
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
||||
no_log: true
|
||||
|
||||
- name: Передать AW server env внутрь CT
|
||||
|
||||
@@ -10,3 +10,4 @@ AW_SERVER_LOG_DIR=/var/log/activitywatch
|
||||
AW_SERVER_USER=activitywatch
|
||||
AW_SERVER_GROUP=activitywatch
|
||||
AW_WORKTIME_REPORT_BASE=http://10.10.10.13:5610
|
||||
AW_WORKTIME_TZ=Europe/Moscow
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
AW = "http://127.0.0.1:5600/api/0"
|
||||
REPORT_TZ = ZoneInfo(os.environ.get("AW_WORKTIME_TZ", "Europe/Moscow"))
|
||||
|
||||
|
||||
def get(u):
|
||||
with urllib.request.urlopen(u, timeout=30) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def pts(s):
|
||||
return datetime.fromisoformat(s.replace("Z", "+00:00")).astimezone(timezone.utc)
|
||||
|
||||
|
||||
def report_today():
|
||||
now_local = datetime.now(REPORT_TZ)
|
||||
start_local = datetime(now_local.year, now_local.month, now_local.day, tzinfo=REPORT_TZ)
|
||||
end_local = start_local + timedelta(days=1) - timedelta(seconds=1)
|
||||
start = start_local.astimezone(timezone.utc)
|
||||
end = end_local.astimezone(timezone.utc)
|
||||
b = get(AW + "/buckets")
|
||||
sb = next((k for k in b if k.startswith("aw-worktime-sessions_")), None)
|
||||
if not sb:
|
||||
return []
|
||||
ev = get(f"{AW}/buckets/{sb}/events?limit=50000")
|
||||
by = {}
|
||||
for e in ev:
|
||||
ts = pts(e.get("timestamp"))
|
||||
if ts < start or ts > end:
|
||||
continue
|
||||
d = e.get("data") or {}
|
||||
user = (d.get("username") or "").strip()
|
||||
if not user:
|
||||
continue
|
||||
state = (d.get("state") or "").lower()
|
||||
active = ("актив" in state) or (state == "active")
|
||||
row = by.setdefault(user, {"active": set(), "first": None, "last": None, "rows": 0})
|
||||
row["rows"] += 1
|
||||
if active:
|
||||
second = ts.replace(microsecond=0)
|
||||
row["active"].add(second)
|
||||
row["first"] = second if row["first"] is None or second < row["first"] else row["first"]
|
||||
row["last"] = second if row["last"] is None or second > row["last"] else row["last"]
|
||||
rows = []
|
||||
full = int((end_local - start_local).total_seconds())
|
||||
for user in sorted(by):
|
||||
row = by[user]
|
||||
active_seconds = len(row["active"])
|
||||
rows.append({
|
||||
"user": user,
|
||||
"active_seconds": active_seconds,
|
||||
"active_hhmm": "%02d:%02d" % (active_seconds // 3600, (active_seconds % 3600) // 60),
|
||||
"first_activity": row["first"].isoformat().replace("+00:00", "Z") if row["first"] else "",
|
||||
"last_activity": row["last"].isoformat().replace("+00:00", "Z") if row["last"] else "",
|
||||
"idle_seconds": max(0, full - active_seconds),
|
||||
"sessions_count": row["rows"],
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if not self.path.startswith("/reports/worktime/today"):
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
fmt = "json"
|
||||
if "format=csv" in self.path:
|
||||
fmt = "csv"
|
||||
rows = report_today()
|
||||
if fmt == "csv":
|
||||
out = io.StringIO()
|
||||
writer = csv.DictWriter(
|
||||
out,
|
||||
fieldnames=[
|
||||
"user",
|
||||
"active_seconds",
|
||||
"active_hhmm",
|
||||
"first_activity",
|
||||
"last_activity",
|
||||
"idle_seconds",
|
||||
"sessions_count",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
data = out.getvalue().encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/csv; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
return
|
||||
obj = {
|
||||
"generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
"report_timezone": str(REPORT_TZ),
|
||||
"rows": rows,
|
||||
}
|
||||
data = json.dumps(obj, ensure_ascii=False, indent=2).encode("utf-8")
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
|
||||
HTTPServer(("0.0.0.0", 5610), H).serve_forever()
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=AW Worktime Report API
|
||||
After=network.target activitywatch-server.service
|
||||
Wants=activitywatch-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/activitywatch/aw-server.env
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/aw-worktime-api.py
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -24,6 +24,8 @@ required_vars=(
|
||||
BOOTSTRAP_DIR="/root/bootstrap"
|
||||
VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
||||
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
||||
|
||||
for var_name in "${required_vars[@]}"; do
|
||||
if [[ -z "${!var_name:-}" ]]; then
|
||||
@@ -89,6 +91,18 @@ systemctl enable activitywatch-server.service
|
||||
systemctl restart activitywatch-server.service
|
||||
systemctl --no-pager --full status activitywatch-server.service || true
|
||||
|
||||
if [[ -f "$WORKTIME_API_SRC" ]]; then
|
||||
install -m 0755 "$WORKTIME_API_SRC" /usr/local/bin/aw-worktime-api.py
|
||||
fi
|
||||
|
||||
if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then
|
||||
install -m 0644 "$WORKTIME_API_SERVICE_SRC" /etc/systemd/system/aw-worktime-api.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable aw-worktime-api.service
|
||||
systemctl restart aw-worktime-api.service
|
||||
systemctl --no-pager --full status aw-worktime-api.service || true
|
||||
fi
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user