feat(webui): add HTML and panel view for worktime reports
This commit is contained in:
@@ -254,6 +254,7 @@
|
|||||||
loop:
|
loop:
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
|
||||||
|
|
||||||
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
||||||
@@ -270,6 +271,7 @@
|
|||||||
loop:
|
loop:
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
||||||
|
|
||||||
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
- aw-sw-cleanup.js
|
- aw-sw-cleanup.js
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
- aw-sw-cleanup.js
|
- aw-sw-cleanup.js
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}"
|
|||||||
REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://10.10.10.13:5610}"
|
REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://10.10.10.13:5610}"
|
||||||
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
||||||
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js"
|
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js"
|
||||||
|
WORKTIME_PANEL_SRC="/root/bootstrap/aw-worktime-panel.js"
|
||||||
HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json"
|
HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json"
|
||||||
INDEX_HTML="$WEBUI_DIR/index.html"
|
INDEX_HTML="$WEBUI_DIR/index.html"
|
||||||
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
|
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
|
||||||
TS=$(date +%Y%m%d%H%M%S)
|
TS=$(date +%Y%m%d%H%M%S)
|
||||||
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
|
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
|
||||||
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
|
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
|
||||||
|
WORKTIME_PANEL_TARGET="$WEBUI_DIR/js/aw-worktime-panel.js"
|
||||||
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
|
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
|
||||||
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
|
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
|
||||||
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
|
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
|
||||||
@@ -29,33 +31,76 @@ CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){retu
|
|||||||
|
|
||||||
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
|
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
|
||||||
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
|
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
|
||||||
|
[[ -f "$WORKTIME_PANEL_SRC" ]] || { echo "missing $WORKTIME_PANEL_SRC" >&2; exit 1; }
|
||||||
[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; }
|
[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; }
|
||||||
[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; }
|
[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; }
|
||||||
|
|
||||||
install -d "$WEBUI_DIR/js"
|
install -d "$WEBUI_DIR/js"
|
||||||
install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET"
|
install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET"
|
||||||
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
|
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
|
||||||
|
install -m 0644 "$WORKTIME_PANEL_SRC" "$WORKTIME_PANEL_TARGET"
|
||||||
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
|
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
|
||||||
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
|
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
|
||||||
|
|
||||||
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
|
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
|
||||||
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
|
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
|
||||||
|
worktime_panel_hash="$(sha1sum "$WORKTIME_PANEL_TARGET" | awk '{print substr($1,1,12)}')"
|
||||||
|
|
||||||
sed -i '/ru-patch-v5.js/d;/sw-cleanup.js/d;/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
|
python3 - "$WORKTIME_PANEL_TARGET" "$REPORT_BASE" <<'PY'
|
||||||
sed -i "s#</head>#<script src=\"/js/sw-cleanup.js?v=$sw_hash\"></script></head>#" "$INDEX_HTML"
|
from pathlib import Path
|
||||||
sed -i "s#</body>#<script defer=\"defer\" src=\"/js/ru-patch-v5.js?v=$patch_hash\"></script></body>#" "$INDEX_HTML"
|
import sys
|
||||||
|
|
||||||
# Replace old relative report links if they exist.
|
path = Path(sys.argv[1])
|
||||||
sed -i "s#href=\"/today.csv\"#href=\"$REPORT_BASE/reports/worktime/today?format=csv\"#g" "$INDEX_HTML"
|
report_base = sys.argv[2]
|
||||||
sed -i "s#href=\"/today.json\"#href=\"$REPORT_BASE/reports/worktime/today\"#g" "$INDEX_HTML"
|
text = path.read_text()
|
||||||
|
text = text.replace("__AW_WORKTIME_REPORT_BASE__", report_base)
|
||||||
|
path.write_text(text)
|
||||||
|
PY
|
||||||
|
|
||||||
# Ensure report links block exists and points to worktime API on :5610.
|
python3 - "$INDEX_HTML" "$sw_hash" "$patch_hash" "$worktime_panel_hash" "$REPORT_BASE" <<'PY'
|
||||||
if ! grep -q 'id="aw-report-links"' "$INDEX_HTML"; then
|
from pathlib import Path
|
||||||
sed -i "s#</body>#<div id=\"aw-report-links\" style=\"position:fixed;right:12px;bottom:12px;z-index:99999;background:#111;color:#fff;padding:8px 10px;border-radius:8px;font:12px/1.4 sans-serif;opacity:.9\">RDP report: <a href=\"$REPORT_BASE/reports/worktime/today?format=csv\" style=\"color:#7dd3fc\" target=\"_blank\">CSV</a> | <a href=\"$REPORT_BASE/reports/worktime/today\" style=\"color:#86efac\" target=\"_blank\">JSON</a></div></body>#g" "$INDEX_HTML"
|
import re
|
||||||
fi
|
import sys
|
||||||
|
|
||||||
# Normalize existing report links block to current REPORT_BASE.
|
path = Path(sys.argv[1])
|
||||||
sed -i "s#<div id=\"aw-report-links\"[^<]*RDP report: <a href=\"[^\"]*\" style=\"color:#7dd3fc\" target=\"_blank\">CSV</a> | <a href=\"[^\"]*\" style=\"color:#86efac\" target=\"_blank\">JSON</a></div>#<div id=\"aw-report-links\" style=\"position:fixed;right:12px;bottom:12px;z-index:99999;background:#111;color:#fff;padding:8px 10px;border-radius:8px;font:12px/1.4 sans-serif;opacity:.9\">RDP report: <a href=\"$REPORT_BASE/reports/worktime/today?format=csv\" style=\"color:#7dd3fc\" target=\"_blank\">CSV</a> | <a href=\"$REPORT_BASE/reports/worktime/today\" style=\"color:#86efac\" target=\"_blank\">JSON</a></div>#g" "$INDEX_HTML"
|
sw_hash = sys.argv[2]
|
||||||
|
patch_hash = sys.argv[3]
|
||||||
|
panel_hash = sys.argv[4]
|
||||||
|
report_base = sys.argv[5]
|
||||||
|
content = path.read_text()
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r'<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js|aw-worktime-panel\.js)[^>]*></script>',
|
||||||
|
'',
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
content = re.sub(r"; frame-src 'self' [^\";>]*", "", content)
|
||||||
|
content = content.replace(
|
||||||
|
"script-src 'self' 'unsafe-eval'",
|
||||||
|
f"script-src 'self' 'unsafe-eval'; frame-src 'self' {report_base}",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
content = content.replace(
|
||||||
|
"</head>",
|
||||||
|
f'<script src="/js/sw-cleanup.js?v={sw_hash}"></script></head>',
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
content = content.replace(
|
||||||
|
"</body>",
|
||||||
|
(
|
||||||
|
f'<script defer="defer" src="/js/ru-patch-v5.js?v={patch_hash}"></script>'
|
||||||
|
f'<script defer="defer" src="/js/aw-worktime-panel.js?v={panel_hash}"></script></body>'
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
if 'id="aw-report-links"' not in content:
|
||||||
|
content = content.replace(
|
||||||
|
"</body>",
|
||||||
|
'<div id="aw-report-links" style="position:fixed;right:12px;bottom:12px;z-index:99999;background:#111;color:#fff;padding:8px 10px;border-radius:8px;font:12px/1.4 sans-serif;opacity:.9">RDP report: loading...</div></body>',
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
path.write_text(content)
|
||||||
|
PY
|
||||||
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
|
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
|
||||||
|
|
||||||
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
||||||
|
|||||||
@@ -67,6 +67,126 @@ def report_today():
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def render_html(rows):
|
||||||
|
generated = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||||
|
date_local = datetime.now(REPORT_TZ).strftime("%Y-%m-%d")
|
||||||
|
trs = []
|
||||||
|
for row in rows:
|
||||||
|
trs.append(
|
||||||
|
"<tr>"
|
||||||
|
f"<td>{row['user']}</td>"
|
||||||
|
f"<td>{row['active_hhmm']}</td>"
|
||||||
|
f"<td>{row['active_seconds']}</td>"
|
||||||
|
f"<td>{row['first_activity']}</td>"
|
||||||
|
f"<td>{row['last_activity']}</td>"
|
||||||
|
f"<td>{row['idle_seconds']}</td>"
|
||||||
|
f"<td>{row['sessions_count']}</td>"
|
||||||
|
"</tr>"
|
||||||
|
)
|
||||||
|
if not trs:
|
||||||
|
trs.append('<tr><td colspan="7">No data for today yet.</td></tr>')
|
||||||
|
return f"""<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>AW-rus Worktime</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f4f7fb;
|
||||||
|
--card: #ffffff;
|
||||||
|
--line: #dbe3ee;
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #475569;
|
||||||
|
--accent: #0f766e;
|
||||||
|
--accent-2: #1d4ed8;
|
||||||
|
}}
|
||||||
|
* {{ box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
font: 14px/1.45 "Segoe UI", "Noto Sans", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(29,78,216,.08), transparent 28%),
|
||||||
|
radial-gradient(circle at top right, rgba(15,118,110,.10), transparent 24%),
|
||||||
|
var(--bg);
|
||||||
|
}}
|
||||||
|
.wrap {{ max-width: 1180px; margin: 0 auto; padding: 24px; }}
|
||||||
|
.hero {{
|
||||||
|
background: linear-gradient(135deg, #0f172a, #1e293b 58%, #0f766e);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
box-shadow: 0 22px 60px rgba(15,23,42,.22);
|
||||||
|
}}
|
||||||
|
.hero h1 {{ margin: 0 0 8px; font-size: 28px; }}
|
||||||
|
.meta {{ color: rgba(255,255,255,.84); }}
|
||||||
|
.actions {{ margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap; }}
|
||||||
|
.actions a {{
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255,255,255,.12);
|
||||||
|
border: 1px solid rgba(255,255,255,.18);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}}
|
||||||
|
.card {{
|
||||||
|
margin-top: 18px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 16px 40px rgba(15,23,42,.08);
|
||||||
|
}}
|
||||||
|
table {{ width: 100%; border-collapse: collapse; }}
|
||||||
|
th, td {{ padding: 12px 14px; border-bottom: 1px solid var(--line); text-align: left; }}
|
||||||
|
th {{ background: #eef4fb; color: var(--muted); font-weight: 600; position: sticky; top: 0; }}
|
||||||
|
tr:nth-child(even) td {{ background: rgba(148,163,184,.06); }}
|
||||||
|
.num {{ font-variant-numeric: tabular-nums; }}
|
||||||
|
.good {{ color: var(--accent); font-weight: 700; }}
|
||||||
|
.muted {{ color: var(--muted); }}
|
||||||
|
@media (max-width: 900px) {{
|
||||||
|
.wrap {{ padding: 14px; }}
|
||||||
|
.hero h1 {{ font-size: 22px; }}
|
||||||
|
.card {{ overflow-x: auto; }}
|
||||||
|
table {{ min-width: 820px; }}
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<section class="hero">
|
||||||
|
<h1>RDP Worktime Report</h1>
|
||||||
|
<div class="meta">Date: {date_local} · Timezone: {REPORT_TZ} · Generated UTC: {generated}</div>
|
||||||
|
<div class="actions">
|
||||||
|
<a href="/reports/worktime/today?format=csv">Download CSV</a>
|
||||||
|
<a href="/reports/worktime/today">View JSON</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="card">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Active</th>
|
||||||
|
<th>Active sec</th>
|
||||||
|
<th>First activity</th>
|
||||||
|
<th>Last activity</th>
|
||||||
|
<th>Idle sec</th>
|
||||||
|
<th>Samples</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{''.join(trs)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
class H(BaseHTTPRequestHandler):
|
class H(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if not self.path.startswith("/reports/worktime/today"):
|
if not self.path.startswith("/reports/worktime/today"):
|
||||||
@@ -76,6 +196,8 @@ class H(BaseHTTPRequestHandler):
|
|||||||
fmt = "json"
|
fmt = "json"
|
||||||
if "format=csv" in self.path:
|
if "format=csv" in self.path:
|
||||||
fmt = "csv"
|
fmt = "csv"
|
||||||
|
elif "format=html" in self.path:
|
||||||
|
fmt = "html"
|
||||||
rows = report_today()
|
rows = report_today()
|
||||||
if fmt == "csv":
|
if fmt == "csv":
|
||||||
out = io.StringIO()
|
out = io.StringIO()
|
||||||
@@ -100,6 +222,14 @@ class H(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
return
|
return
|
||||||
|
if fmt == "html":
|
||||||
|
data = render_html(rows).encode("utf-8")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(data)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
return
|
||||||
obj = {
|
obj = {
|
||||||
"generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
"generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||||
"report_timezone": str(REPORT_TZ),
|
"report_timezone": str(REPORT_TZ),
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
(function () {
|
||||||
|
var reportBase = "__AW_WORKTIME_REPORT_BASE__";
|
||||||
|
var reportUrl = reportBase + "/reports/worktime/today?format=html";
|
||||||
|
var existing = document.getElementById("aw-report-links");
|
||||||
|
if (!existing) return;
|
||||||
|
|
||||||
|
existing.innerHTML =
|
||||||
|
'RDP report: ' +
|
||||||
|
'<a href="' + reportUrl + '" style="color:#fcd34d" target="_blank">HTML</a> | ' +
|
||||||
|
'<a href="' + reportBase + '/reports/worktime/today?format=csv" style="color:#7dd3fc" target="_blank">CSV</a> | ' +
|
||||||
|
'<a href="' + reportBase + '/reports/worktime/today" style="color:#86efac" target="_blank">JSON</a> | ' +
|
||||||
|
'<a href="#" id="aw-report-toggle" style="color:#f9fafb">Panel</a>';
|
||||||
|
|
||||||
|
var panel = document.createElement("div");
|
||||||
|
panel.id = "aw-report-panel";
|
||||||
|
panel.style.cssText = [
|
||||||
|
"position:fixed",
|
||||||
|
"top:16px",
|
||||||
|
"right:16px",
|
||||||
|
"width:min(980px,calc(100vw - 32px))",
|
||||||
|
"height:min(760px,calc(100vh - 32px))",
|
||||||
|
"background:#fff",
|
||||||
|
"border:1px solid rgba(15,23,42,.15)",
|
||||||
|
"border-radius:12px",
|
||||||
|
"box-shadow:0 24px 80px rgba(15,23,42,.28)",
|
||||||
|
"overflow:hidden",
|
||||||
|
"z-index:100000",
|
||||||
|
"display:none"
|
||||||
|
].join(";");
|
||||||
|
|
||||||
|
panel.innerHTML =
|
||||||
|
'<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#0f172a;color:#fff;font:600 13px/1.2 sans-serif">' +
|
||||||
|
'<div>RDP Worktime Report</div>' +
|
||||||
|
'<div style="display:flex;gap:12px;align-items:center">' +
|
||||||
|
'<a href="' + reportUrl + '" target="_blank" style="color:#93c5fd;text-decoration:none">Open</a>' +
|
||||||
|
'<a href="#" id="aw-report-close" style="color:#fff;text-decoration:none">Close</a>' +
|
||||||
|
"</div></div>" +
|
||||||
|
'<iframe src="' + reportUrl + '" title="RDP Worktime Report" style="border:0;width:100%;height:calc(100% - 42px);background:#fff"></iframe>';
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
function openPanel(ev) {
|
||||||
|
if (ev) ev.preventDefault();
|
||||||
|
panel.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePanel(ev) {
|
||||||
|
if (ev) ev.preventDefault();
|
||||||
|
panel.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
var toggle = document.getElementById("aw-report-toggle");
|
||||||
|
if (toggle) toggle.addEventListener("click", openPanel);
|
||||||
|
var close = panel.querySelector("#aw-report-close");
|
||||||
|
if (close) close.addEventListener("click", closePanel);
|
||||||
|
})();
|
||||||
@@ -194,6 +194,7 @@
|
|||||||
loop:
|
loop:
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
|
||||||
|
|
||||||
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
loop:
|
loop:
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
||||||
|
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" }
|
||||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
||||||
|
|
||||||
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
- aw-sw-cleanup.js
|
- aw-sw-cleanup.js
|
||||||
|
|||||||
+1
@@ -11,6 +11,7 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
- aw-sw-cleanup.js
|
- aw-sw-cleanup.js
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}"
|
|||||||
REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://10.10.10.13:5610}"
|
REPORT_BASE="${AW_WORKTIME_REPORT_BASE:-http://10.10.10.13:5610}"
|
||||||
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
||||||
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js"
|
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js"
|
||||||
|
WORKTIME_PANEL_SRC="/root/bootstrap/aw-worktime-panel.js"
|
||||||
HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json"
|
HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json"
|
||||||
INDEX_HTML="$WEBUI_DIR/index.html"
|
INDEX_HTML="$WEBUI_DIR/index.html"
|
||||||
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
|
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
|
||||||
TS=$(date +%Y%m%d%H%M%S)
|
TS=$(date +%Y%m%d%H%M%S)
|
||||||
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
|
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
|
||||||
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
|
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
|
||||||
|
WORKTIME_PANEL_TARGET="$WEBUI_DIR/js/aw-worktime-panel.js"
|
||||||
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
|
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
|
||||||
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
|
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
|
||||||
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
|
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
|
||||||
@@ -29,30 +31,76 @@ CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){retu
|
|||||||
|
|
||||||
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
|
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
|
||||||
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
|
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
|
||||||
|
[[ -f "$WORKTIME_PANEL_SRC" ]] || { echo "missing $WORKTIME_PANEL_SRC" >&2; exit 1; }
|
||||||
[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; }
|
[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; }
|
||||||
[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; }
|
[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; }
|
||||||
|
|
||||||
install -d "$WEBUI_DIR/js"
|
install -d "$WEBUI_DIR/js"
|
||||||
install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET"
|
install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET"
|
||||||
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
|
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
|
||||||
|
install -m 0644 "$WORKTIME_PANEL_SRC" "$WORKTIME_PANEL_TARGET"
|
||||||
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
|
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
|
||||||
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
|
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
|
||||||
|
|
||||||
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
|
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
|
||||||
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
|
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
|
||||||
|
worktime_panel_hash="$(sha1sum "$WORKTIME_PANEL_TARGET" | awk '{print substr($1,1,12)}')"
|
||||||
|
|
||||||
sed -i '/ru-patch-v5.js/d;/sw-cleanup.js/d;/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
|
python3 - "$WORKTIME_PANEL_TARGET" "$REPORT_BASE" <<'PY'
|
||||||
sed -i "s#</head>#<script src=\"/js/sw-cleanup.js?v=$sw_hash\"></script></head>#" "$INDEX_HTML"
|
from pathlib import Path
|
||||||
sed -i "s#</body>#<script defer=\"defer\" src=\"/js/ru-patch-v5.js?v=$patch_hash\"></script></body>#" "$INDEX_HTML"
|
import sys
|
||||||
|
|
||||||
sed -i "s#href=\"/today.csv\"#href=\"$REPORT_BASE/reports/worktime/today?format=csv\"#g" "$INDEX_HTML"
|
path = Path(sys.argv[1])
|
||||||
sed -i "s#href=\"/today.json\"#href=\"$REPORT_BASE/reports/worktime/today\"#g" "$INDEX_HTML"
|
report_base = sys.argv[2]
|
||||||
|
text = path.read_text()
|
||||||
|
text = text.replace("__AW_WORKTIME_REPORT_BASE__", report_base)
|
||||||
|
path.write_text(text)
|
||||||
|
PY
|
||||||
|
|
||||||
if ! grep -q 'id="aw-report-links"' "$INDEX_HTML"; then
|
python3 - "$INDEX_HTML" "$sw_hash" "$patch_hash" "$worktime_panel_hash" "$REPORT_BASE" <<'PY'
|
||||||
sed -i "s#</body>#<div id=\"aw-report-links\" style=\"position:fixed;right:12px;bottom:12px;z-index:99999;background:#111;color:#fff;padding:8px 10px;border-radius:8px;font:12px/1.4 sans-serif;opacity:.9\">RDP report: <a href=\"$REPORT_BASE/reports/worktime/today?format=csv\" style=\"color:#7dd3fc\" target=\"_blank\">CSV</a> | <a href=\"$REPORT_BASE/reports/worktime/today\" style=\"color:#86efac\" target=\"_blank\">JSON</a></div></body>#g" "$INDEX_HTML"
|
from pathlib import Path
|
||||||
fi
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
sed -i "s#<div id=\"aw-report-links\"[^<]*RDP report: <a href=\"[^\"]*\" style=\"color:#7dd3fc\" target=\"_blank\">CSV</a> | <a href=\"[^\"]*\" style=\"color:#86efac\" target=\"_blank\">JSON</a></div>#<div id=\"aw-report-links\" style=\"position:fixed;right:12px;bottom:12px;z-index:99999;background:#111;color:#fff;padding:8px 10px;border-radius:8px;font:12px/1.4 sans-serif;opacity:.9\">RDP report: <a href=\"$REPORT_BASE/reports/worktime/today?format=csv\" style=\"color:#7dd3fc\" target=\"_blank\">CSV</a> | <a href=\"$REPORT_BASE/reports/worktime/today\" style=\"color:#86efac\" target=\"_blank\">JSON</a></div>#g" "$INDEX_HTML"
|
path = Path(sys.argv[1])
|
||||||
|
sw_hash = sys.argv[2]
|
||||||
|
patch_hash = sys.argv[3]
|
||||||
|
panel_hash = sys.argv[4]
|
||||||
|
report_base = sys.argv[5]
|
||||||
|
content = path.read_text()
|
||||||
|
|
||||||
|
content = re.sub(
|
||||||
|
r'<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js|aw-worktime-panel\.js)[^>]*></script>',
|
||||||
|
'',
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
content = re.sub(r"; frame-src 'self' [^\";>]*", "", content)
|
||||||
|
content = content.replace(
|
||||||
|
"script-src 'self' 'unsafe-eval'",
|
||||||
|
f"script-src 'self' 'unsafe-eval'; frame-src 'self' {report_base}",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
content = content.replace(
|
||||||
|
"</head>",
|
||||||
|
f'<script src="/js/sw-cleanup.js?v={sw_hash}"></script></head>',
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
content = content.replace(
|
||||||
|
"</body>",
|
||||||
|
(
|
||||||
|
f'<script defer="defer" src="/js/ru-patch-v5.js?v={patch_hash}"></script>'
|
||||||
|
f'<script defer="defer" src="/js/aw-worktime-panel.js?v={panel_hash}"></script></body>'
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
if 'id="aw-report-links"' not in content:
|
||||||
|
content = content.replace(
|
||||||
|
"</body>",
|
||||||
|
'<div id="aw-report-links" style="position:fixed;right:12px;bottom:12px;z-index:99999;background:#111;color:#fff;padding:8px 10px;border-radius:8px;font:12px/1.4 sans-serif;opacity:.9">RDP report: loading...</div></body>',
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
path.write_text(content)
|
||||||
|
PY
|
||||||
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
|
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
|
||||||
|
|
||||||
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
|
||||||
|
|||||||
@@ -67,6 +67,126 @@ def report_today():
|
|||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def render_html(rows):
|
||||||
|
generated = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||||
|
date_local = datetime.now(REPORT_TZ).strftime("%Y-%m-%d")
|
||||||
|
trs = []
|
||||||
|
for row in rows:
|
||||||
|
trs.append(
|
||||||
|
"<tr>"
|
||||||
|
f"<td>{row['user']}</td>"
|
||||||
|
f"<td>{row['active_hhmm']}</td>"
|
||||||
|
f"<td>{row['active_seconds']}</td>"
|
||||||
|
f"<td>{row['first_activity']}</td>"
|
||||||
|
f"<td>{row['last_activity']}</td>"
|
||||||
|
f"<td>{row['idle_seconds']}</td>"
|
||||||
|
f"<td>{row['sessions_count']}</td>"
|
||||||
|
"</tr>"
|
||||||
|
)
|
||||||
|
if not trs:
|
||||||
|
trs.append('<tr><td colspan="7">No data for today yet.</td></tr>')
|
||||||
|
return f"""<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>AW-rus Worktime</title>
|
||||||
|
<style>
|
||||||
|
:root {{
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f4f7fb;
|
||||||
|
--card: #ffffff;
|
||||||
|
--line: #dbe3ee;
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #475569;
|
||||||
|
--accent: #0f766e;
|
||||||
|
--accent-2: #1d4ed8;
|
||||||
|
}}
|
||||||
|
* {{ box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
margin: 0;
|
||||||
|
font: 14px/1.45 "Segoe UI", "Noto Sans", sans-serif;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(29,78,216,.08), transparent 28%),
|
||||||
|
radial-gradient(circle at top right, rgba(15,118,110,.10), transparent 24%),
|
||||||
|
var(--bg);
|
||||||
|
}}
|
||||||
|
.wrap {{ max-width: 1180px; margin: 0 auto; padding: 24px; }}
|
||||||
|
.hero {{
|
||||||
|
background: linear-gradient(135deg, #0f172a, #1e293b 58%, #0f766e);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
box-shadow: 0 22px 60px rgba(15,23,42,.22);
|
||||||
|
}}
|
||||||
|
.hero h1 {{ margin: 0 0 8px; font-size: 28px; }}
|
||||||
|
.meta {{ color: rgba(255,255,255,.84); }}
|
||||||
|
.actions {{ margin-top: 14px; display: flex; gap: 10px; flex-wrap: wrap; }}
|
||||||
|
.actions a {{
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(255,255,255,.12);
|
||||||
|
border: 1px solid rgba(255,255,255,.18);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}}
|
||||||
|
.card {{
|
||||||
|
margin-top: 18px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 16px 40px rgba(15,23,42,.08);
|
||||||
|
}}
|
||||||
|
table {{ width: 100%; border-collapse: collapse; }}
|
||||||
|
th, td {{ padding: 12px 14px; border-bottom: 1px solid var(--line); text-align: left; }}
|
||||||
|
th {{ background: #eef4fb; color: var(--muted); font-weight: 600; position: sticky; top: 0; }}
|
||||||
|
tr:nth-child(even) td {{ background: rgba(148,163,184,.06); }}
|
||||||
|
.num {{ font-variant-numeric: tabular-nums; }}
|
||||||
|
.good {{ color: var(--accent); font-weight: 700; }}
|
||||||
|
.muted {{ color: var(--muted); }}
|
||||||
|
@media (max-width: 900px) {{
|
||||||
|
.wrap {{ padding: 14px; }}
|
||||||
|
.hero h1 {{ font-size: 22px; }}
|
||||||
|
.card {{ overflow-x: auto; }}
|
||||||
|
table {{ min-width: 820px; }}
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<section class="hero">
|
||||||
|
<h1>RDP Worktime Report</h1>
|
||||||
|
<div class="meta">Date: {date_local} · Timezone: {REPORT_TZ} · Generated UTC: {generated}</div>
|
||||||
|
<div class="actions">
|
||||||
|
<a href="/reports/worktime/today?format=csv">Download CSV</a>
|
||||||
|
<a href="/reports/worktime/today">View JSON</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="card">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Active</th>
|
||||||
|
<th>Active sec</th>
|
||||||
|
<th>First activity</th>
|
||||||
|
<th>Last activity</th>
|
||||||
|
<th>Idle sec</th>
|
||||||
|
<th>Samples</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{''.join(trs)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
class H(BaseHTTPRequestHandler):
|
class H(BaseHTTPRequestHandler):
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if not self.path.startswith("/reports/worktime/today"):
|
if not self.path.startswith("/reports/worktime/today"):
|
||||||
@@ -76,6 +196,8 @@ class H(BaseHTTPRequestHandler):
|
|||||||
fmt = "json"
|
fmt = "json"
|
||||||
if "format=csv" in self.path:
|
if "format=csv" in self.path:
|
||||||
fmt = "csv"
|
fmt = "csv"
|
||||||
|
elif "format=html" in self.path:
|
||||||
|
fmt = "html"
|
||||||
rows = report_today()
|
rows = report_today()
|
||||||
if fmt == "csv":
|
if fmt == "csv":
|
||||||
out = io.StringIO()
|
out = io.StringIO()
|
||||||
@@ -100,6 +222,14 @@ class H(BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
return
|
return
|
||||||
|
if fmt == "html":
|
||||||
|
data = render_html(rows).encode("utf-8")
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(data)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(data)
|
||||||
|
return
|
||||||
obj = {
|
obj = {
|
||||||
"generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
"generated_at_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||||
"report_timezone": str(REPORT_TZ),
|
"report_timezone": str(REPORT_TZ),
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
(function () {
|
||||||
|
var reportBase = "__AW_WORKTIME_REPORT_BASE__";
|
||||||
|
var reportUrl = reportBase + "/reports/worktime/today?format=html";
|
||||||
|
var existing = document.getElementById("aw-report-links");
|
||||||
|
if (!existing) return;
|
||||||
|
|
||||||
|
existing.innerHTML =
|
||||||
|
'RDP report: ' +
|
||||||
|
'<a href="' + reportUrl + '" style="color:#fcd34d" target="_blank">HTML</a> | ' +
|
||||||
|
'<a href="' + reportBase + '/reports/worktime/today?format=csv" style="color:#7dd3fc" target="_blank">CSV</a> | ' +
|
||||||
|
'<a href="' + reportBase + '/reports/worktime/today" style="color:#86efac" target="_blank">JSON</a> | ' +
|
||||||
|
'<a href="#" id="aw-report-toggle" style="color:#f9fafb">Panel</a>';
|
||||||
|
|
||||||
|
var panel = document.createElement("div");
|
||||||
|
panel.id = "aw-report-panel";
|
||||||
|
panel.style.cssText = [
|
||||||
|
"position:fixed",
|
||||||
|
"top:16px",
|
||||||
|
"right:16px",
|
||||||
|
"width:min(980px,calc(100vw - 32px))",
|
||||||
|
"height:min(760px,calc(100vh - 32px))",
|
||||||
|
"background:#fff",
|
||||||
|
"border:1px solid rgba(15,23,42,.15)",
|
||||||
|
"border-radius:12px",
|
||||||
|
"box-shadow:0 24px 80px rgba(15,23,42,.28)",
|
||||||
|
"overflow:hidden",
|
||||||
|
"z-index:100000",
|
||||||
|
"display:none"
|
||||||
|
].join(";");
|
||||||
|
|
||||||
|
panel.innerHTML =
|
||||||
|
'<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#0f172a;color:#fff;font:600 13px/1.2 sans-serif">' +
|
||||||
|
'<div>RDP Worktime Report</div>' +
|
||||||
|
'<div style="display:flex;gap:12px;align-items:center">' +
|
||||||
|
'<a href="' + reportUrl + '" target="_blank" style="color:#93c5fd;text-decoration:none">Open</a>' +
|
||||||
|
'<a href="#" id="aw-report-close" style="color:#fff;text-decoration:none">Close</a>' +
|
||||||
|
"</div></div>" +
|
||||||
|
'<iframe src="' + reportUrl + '" title="RDP Worktime Report" style="border:0;width:100%;height:calc(100% - 42px);background:#fff"></iframe>';
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
|
||||||
|
function openPanel(ev) {
|
||||||
|
if (ev) ev.preventDefault();
|
||||||
|
panel.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePanel(ev) {
|
||||||
|
if (ev) ev.preventDefault();
|
||||||
|
panel.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
var toggle = document.getElementById("aw-report-toggle");
|
||||||
|
if (toggle) toggle.addEventListener("click", openPanel);
|
||||||
|
var close = panel.querySelector("#aw-report-close");
|
||||||
|
if (close) close.addEventListener("click", closePanel);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user