estela 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -0
- package/dist/billing/amortize.d.ts +44 -0
- package/dist/billing/amortize.d.ts.map +1 -0
- package/dist/billing/amortize.js +83 -0
- package/dist/billing/amortize.js.map +1 -0
- package/dist/billing/amortize.test.d.ts +2 -0
- package/dist/billing/amortize.test.d.ts.map +1 -0
- package/dist/billing/amortize.test.js +121 -0
- package/dist/billing/amortize.test.js.map +1 -0
- package/dist/billing/format.test.d.ts +2 -0
- package/dist/billing/format.test.d.ts.map +1 -0
- package/dist/billing/format.test.js +110 -0
- package/dist/billing/format.test.js.map +1 -0
- package/dist/billing/invoice.d.ts +52 -0
- package/dist/billing/invoice.d.ts.map +1 -0
- package/dist/billing/invoice.js +148 -0
- package/dist/billing/invoice.js.map +1 -0
- package/dist/billing/invoice.test.d.ts +2 -0
- package/dist/billing/invoice.test.d.ts.map +1 -0
- package/dist/billing/invoice.test.js +200 -0
- package/dist/billing/invoice.test.js.map +1 -0
- package/dist/billing/sessionize.d.ts +104 -0
- package/dist/billing/sessionize.d.ts.map +1 -0
- package/dist/billing/sessionize.js +0 -0
- package/dist/billing/sessionize.js.map +1 -0
- package/dist/billing/sessionize.test.d.ts +2 -0
- package/dist/billing/sessionize.test.d.ts.map +1 -0
- package/dist/billing/sessionize.test.js +67 -0
- package/dist/billing/sessionize.test.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1091 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud/auth.d.ts +4 -0
- package/dist/cloud/auth.d.ts.map +1 -0
- package/dist/cloud/auth.js +98 -0
- package/dist/cloud/auth.js.map +1 -0
- package/dist/cloud/client.d.ts +12 -0
- package/dist/cloud/client.d.ts.map +1 -0
- package/dist/cloud/client.js +34 -0
- package/dist/cloud/client.js.map +1 -0
- package/dist/db/schema.d.ts +11 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +424 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/schema.test.d.ts +2 -0
- package/dist/db/schema.test.d.ts.map +1 -0
- package/dist/db/schema.test.js +335 -0
- package/dist/db/schema.test.js.map +1 -0
- package/dist/db/store.d.ts +113 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/db/store.js +550 -0
- package/dist/db/store.js.map +1 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +229 -0
- package/dist/doctor.js.map +1 -0
- package/dist/export/csv.d.ts +9 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/csv.js +81 -0
- package/dist/export/csv.js.map +1 -0
- package/dist/export/invoice-pdf.d.ts +21 -0
- package/dist/export/invoice-pdf.d.ts.map +1 -0
- package/dist/export/invoice-pdf.js +135 -0
- package/dist/export/invoice-pdf.js.map +1 -0
- package/dist/export/panel.d.ts +78 -0
- package/dist/export/panel.d.ts.map +1 -0
- package/dist/export/panel.js +693 -0
- package/dist/export/panel.js.map +1 -0
- package/dist/export/panel.test.d.ts +2 -0
- package/dist/export/panel.test.d.ts.map +1 -0
- package/dist/export/panel.test.js +273 -0
- package/dist/export/panel.test.js.map +1 -0
- package/dist/export/pdf.d.ts +32 -0
- package/dist/export/pdf.d.ts.map +1 -0
- package/dist/export/pdf.js +229 -0
- package/dist/export/pdf.js.map +1 -0
- package/dist/export/pdf.test.d.ts +2 -0
- package/dist/export/pdf.test.d.ts.map +1 -0
- package/dist/export/pdf.test.js +151 -0
- package/dist/export/pdf.test.js.map +1 -0
- package/dist/export/share.d.ts +42 -0
- package/dist/export/share.d.ts.map +1 -0
- package/dist/export/share.js +181 -0
- package/dist/export/share.js.map +1 -0
- package/dist/export/share.test.d.ts +2 -0
- package/dist/export/share.test.d.ts.map +1 -0
- package/dist/export/share.test.js +116 -0
- package/dist/export/share.test.js.map +1 -0
- package/dist/metrics/index.d.ts +101 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +221 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/metrics.test.d.ts +2 -0
- package/dist/metrics/metrics.test.d.ts.map +1 -0
- package/dist/metrics/metrics.test.js +162 -0
- package/dist/metrics/metrics.test.js.map +1 -0
- package/dist/metrics/people.d.ts +46 -0
- package/dist/metrics/people.d.ts.map +1 -0
- package/dist/metrics/people.js +126 -0
- package/dist/metrics/people.js.map +1 -0
- package/dist/metrics/people.test.d.ts +2 -0
- package/dist/metrics/people.test.d.ts.map +1 -0
- package/dist/metrics/people.test.js +84 -0
- package/dist/metrics/people.test.js.map +1 -0
- package/dist/metrics/team.d.ts +41 -0
- package/dist/metrics/team.d.ts.map +1 -0
- package/dist/metrics/team.js +0 -0
- package/dist/metrics/team.js.map +1 -0
- package/dist/metrics/team.test.d.ts +2 -0
- package/dist/metrics/team.test.d.ts.map +1 -0
- package/dist/metrics/team.test.js +90 -0
- package/dist/metrics/team.test.js.map +1 -0
- package/dist/pricing/catalog.d.ts +39 -0
- package/dist/pricing/catalog.d.ts.map +1 -0
- package/dist/pricing/catalog.js +59 -0
- package/dist/pricing/catalog.js.map +1 -0
- package/dist/pricing/cost.d.ts +25 -0
- package/dist/pricing/cost.d.ts.map +1 -0
- package/dist/pricing/cost.js +50 -0
- package/dist/pricing/cost.js.map +1 -0
- package/dist/pricing/cost.test.d.ts +2 -0
- package/dist/pricing/cost.test.d.ts.map +1 -0
- package/dist/pricing/cost.test.js +74 -0
- package/dist/pricing/cost.test.js.map +1 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1141 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +70 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +223 -0
- package/dist/setup.js.map +1 -0
- package/dist/setup.test.d.ts +2 -0
- package/dist/setup.test.d.ts.map +1 -0
- package/dist/setup.test.js +131 -0
- package/dist/setup.test.js.map +1 -0
- package/dist/watchers/claude.d.ts +34 -0
- package/dist/watchers/claude.d.ts.map +1 -0
- package/dist/watchers/claude.js +235 -0
- package/dist/watchers/claude.js.map +1 -0
- package/dist/watchers/claude.test.d.ts +2 -0
- package/dist/watchers/claude.test.d.ts.map +1 -0
- package/dist/watchers/claude.test.js +180 -0
- package/dist/watchers/claude.test.js.map +1 -0
- package/dist/watchers/git.d.ts +42 -0
- package/dist/watchers/git.d.ts.map +1 -0
- package/dist/watchers/git.js +218 -0
- package/dist/watchers/git.js.map +1 -0
- package/dist/watchers/git.test.d.ts +2 -0
- package/dist/watchers/git.test.d.ts.map +1 -0
- package/dist/watchers/git.test.js +73 -0
- package/dist/watchers/git.test.js.map +1 -0
- package/dist/watchers/identity.d.ts +34 -0
- package/dist/watchers/identity.d.ts.map +1 -0
- package/dist/watchers/identity.js +85 -0
- package/dist/watchers/identity.js.map +1 -0
- package/dist/watchers/identity.test.d.ts +2 -0
- package/dist/watchers/identity.test.d.ts.map +1 -0
- package/dist/watchers/identity.test.js +48 -0
- package/dist/watchers/identity.test.js.map +1 -0
- package/dist/web-ui.test.d.ts +2 -0
- package/dist/web-ui.test.d.ts.map +1 -0
- package/dist/web-ui.test.js +312 -0
- package/dist/web-ui.test.js.map +1 -0
- package/node_modules/@estela/shared/dist/index.d.ts +3 -0
- package/node_modules/@estela/shared/dist/index.d.ts.map +1 -0
- package/node_modules/@estela/shared/dist/index.js +19 -0
- package/node_modules/@estela/shared/dist/index.js.map +1 -0
- package/node_modules/@estela/shared/dist/money.d.ts +71 -0
- package/node_modules/@estela/shared/dist/money.d.ts.map +1 -0
- package/node_modules/@estela/shared/dist/money.js +171 -0
- package/node_modules/@estela/shared/dist/money.js.map +1 -0
- package/node_modules/@estela/shared/dist/types.d.ts +365 -0
- package/node_modules/@estela/shared/dist/types.d.ts.map +1 -0
- package/node_modules/@estela/shared/dist/types.js +24 -0
- package/node_modules/@estela/shared/dist/types.js.map +1 -0
- package/node_modules/@estela/shared/package.json +5 -0
- package/package.json +41 -0
- package/web/app.css +749 -0
- package/web/app.js +1257 -0
- package/web/apple-touch-icon.png +0 -0
- package/web/favicon.png +0 -0
- package/web/index.html +132 -0
- package/web/lib.js +94 -0
- package/web/logo-mark.png +0 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.newPanelToken = newPanelToken;
|
|
4
|
+
exports.buildPanel = buildPanel;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const shared_1 = require("@estela/shared");
|
|
7
|
+
const index_js_1 = require("../metrics/index.js");
|
|
8
|
+
/**
|
|
9
|
+
* Token de acceso.
|
|
10
|
+
*
|
|
11
|
+
* 128 bits en la ruta: una URL publicada es pública para quien la tenga, así
|
|
12
|
+
* que lo único que la protege es que nadie pueda adivinarla. Revocar el acceso
|
|
13
|
+
* es borrar el fichero del servidor — no hay caducidad falsa en el cliente,
|
|
14
|
+
* porque una caducidad que comprueba el navegador no protege de nada.
|
|
15
|
+
*/
|
|
16
|
+
function newPanelToken() {
|
|
17
|
+
return (0, node_crypto_1.randomBytes)(16).toString("hex");
|
|
18
|
+
}
|
|
19
|
+
/** Las dos últimas partes de la ruta: el fichero se reconoce, el árbol estorba. */
|
|
20
|
+
function shortPath(file) {
|
|
21
|
+
return file.split("/").slice(-2).join("/");
|
|
22
|
+
}
|
|
23
|
+
function buildPanel(options) {
|
|
24
|
+
const { project, client, entries } = options;
|
|
25
|
+
const showAmounts = options.withAmounts === true && options.rateAt !== undefined;
|
|
26
|
+
const included = entries
|
|
27
|
+
.filter((e) => e.billable)
|
|
28
|
+
.sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
|
|
29
|
+
const byDay = new Map();
|
|
30
|
+
let totalSeconds = 0;
|
|
31
|
+
let totalMinor = 0;
|
|
32
|
+
for (const entry of included) {
|
|
33
|
+
const date = (0, shared_1.localDate)(entry.startedAt);
|
|
34
|
+
let day = byDay.get(date);
|
|
35
|
+
if (!day) {
|
|
36
|
+
day = { date, seconds: 0, items: [] };
|
|
37
|
+
byDay.set(date, day);
|
|
38
|
+
}
|
|
39
|
+
let amount = null;
|
|
40
|
+
if (showAmounts) {
|
|
41
|
+
const rate = options.rateAt(entry.startedAt);
|
|
42
|
+
if (rate) {
|
|
43
|
+
const minor = Math.round((rate.amount * entry.seconds) / 3600);
|
|
44
|
+
totalMinor += minor;
|
|
45
|
+
amount = (0, shared_1.formatMoney)({ amount: minor, currency: rate.currency });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
day.seconds += entry.seconds;
|
|
49
|
+
totalSeconds += entry.seconds;
|
|
50
|
+
day.items.push({
|
|
51
|
+
what: entry.description,
|
|
52
|
+
seconds: entry.seconds,
|
|
53
|
+
kind: entry.kind === "development" ? "" : (shared_1.WORK_KIND_LABELS[entry.kind] ?? ""),
|
|
54
|
+
amount,
|
|
55
|
+
commits: [...(options.commitsOf?.(entry.commitHashes) ?? [])]
|
|
56
|
+
.map((c) => ({ hash: c.hash.slice(0, 7), subject: c.subject })),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const days = [...byDay.values()].sort((a, b) => a.date.localeCompare(b.date));
|
|
60
|
+
// Todo el estado viaja embebido. La página no hace una sola petición.
|
|
61
|
+
// Las métricas se calculan sobre lo mismo que se enseña, para que un número
|
|
62
|
+
// de arriba y el detalle de abajo no puedan contradecirse.
|
|
63
|
+
const commits = options.commits ?? [];
|
|
64
|
+
const merged = options.merged ?? null;
|
|
65
|
+
const features = (0, index_js_1.effortByFeature)(included, commits, merged).map((f) => ({
|
|
66
|
+
name: f.name,
|
|
67
|
+
seconds: f.seconds,
|
|
68
|
+
days: f.days,
|
|
69
|
+
merged: f.merged,
|
|
70
|
+
delivered: f.delivered.slice(0, 8),
|
|
71
|
+
moreDelivered: Math.max(0, f.delivered.length - 8),
|
|
72
|
+
}));
|
|
73
|
+
const rhythm = (0, index_js_1.cadence)(included);
|
|
74
|
+
const rework = (0, index_js_1.churn)(commits, { minTouches: 3, withinDays: 14 }).slice(0, 6)
|
|
75
|
+
.map((f) => ({ file: shortPath(f.file), touches: f.touches, spanDays: f.spanDays }));
|
|
76
|
+
const open = merged
|
|
77
|
+
? (0, index_js_1.openWork)(commits, merged, options.generatedAt ?? new Date()).slice(0, 6)
|
|
78
|
+
: [];
|
|
79
|
+
const data = {
|
|
80
|
+
project: project.name,
|
|
81
|
+
client: client.name,
|
|
82
|
+
author: options.authorName ?? null,
|
|
83
|
+
generatedAt: (options.generatedAt ?? new Date()).toISOString(),
|
|
84
|
+
totalSeconds,
|
|
85
|
+
totalAmount: showAmounts && totalMinor > 0
|
|
86
|
+
? (0, shared_1.formatMoney)({ amount: totalMinor, currency: client.currency }) : null,
|
|
87
|
+
days,
|
|
88
|
+
features,
|
|
89
|
+
rhythm: {
|
|
90
|
+
activeDays: rhythm.activeDays,
|
|
91
|
+
spanDays: rhythm.spanDays,
|
|
92
|
+
medianSeconds: rhythm.medianSecondsPerActiveDay,
|
|
93
|
+
longestGapDays: rhythm.longestGapDays,
|
|
94
|
+
},
|
|
95
|
+
rework,
|
|
96
|
+
open,
|
|
97
|
+
team: options.team ?? [],
|
|
98
|
+
site: (options.siteUrl ?? "https://getestela.dev").replace(/\/$/, ""),
|
|
99
|
+
};
|
|
100
|
+
return render(data);
|
|
101
|
+
}
|
|
102
|
+
function render(data) {
|
|
103
|
+
// JSON dentro de <script>: hay que romper cualquier "</script>" literal que
|
|
104
|
+
// pudiera venir en un mensaje de commit, o cerraría la etiqueta.
|
|
105
|
+
const payload = JSON.stringify(data)
|
|
106
|
+
.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
107
|
+
return `<!doctype html>
|
|
108
|
+
<html lang="es">
|
|
109
|
+
<head>
|
|
110
|
+
<meta charset="utf-8">
|
|
111
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
112
|
+
<!-- Publicado con enlace no listado: que ningún buscador lo indexe. -->
|
|
113
|
+
<meta name="robots" content="noindex, nofollow, noarchive">
|
|
114
|
+
<meta name="referrer" content="no-referrer">
|
|
115
|
+
<title>Avance del proyecto</title>
|
|
116
|
+
<style>
|
|
117
|
+
/* Panel de avance.
|
|
118
|
+
Rejilla, no columna: quien lo abre quiere saber cómo va en treinta segundos,
|
|
119
|
+
no leer un documento. El detalle diario vive plegado porque es lo único que
|
|
120
|
+
se recorre entero, y solo cuando alguien busca un día concreto. */
|
|
121
|
+
|
|
122
|
+
:root{
|
|
123
|
+
--paper:#F1F3F1; --card:#fff; --card2:#EAEDEA; --line:#DEE2DD; --line2:#C5CAC4;
|
|
124
|
+
--ink:#161A18; --ink2:#4E5652; --ink3:#7E8781;
|
|
125
|
+
--jade:#0E7C6B; --jade2:#0A5F52; --jadebg:#E1EEEA; --jadedim:#8FC4B8;
|
|
126
|
+
--amber:#8F5C0D; --amberbg:#F6E9D0;
|
|
127
|
+
--on-jade:#fff;
|
|
128
|
+
--r:14px;
|
|
129
|
+
--shadow:0 1px 2px rgba(20,26,24,.04), 0 12px 32px -20px rgba(20,26,24,.28);
|
|
130
|
+
}
|
|
131
|
+
@media(prefers-color-scheme:dark){:root{
|
|
132
|
+
--paper:#0C100F; --card:#151A18; --card2:#1D2422; --line:#28302D; --line2:#3A4541;
|
|
133
|
+
--ink:#E8EEEB; --ink2:#A4AFA9; --ink3:#76817B;
|
|
134
|
+
--jade:#45C0A7; --jade2:#6FD8C2; --jadebg:#0F2F29; --jadedim:#1E5A4F;
|
|
135
|
+
--amber:#DDA85A; --amberbg:#31270F;
|
|
136
|
+
--on-jade:#07201C;
|
|
137
|
+
--shadow:0 1px 2px rgba(0,0,0,.5), 0 12px 32px -20px rgba(0,0,0,.8);
|
|
138
|
+
}}
|
|
139
|
+
|
|
140
|
+
*{box-sizing:border-box}
|
|
141
|
+
[hidden]{display:none!important}
|
|
142
|
+
body{margin:0;background:var(--paper);color:var(--ink);
|
|
143
|
+
font:15px/1.55 ui-sans-serif,-apple-system,"Segoe UI",Roboto,sans-serif;
|
|
144
|
+
-webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums}
|
|
145
|
+
|
|
146
|
+
/* ── Cabecera ─────────────────────────────────────────────────── */
|
|
147
|
+
.top{background:var(--card);border-bottom:1px solid var(--line);
|
|
148
|
+
padding:16px 28px;position:sticky;top:0;z-index:5}
|
|
149
|
+
.top-in{max-width:1180px;margin:0 auto;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
|
150
|
+
.mark{width:19px;height:19px;border-radius:6px;background:var(--jade);flex:none;
|
|
151
|
+
-webkit-mask:linear-gradient(#000 0 0) 3px 4px/3px 11px no-repeat,
|
|
152
|
+
linear-gradient(#000 0 0) 8px 6px/3px 7px no-repeat,
|
|
153
|
+
linear-gradient(#000 0 0) 13px 8px/3px 3px no-repeat;
|
|
154
|
+
mask:linear-gradient(#000 0 0) 3px 4px/3px 11px no-repeat,
|
|
155
|
+
linear-gradient(#000 0 0) 8px 6px/3px 7px no-repeat,
|
|
156
|
+
linear-gradient(#000 0 0) 13px 8px/3px 3px no-repeat}
|
|
157
|
+
.brand{font-weight:660;letter-spacing:-.015em;font-size:.95rem}
|
|
158
|
+
.top-sep{width:1px;height:18px;background:var(--line2)}
|
|
159
|
+
.top-project{font-weight:560;letter-spacing:-.01em}
|
|
160
|
+
.top-client{color:var(--ink3);font-size:.88rem}
|
|
161
|
+
.ro{margin-left:auto;font-size:.75rem;color:var(--ink3);
|
|
162
|
+
border:1px solid var(--line2);border-radius:999px;padding:3px 11px;white-space:nowrap}
|
|
163
|
+
|
|
164
|
+
main{max-width:1180px;margin:0 auto;padding:30px 28px 70px}
|
|
165
|
+
|
|
166
|
+
/* ── Titular ──────────────────────────────────────────────────── */
|
|
167
|
+
.lead{font-size:1.35rem;line-height:1.4;font-weight:480;letter-spacing:-.018em;
|
|
168
|
+
margin:0 0 24px;max-width:40ch;color:var(--ink2)}
|
|
169
|
+
.lead b{color:var(--ink);font-weight:660}
|
|
170
|
+
|
|
171
|
+
/* ── Fila de cifras ───────────────────────────────────────────── */
|
|
172
|
+
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));
|
|
173
|
+
gap:12px;margin-bottom:26px}
|
|
174
|
+
.kpi{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
|
|
175
|
+
padding:18px 20px}
|
|
176
|
+
.kpi.accent{background:var(--jadebg);border-color:transparent}
|
|
177
|
+
.kpi b{display:block;font-size:1.9rem;font-weight:640;letter-spacing:-.038em;line-height:1.05}
|
|
178
|
+
.kpi.accent b{color:var(--jade2)}
|
|
179
|
+
.kpi span{display:block;font-size:.8rem;color:var(--ink3);margin-top:5px;line-height:1.35}
|
|
180
|
+
.kpi.accent span{color:var(--jade2);opacity:.8}
|
|
181
|
+
|
|
182
|
+
/* ── Rejilla principal ────────────────────────────────────────── */
|
|
183
|
+
.grid{display:grid;grid-template-columns:1.55fr 1fr;gap:14px;align-items:start}
|
|
184
|
+
/* min-width:auto es el valor por defecto de un hijo de rejilla: no encoge por
|
|
185
|
+
debajo de su contenido, y basta un nombre largo con nowrap para desbordar
|
|
186
|
+
la pantalla en móvil. */
|
|
187
|
+
.grid>*{min-width:0}
|
|
188
|
+
@media(max-width:940px){.grid{grid-template-columns:1fr}}
|
|
189
|
+
|
|
190
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r);
|
|
191
|
+
overflow:hidden;margin-bottom:14px}
|
|
192
|
+
.card-h{padding:15px 20px 0}
|
|
193
|
+
.card-t{font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
|
|
194
|
+
color:var(--ink3)}
|
|
195
|
+
.card-s{font-size:.85rem;color:var(--ink3);margin:7px 0 0;line-height:1.5}
|
|
196
|
+
|
|
197
|
+
/* ── Barras de esfuerzo ───────────────────────────────────────── */
|
|
198
|
+
.feats{padding:14px 20px 18px;display:flex;flex-direction:column;gap:3px}
|
|
199
|
+
.feat{border:0;background:none;font:inherit;color:inherit;text-align:left;
|
|
200
|
+
padding:10px 10px;margin:0 -10px;border-radius:10px;cursor:pointer;display:block;width:calc(100% + 20px)}
|
|
201
|
+
.feat:hover{background:var(--card2)}
|
|
202
|
+
.feat-top{display:flex;align-items:baseline;gap:12px;margin-bottom:7px}
|
|
203
|
+
.feat-body{display:none}
|
|
204
|
+
.feat-name{flex:1;min-width:0;font-weight:560;overflow:hidden;text-overflow:ellipsis;
|
|
205
|
+
white-space:nowrap;letter-spacing:-.01em}
|
|
206
|
+
.feat-h{font-weight:620;white-space:nowrap;font-size:.95rem}
|
|
207
|
+
.feat-pct{color:var(--ink3);font-size:.8rem;white-space:nowrap;min-width:34px;text-align:right}
|
|
208
|
+
.bar{display:block;height:7px;border-radius:4px;background:var(--card2);overflow:hidden}
|
|
209
|
+
.bar i{display:block;height:100%;border-radius:4px;background:var(--jade);
|
|
210
|
+
width:0;transition:width .7s cubic-bezier(.2,.8,.25,1)}
|
|
211
|
+
.feat.kindwork .bar i{background:var(--jadedim)}
|
|
212
|
+
.pill{font-size:.66rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
|
|
213
|
+
padding:2px 8px;border-radius:999px;white-space:nowrap}
|
|
214
|
+
.pill.open{background:var(--amberbg);color:var(--amber)}
|
|
215
|
+
.pill.done{background:var(--jadebg);color:var(--jade2)}
|
|
216
|
+
|
|
217
|
+
/* ── Equipo ──────────────────────────────────────────────────── */
|
|
218
|
+
.mems{padding:6px 20px 16px;display:flex;flex-direction:column;gap:2px}
|
|
219
|
+
.mem{padding:11px 0;border-bottom:1px solid var(--line)}
|
|
220
|
+
.mem:last-child{border-bottom:none}
|
|
221
|
+
.mem-top{display:flex;align-items:center;gap:9px;margin-bottom:7px}
|
|
222
|
+
.mem-name{font-weight:580;letter-spacing:-.01em;flex:1;min-width:0;
|
|
223
|
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
224
|
+
.mem-h{font-weight:620;white-space:nowrap;font-size:.95rem}
|
|
225
|
+
/* Un guion se leería como "no hay dato". Aquí SÍ hay dato, y es justo lo que
|
|
226
|
+
se vende: la etiqueta tiene que decir que está cerrado, no que falta. */
|
|
227
|
+
.mem-lock{font-size:.66rem;font-weight:700;letter-spacing:.04em;
|
|
228
|
+
text-transform:uppercase;padding:3px 9px 3px 7px;border-radius:999px;
|
|
229
|
+
color:var(--ink3);border:1px dashed var(--line2);white-space:nowrap;cursor:help}
|
|
230
|
+
.mem-lock::before{content:"🔒";font-size:.9em;margin-right:4px;
|
|
231
|
+
text-transform:none;letter-spacing:0}
|
|
232
|
+
.mem-meta{display:block;margin-top:7px;font-size:.79rem;color:var(--ink3)}
|
|
233
|
+
/* La fila propia es la única con horas, así que se distingue sin gritar. */
|
|
234
|
+
.mem.is-me .bar>i{background:var(--jade)}
|
|
235
|
+
.mem .bar>i{background:var(--jadedim)}
|
|
236
|
+
|
|
237
|
+
.lock{margin:0 20px 18px;padding:15px 17px;border-radius:12px;
|
|
238
|
+
background:var(--jadebg);border:1px solid transparent}
|
|
239
|
+
.lock-t{font-weight:640;font-size:.93rem;color:var(--jade2);margin-bottom:4px}
|
|
240
|
+
.lock-t::before{content:"🔒 ";font-size:.85em}
|
|
241
|
+
.lock-d{font-size:.83rem;line-height:1.5;color:var(--jade2);opacity:.88;max-width:62ch}
|
|
242
|
+
.lock-cta{display:inline-block;margin-top:11px;font-size:.84rem;font-weight:600;
|
|
243
|
+
color:var(--on-jade);background:var(--jade);text-decoration:none;
|
|
244
|
+
padding:8px 15px;border-radius:9px}
|
|
245
|
+
.lock-cta:hover{background:var(--jade2)}
|
|
246
|
+
|
|
247
|
+
.feat-body{padding:2px 0 6px}
|
|
248
|
+
.feat.is-open .feat-body{display:block}
|
|
249
|
+
.commits{list-style:none;margin:8px 0 0;padding:0;display:flex;flex-direction:column;gap:5px}
|
|
250
|
+
.commits li{font-size:.86rem;color:var(--ink2);display:flex;gap:9px;align-items:baseline}
|
|
251
|
+
.commits code{font-family:ui-monospace,Menlo,monospace;font-size:.76rem;color:var(--jade2);
|
|
252
|
+
background:var(--jadebg);padding:1px 6px;border-radius:5px;flex:none}
|
|
253
|
+
.commits span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
254
|
+
|
|
255
|
+
/* ── Filas de la columna lateral ──────────────────────────────── */
|
|
256
|
+
.rows{padding:6px 20px 16px;display:flex;flex-direction:column}
|
|
257
|
+
.row{display:flex;align-items:baseline;gap:12px;padding:10px 0;
|
|
258
|
+
border-bottom:1px solid var(--line);font-size:.88rem}
|
|
259
|
+
.row:last-child{border-bottom:0}
|
|
260
|
+
.row-a{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
261
|
+
.row-a.mono{font-family:ui-monospace,Menlo,monospace;font-size:.78rem}
|
|
262
|
+
.row-b{font-weight:640;color:var(--amber);white-space:nowrap}
|
|
263
|
+
.row-c{color:var(--ink3);font-size:.8rem;white-space:nowrap}
|
|
264
|
+
|
|
265
|
+
/* ── Ritmo ────────────────────────────────────────────────────── */
|
|
266
|
+
.rhythm{padding:16px 20px 18px}
|
|
267
|
+
.rhythm-n{display:flex;gap:22px;flex-wrap:wrap;margin-bottom:16px}
|
|
268
|
+
.rhythm-n div b{display:block;font-size:1.2rem;font-weight:640;letter-spacing:-.03em}
|
|
269
|
+
.rhythm-n div span{display:block;font-size:.76rem;color:var(--ink3);margin-top:2px}
|
|
270
|
+
.bars{display:flex;align-items:flex-end;gap:2px;height:52px}
|
|
271
|
+
.bars i{flex:1;background:var(--jade);border-radius:2px 2px 0 0;min-height:2px;
|
|
272
|
+
opacity:.8;height:0;transition:height .6s cubic-bezier(.2,.8,.25,1)}
|
|
273
|
+
.bars i:hover{opacity:1}
|
|
274
|
+
.axis{display:flex;justify-content:space-between;font-size:.7rem;color:var(--ink3);margin-top:8px}
|
|
275
|
+
|
|
276
|
+
/* ── Detalle plegado ──────────────────────────────────────────── */
|
|
277
|
+
.detail{margin-top:14px}
|
|
278
|
+
.detail-h{width:100%;border:0;background:var(--card);border:1px solid var(--line);
|
|
279
|
+
border-radius:var(--r);padding:15px 20px;font:inherit;color:inherit;cursor:pointer;
|
|
280
|
+
display:flex;align-items:center;gap:12px;text-align:left}
|
|
281
|
+
.detail-h:hover{border-color:var(--line2)}
|
|
282
|
+
.detail-h .card-t{flex:1}
|
|
283
|
+
.chev{color:var(--ink3);font-size:.8rem;transition:transform .2s}
|
|
284
|
+
.detail.is-open .chev{transform:rotate(90deg)}
|
|
285
|
+
.detail-body{display:none;margin-top:10px}
|
|
286
|
+
.detail.is-open .detail-body{display:block}
|
|
287
|
+
.filters{display:flex;gap:5px;margin-bottom:10px;flex-wrap:wrap}
|
|
288
|
+
.filters button{border:1px solid var(--line);background:var(--card);color:var(--ink2);
|
|
289
|
+
border-radius:8px;padding:5px 12px;font:inherit;font-size:.83rem;cursor:pointer}
|
|
290
|
+
.filters button:hover{border-color:var(--line2);color:var(--ink)}
|
|
291
|
+
.filters button[aria-pressed=true]{background:var(--ink);color:var(--paper);border-color:var(--ink)}
|
|
292
|
+
.days{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:8px}
|
|
293
|
+
.day{background:var(--card);border:1px solid var(--line);border-radius:11px;overflow:hidden}
|
|
294
|
+
.day-h{display:flex;align-items:center;gap:10px;width:100%;padding:12px 15px;
|
|
295
|
+
background:none;border:0;font:inherit;color:inherit;text-align:left;cursor:pointer}
|
|
296
|
+
.day-h:hover{background:var(--card2)}
|
|
297
|
+
.day-d{flex:1;font-size:.88rem;font-weight:540}
|
|
298
|
+
.day-t{font-weight:620;white-space:nowrap;font-size:.88rem}
|
|
299
|
+
.day-body{padding:0 15px 13px;border-top:1px solid var(--line)}
|
|
300
|
+
.item{padding:10px 0;border-bottom:1px solid var(--line);font-size:.85rem}
|
|
301
|
+
.item:last-child{border-bottom:0}
|
|
302
|
+
.item-top{display:flex;gap:10px;align-items:baseline}
|
|
303
|
+
.item-w{flex:1;min-width:0}
|
|
304
|
+
.item-t{color:var(--ink2);white-space:nowrap}
|
|
305
|
+
.item-a{color:var(--jade2);font-weight:620;white-space:nowrap}
|
|
306
|
+
.kindtag{display:inline-block;font-size:.64rem;font-weight:700;letter-spacing:.04em;
|
|
307
|
+
text-transform:uppercase;padding:2px 6px;border-radius:5px;margin-right:7px;
|
|
308
|
+
background:var(--amberbg);color:var(--amber);vertical-align:1px}
|
|
309
|
+
|
|
310
|
+
.empty{color:var(--ink3);text-align:center;padding:38px 0;font-size:.9rem}
|
|
311
|
+
|
|
312
|
+
/* ── Pie ──────────────────────────────────────────────────────── */
|
|
313
|
+
.cta{background:var(--card);border:1px solid var(--line2);border-radius:var(--r);
|
|
314
|
+
padding:18px 22px;margin-top:24px;display:flex;gap:20px;align-items:center;flex-wrap:wrap}
|
|
315
|
+
.cta div{flex:1;min-width:230px}
|
|
316
|
+
.cta b{display:block;font-size:.98rem;margin-bottom:4px;letter-spacing:-.01em}
|
|
317
|
+
.cta p{margin:0;font-size:.87rem;color:var(--ink2);line-height:1.5}
|
|
318
|
+
.cta a{color:var(--on-jade);background:var(--jade);text-decoration:none;
|
|
319
|
+
padding:9px 18px;border-radius:9px;font-size:.88rem;font-weight:620;white-space:nowrap}
|
|
320
|
+
footer{margin-top:26px;font-size:.8rem;color:var(--ink3);line-height:1.6}
|
|
321
|
+
footer a{color:var(--jade2)}
|
|
322
|
+
|
|
323
|
+
/* Una sola entrada orquestada: las tarjetas suben al cargar y las barras
|
|
324
|
+
crecen. Más movimiento que esto distrae de lo que hay que leer. */
|
|
325
|
+
.rise{opacity:0;transform:translateY(10px);
|
|
326
|
+
transition:opacity .5s ease,transform .5s cubic-bezier(.2,.8,.25,1)}
|
|
327
|
+
.rise.in{opacity:1;transform:none}
|
|
328
|
+
|
|
329
|
+
@media(prefers-reduced-motion:reduce){
|
|
330
|
+
*,*::before,*::after{transition-duration:.01ms!important;animation-duration:.01ms!important}
|
|
331
|
+
.rise{opacity:1;transform:none}
|
|
332
|
+
}
|
|
333
|
+
@media(max-width:560px){
|
|
334
|
+
main{padding:22px 16px 56px}
|
|
335
|
+
.top{padding:14px 16px}
|
|
336
|
+
.lead{font-size:1.12rem}
|
|
337
|
+
.kpi b{font-size:1.6rem}
|
|
338
|
+
.days{grid-template-columns:1fr}
|
|
339
|
+
.top-client{display:none}
|
|
340
|
+
}
|
|
341
|
+
@media print{
|
|
342
|
+
.top{position:static}.filters,.cta,.detail-h{display:none}
|
|
343
|
+
.detail-body{display:block!important}.rise{opacity:1;transform:none}
|
|
344
|
+
.bar i,.bars i{transition:none}
|
|
345
|
+
}
|
|
346
|
+
</style>
|
|
347
|
+
</head>
|
|
348
|
+
<body>
|
|
349
|
+
<header class="top">
|
|
350
|
+
<div class="top-in">
|
|
351
|
+
<span class="mark"></span><span class="brand">Estela</span>
|
|
352
|
+
<span class="top-sep"></span>
|
|
353
|
+
<span class="top-project" id="h-project"></span>
|
|
354
|
+
<span class="top-client" id="h-client"></span>
|
|
355
|
+
<span class="ro">Solo lectura</span>
|
|
356
|
+
</div>
|
|
357
|
+
</header>
|
|
358
|
+
|
|
359
|
+
<main>
|
|
360
|
+
<p class="lead" id="lead"></p>
|
|
361
|
+
<div class="kpis" id="kpis"></div>
|
|
362
|
+
|
|
363
|
+
<div class="grid">
|
|
364
|
+
<div>
|
|
365
|
+
<section class="card rise">
|
|
366
|
+
<div class="card-h"><div class="card-t">En qué se fue el esfuerzo</div></div>
|
|
367
|
+
<div class="feats" id="feats"></div>
|
|
368
|
+
</section>
|
|
369
|
+
<div id="team"></div>
|
|
370
|
+
<div id="main-extra"></div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
<div id="side"></div>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<div class="detail" id="detail">
|
|
377
|
+
<button class="detail-h" id="detail-toggle" aria-expanded="false">
|
|
378
|
+
<span class="card-t">Detalle por día</span>
|
|
379
|
+
<span class="card-s" id="detail-count" style="margin:0"></span>
|
|
380
|
+
<span class="chev">›</span>
|
|
381
|
+
</button>
|
|
382
|
+
<div class="detail-body">
|
|
383
|
+
<div class="filters" id="filters"></div>
|
|
384
|
+
<div class="days" id="days"></div>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<div class="cta">
|
|
389
|
+
<div>
|
|
390
|
+
<b>Esto es una foto del proyecto</b>
|
|
391
|
+
<p>Se genera de la actividad real de Git y del editor, y se actualiza cuando
|
|
392
|
+
quien lo publica lo vuelve a publicar.</p>
|
|
393
|
+
</div>
|
|
394
|
+
<a id="cta-more" rel="noopener noreferrer">Verlo para un equipo</a>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
<footer>
|
|
398
|
+
Cada bloque está respaldado por sus commits.
|
|
399
|
+
<a id="footer-site" rel="noopener noreferrer"></a>
|
|
400
|
+
</footer>
|
|
401
|
+
</main>
|
|
402
|
+
|
|
403
|
+
<script id="data" type="application/json">__DATA__</script>
|
|
404
|
+
<script>
|
|
405
|
+
"use strict";
|
|
406
|
+
var D = JSON.parse(document.getElementById("data").textContent);
|
|
407
|
+
var MONTHS = ["enero","febrero","marzo","abril","mayo","junio","julio","agosto",
|
|
408
|
+
"septiembre","octubre","noviembre","diciembre"];
|
|
409
|
+
var DAYS = ["domingo","lunes","martes","miércoles","jueves","viernes","sábado"];
|
|
410
|
+
var openDay = {}, openFeat = {}, filter = "all";
|
|
411
|
+
|
|
412
|
+
function dur(s){var t=Math.round(s/60),h=Math.floor(t/60),m=t%60;
|
|
413
|
+
return h===0?m+"m":(m===0?h+"h":h+"h "+String(m).padStart(2,"0")+"m");}
|
|
414
|
+
function esc(t){return String(t).replace(/[&<>"']/g,function(c){
|
|
415
|
+
return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c];});}
|
|
416
|
+
function longDate(iso){var d=new Date(iso+"T12:00:00Z");
|
|
417
|
+
return DAYS[d.getUTCDay()]+" "+d.getUTCDate()+" de "+MONTHS[d.getUTCMonth()];}
|
|
418
|
+
function el(id){return document.getElementById(id);}
|
|
419
|
+
|
|
420
|
+
function shown(){
|
|
421
|
+
return filter==="all" ? D.days : D.days.filter(function(d){return d.date.indexOf(filter)===0;});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* ── Cabecera y titular ─────────────────────────────────────── */
|
|
425
|
+
function renderHead(){
|
|
426
|
+
el("h-project").textContent = D.project;
|
|
427
|
+
el("h-client").textContent = "· " + D.client;
|
|
428
|
+
|
|
429
|
+
var top = D.features[0];
|
|
430
|
+
var entregas = D.features.reduce(function(s,f){return s+f.delivered.length;},0);
|
|
431
|
+
var frase = [];
|
|
432
|
+
if (top) frase.push("El grueso del trabajo fue <b>"+esc(top.name)+"</b>");
|
|
433
|
+
if (entregas) frase.push("con <b>"+entregas+(entregas===1?" entrega":" entregas")+"</b>");
|
|
434
|
+
if (D.open.length) frase.push("y <b>"+D.open.length+
|
|
435
|
+
(D.open.length===1?" frente abierto":" frentes abiertos")+"</b>");
|
|
436
|
+
el("lead").innerHTML = frase.length ? frase.join(" ")+"." : "";
|
|
437
|
+
|
|
438
|
+
var k = [["", dur(D.totalSeconds), "horas trabajadas"],
|
|
439
|
+
["", D.rhythm.activeDays+" de "+D.rhythm.spanDays, "días con actividad"],
|
|
440
|
+
["", String(D.features.length), D.features.length===1?"frente de trabajo":"frentes de trabajo"]];
|
|
441
|
+
if (D.totalAmount) k.push(["accent", D.totalAmount, "valor del trabajo"]);
|
|
442
|
+
el("kpis").innerHTML = k.map(function(x){
|
|
443
|
+
return '<div class="kpi '+x[0]+'"><b>'+esc(x[1])+'</b><span>'+esc(x[2])+'</span></div>';
|
|
444
|
+
}).join("");
|
|
445
|
+
|
|
446
|
+
// Mismo sitio configurable que el candado de "Equipo del proyecto": el
|
|
447
|
+
// dominio final aún no está comprado, y un enlace roto es peor que no tener
|
|
448
|
+
// enlace. Quien busca de dónde sale esto y encuentra un dominio muerto duda
|
|
449
|
+
// más que si no hubiera buscado nada.
|
|
450
|
+
var site = D.site || "https://getestela.dev";
|
|
451
|
+
el("cta-more").setAttribute("href", site + "/teams?d=" + encodeURIComponent(D.client||""));
|
|
452
|
+
var siteLink = el("footer-site");
|
|
453
|
+
siteLink.setAttribute("href", site);
|
|
454
|
+
// Sin regex a propósito: dentro de este template literal, "\/" no es un
|
|
455
|
+
// escape válido y se colapsa a "/" al compilar. Un "/^https?:\/\//" acababa
|
|
456
|
+
// como "/^https?:///" en el navegador — sintaxis inválida que rompía la
|
|
457
|
+
// etiqueta <script> entera, no solo este enlace. String.replace con texto
|
|
458
|
+
// plano no tiene esa trampa.
|
|
459
|
+
siteLink.textContent = site.replace("https://", "").replace("http://", "");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/* ── Esfuerzo, con proporción visible ───────────────────────── */
|
|
463
|
+
function renderFeats(){
|
|
464
|
+
var max = D.features.reduce(function(m,f){return Math.max(m,f.seconds);},0) || 1;
|
|
465
|
+
var total = D.features.reduce(function(s,f){return s+f.seconds;},0) || 1;
|
|
466
|
+
|
|
467
|
+
el("feats").innerHTML = D.features.map(function(f,i){
|
|
468
|
+
var isOpen = !!openFeat[i];
|
|
469
|
+
var pill = f.merged===null ? "" :
|
|
470
|
+
'<span class="pill '+(f.merged?"done":"open")+'">'+(f.merged?"integrado":"en curso")+'</span>';
|
|
471
|
+
return '<button class="feat'+(isOpen?" is-open":"")+(f.merged===null&&!f.delivered.length?" kindwork":"")+
|
|
472
|
+
'" data-f="'+i+'" aria-expanded="'+isOpen+'">'+
|
|
473
|
+
'<span class="feat-top">'+
|
|
474
|
+
'<span class="feat-name">'+esc(f.name)+'</span>'+pill+
|
|
475
|
+
'<span class="feat-h">'+dur(f.seconds)+'</span>'+
|
|
476
|
+
'<span class="feat-pct">'+Math.round(f.seconds/total*100)+'%</span></span>'+
|
|
477
|
+
'<span class="bar"><i data-w="'+(f.seconds/max*100)+'"></i></span>'+
|
|
478
|
+
'<span class="feat-body">'+
|
|
479
|
+
(f.delivered.length
|
|
480
|
+
? '<ul class="commits">'+f.delivered.map(function(c){
|
|
481
|
+
return '<li><code>'+esc(c.hash)+'</code><span>'+esc(c.subject)+'</span></li>';
|
|
482
|
+
}).join("")+
|
|
483
|
+
(f.moreDelivered?'<li><span>y '+f.moreDelivered+' más</span></li>':"")+'</ul>'
|
|
484
|
+
: '<p class="card-s">'+f.days+(f.days===1?" día":" días")+
|
|
485
|
+
' de trabajo, sin commits asociados.</p>')+
|
|
486
|
+
'</span></button>';
|
|
487
|
+
}).join("");
|
|
488
|
+
|
|
489
|
+
Array.prototype.forEach.call(document.querySelectorAll(".feat"), function(b){
|
|
490
|
+
b.onclick = function(){ openFeat[b.dataset.f] = !openFeat[b.dataset.f]; renderFeats(); grow(); };
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ── Equipo del proyecto ────────────────────────────────────── */
|
|
495
|
+
/* Los nombres, ramas y commits de su gente ya están en su repositorio: se los
|
|
496
|
+
enseñamos porque demuestra que esto funciona sobre sus datos, no sobre una
|
|
497
|
+
demo. Las horas de cada uno son lo que Estela produce y él no puede sacar
|
|
498
|
+
solo, y por eso no viajan en este fichero. */
|
|
499
|
+
function renderTeam(){
|
|
500
|
+
var t = D.team || [];
|
|
501
|
+
if (!t.length) { el("team").innerHTML = ""; return; }
|
|
502
|
+
|
|
503
|
+
var max = t.reduce(function(m,p){return Math.max(m,p.commits);},0) || 1;
|
|
504
|
+
var others = t.filter(function(p){return !p.isMe;}).length;
|
|
505
|
+
|
|
506
|
+
var rows = t.map(function(p){
|
|
507
|
+
var bar = '<span class="bar"><i data-w="'+(p.commits/max*100)+'"></i></span>';
|
|
508
|
+
var meta = p.commits+(p.commits===1?" commit":" commits")+
|
|
509
|
+
" · "+p.branches+(p.branches===1?" rama":" ramas")+
|
|
510
|
+
(p.lastDay?" · hasta "+p.lastDay:"");
|
|
511
|
+
|
|
512
|
+
// dur() solo se llama cuando seconds viene, y solo viene para quien publica.
|
|
513
|
+
var right = p.isMe && p.seconds!=null
|
|
514
|
+
? '<span class="mem-h">'+dur(p.seconds)+'</span><span class="pill done">medido</span>'
|
|
515
|
+
: '<span class="mem-lock" title="Se mide con Teams">Teams</span>';
|
|
516
|
+
|
|
517
|
+
return '<div class="mem'+(p.isMe?" is-me":"")+'">'+
|
|
518
|
+
'<span class="mem-top">'+
|
|
519
|
+
'<span class="mem-name">'+esc(p.name)+(p.isMe?' <span class="pill">tú</span>':"")+'</span>'+
|
|
520
|
+
right+'</span>'+
|
|
521
|
+
bar+
|
|
522
|
+
'<span class="mem-meta">'+esc(meta)+'</span></div>';
|
|
523
|
+
}).join("");
|
|
524
|
+
|
|
525
|
+
var lock = others>0
|
|
526
|
+
? '<div class="lock">'+
|
|
527
|
+
'<div class="lock-t">Horas medidas de tu equipo</div>'+
|
|
528
|
+
'<div class="lock-d">Las de tu gente pueden estimarse desde sus commits, '+
|
|
529
|
+
'pero medirlas exige que instalen Estela. Con <b>Teams</b> se miden, y '+
|
|
530
|
+
'dejan de ser una suposición que alguien pueda discutir.'+
|
|
531
|
+
'<br><br>El coste de IA se informa <b>por proyecto</b>, nunca por '+
|
|
532
|
+
'persona, y solo la que paga la empresa. Lo que cada cual gasta de su '+
|
|
533
|
+
'bolsillo es suyo.</div>'+
|
|
534
|
+
'<a class="lock-cta" href="'+D.site+'/teams?d='+
|
|
535
|
+
encodeURIComponent(D.client||"")+'" target="_blank" rel="noopener">'+
|
|
536
|
+
'Ver qué incluye Teams →</a>'+
|
|
537
|
+
'</div>'
|
|
538
|
+
: "";
|
|
539
|
+
|
|
540
|
+
el("team").innerHTML = '<section class="card rise">'+
|
|
541
|
+
'<div class="card-h"><div class="card-t">Equipo del proyecto</div>'+
|
|
542
|
+
'<div class="card-s">'+t.length+(t.length===1?" persona":" personas")+
|
|
543
|
+
' con actividad. Salen de los commits del repositorio, con sus identidades '+
|
|
544
|
+
'de git ya unificadas.</div></div>'+
|
|
545
|
+
'<div class="mems">'+rows+'</div>'+lock+'</section>';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* ── Columna lateral ────────────────────────────────────────── */
|
|
549
|
+
function renderSide(){
|
|
550
|
+
var out = [];
|
|
551
|
+
|
|
552
|
+
var r = D.rhythm;
|
|
553
|
+
var list = D.days;
|
|
554
|
+
var max = list.reduce(function(m,d){return Math.max(m,d.seconds);},0) || 1;
|
|
555
|
+
out.push('<section class="card rise"><div class="card-h"><div class="card-t">Ritmo</div></div>'+
|
|
556
|
+
'<div class="rhythm"><div class="rhythm-n">'+
|
|
557
|
+
'<div><b>'+dur(r.medianSeconds)+'</b><span>jornada típica</span></div>'+
|
|
558
|
+
(r.longestGapDays>0?'<div><b>'+r.longestGapDays+'</b><span>días la pausa más larga</span></div>':"")+
|
|
559
|
+
'</div>'+
|
|
560
|
+
(list.length>1
|
|
561
|
+
? '<div class="bars">'+list.map(function(d){
|
|
562
|
+
return '<i data-h="'+Math.max(4,Math.round(d.seconds/max*100))+
|
|
563
|
+
'" title="'+esc(d.date+": "+dur(d.seconds))+'"></i>';
|
|
564
|
+
}).join("")+'</div>'+
|
|
565
|
+
'<div class="axis"><span>'+esc(list[0].date.slice(5))+'</span><span>'+
|
|
566
|
+
esc(list[list.length-1].date.slice(5))+'</span></div>'
|
|
567
|
+
: "")+
|
|
568
|
+
'</div></section>');
|
|
569
|
+
|
|
570
|
+
if (D.open.length) {
|
|
571
|
+
out.push('<section class="card rise"><div class="card-h">'+
|
|
572
|
+
'<div class="card-t">Pendiente de integrar</div>'+
|
|
573
|
+
'<p class="card-s">Trabajo terminado que aún no está en la rama principal.</p></div>'+
|
|
574
|
+
'<div class="rows">'+D.open.map(function(o){
|
|
575
|
+
return '<div class="row"><span class="row-a">'+esc(o.name)+'</span>'+
|
|
576
|
+
'<span class="row-b">'+o.commits+(o.commits===1?" commit":" commits")+'</span>'+
|
|
577
|
+
'<span class="row-c">'+(o.ageDays===0?"hoy":o.ageDays===1?"ayer":"hace "+o.ageDays+" d")+
|
|
578
|
+
'</span></div>';
|
|
579
|
+
}).join("")+'</div></section>');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
el("side").innerHTML = out.join("");
|
|
583
|
+
|
|
584
|
+
var extra = [];
|
|
585
|
+
if (D.rework.length) {
|
|
586
|
+
extra.push('<section class="card rise"><div class="card-h">'+
|
|
587
|
+
'<div class="card-t">Dónde costó más</div>'+
|
|
588
|
+
'<p class="card-s">Ficheros retocados varias veces en pocos días. Suele señalar '+
|
|
589
|
+
'requisitos que se afinaron sobre la marcha.</p></div>'+
|
|
590
|
+
'<div class="rows">'+D.rework.map(function(f){
|
|
591
|
+
return '<div class="row"><span class="row-a mono">'+esc(f.file)+'</span>'+
|
|
592
|
+
'<span class="row-b">'+f.touches+'×</span>'+
|
|
593
|
+
'<span class="row-c">en '+f.spanDays+" d"+'</span></div>';
|
|
594
|
+
}).join("")+'</div></section>');
|
|
595
|
+
}
|
|
596
|
+
el("main-extra").innerHTML = extra.join("");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/* ── Detalle por día ────────────────────────────────────────── */
|
|
600
|
+
function renderDays(){
|
|
601
|
+
var list = shown();
|
|
602
|
+
el("detail-count").textContent = list.length + (list.length===1?" día":" días");
|
|
603
|
+
|
|
604
|
+
var ms = {};
|
|
605
|
+
D.days.forEach(function(d){ ms[d.date.slice(0,7)] = true; });
|
|
606
|
+
var months = Object.keys(ms).sort();
|
|
607
|
+
|
|
608
|
+
el("filters").innerHTML = months.length<2 ? "" :
|
|
609
|
+
['<button data-f="all" aria-pressed="'+(filter==="all")+'">Todo</button>']
|
|
610
|
+
.concat(months.map(function(m){
|
|
611
|
+
return '<button data-f="'+m+'" aria-pressed="'+(filter===m)+'">'+
|
|
612
|
+
MONTHS[Number(m.slice(5,7))-1]+" "+m.slice(0,4)+'</button>';
|
|
613
|
+
})).join("");
|
|
614
|
+
|
|
615
|
+
el("days").innerHTML = list.length===0
|
|
616
|
+
? '<p class="empty">Sin actividad en este periodo.</p>'
|
|
617
|
+
: list.map(function(d){
|
|
618
|
+
var isOpen = !!openDay[d.date];
|
|
619
|
+
return '<div class="day'+(isOpen?" is-open":"")+'">'+
|
|
620
|
+
'<button class="day-h" data-d="'+esc(d.date)+'" aria-expanded="'+isOpen+'">'+
|
|
621
|
+
'<span class="day-d">'+esc(longDate(d.date))+'</span>'+
|
|
622
|
+
'<span class="day-t">'+dur(d.seconds)+'</span>'+
|
|
623
|
+
'<span class="chev">›</span></button>'+
|
|
624
|
+
(isOpen?'<div class="day-body">'+d.items.map(item).join("")+'</div>':"")+
|
|
625
|
+
'</div>';
|
|
626
|
+
}).join("");
|
|
627
|
+
|
|
628
|
+
Array.prototype.forEach.call(document.querySelectorAll(".day-h"), function(b){
|
|
629
|
+
b.onclick = function(){ openDay[b.dataset.d] = !openDay[b.dataset.d]; renderDays(); };
|
|
630
|
+
});
|
|
631
|
+
Array.prototype.forEach.call(document.querySelectorAll("#filters button"), function(b){
|
|
632
|
+
b.onclick = function(){ filter = b.dataset.f; renderDays(); };
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function item(it){
|
|
637
|
+
return '<div class="item"><div class="item-top">'+
|
|
638
|
+
'<span class="item-w">'+(it.kind?'<span class="kindtag">'+esc(it.kind)+'</span>':"")+
|
|
639
|
+
esc(it.what)+'</span>'+
|
|
640
|
+
'<span class="item-t">'+dur(it.seconds)+'</span>'+
|
|
641
|
+
(it.amount?'<span class="item-a">'+esc(it.amount)+'</span>':"")+'</div>'+
|
|
642
|
+
(it.commits.length
|
|
643
|
+
? '<ul class="commits">'+it.commits.map(function(c){
|
|
644
|
+
return '<li><code>'+esc(c.hash)+'</code><span>'+esc(c.subject)+'</span></li>';
|
|
645
|
+
}).join("")+'</ul>'
|
|
646
|
+
: "")+'</div>';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/* ── Entrada ────────────────────────────────────────────────── */
|
|
650
|
+
function grow(){
|
|
651
|
+
requestAnimationFrame(function(){
|
|
652
|
+
Array.prototype.forEach.call(document.querySelectorAll(".bar i"), function(b){
|
|
653
|
+
b.style.width = b.dataset.w + "%";
|
|
654
|
+
});
|
|
655
|
+
Array.prototype.forEach.call(document.querySelectorAll(".bars i"), function(b){
|
|
656
|
+
b.style.height = b.dataset.h + "%";
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
renderHead();
|
|
662
|
+
renderFeats();
|
|
663
|
+
renderTeam();
|
|
664
|
+
renderSide();
|
|
665
|
+
renderDays();
|
|
666
|
+
|
|
667
|
+
el("detail-toggle").onclick = function(){
|
|
668
|
+
var d = el("detail"), on = d.classList.toggle("is-open");
|
|
669
|
+
this.setAttribute("aria-expanded", String(on));
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
requestAnimationFrame(function(){
|
|
673
|
+
Array.prototype.forEach.call(document.querySelectorAll(".rise"), function(c,i){
|
|
674
|
+
setTimeout(function(){ c.classList.add("in"); }, i*70);
|
|
675
|
+
});
|
|
676
|
+
grow();
|
|
677
|
+
});
|
|
678
|
+
</script>
|
|
679
|
+
</body>
|
|
680
|
+
</html>`.replace("__DATA__", payload);
|
|
681
|
+
}
|
|
682
|
+
/** Las dos últimas partes de la ruta: el fichero se reconoce, el árbol estorba. */
|
|
683
|
+
const MONTHS_ES = ["enero", "febrero", "marzo", "abril", "mayo", "junio",
|
|
684
|
+
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"];
|
|
685
|
+
const DAYS_ES = ["domingo", "lunes", "martes", "miércoles", "jueves", "viernes", "sábado"];
|
|
686
|
+
function longDate(iso) {
|
|
687
|
+
const d = new Date(`${iso}T12:00:00Z`);
|
|
688
|
+
return `${DAYS_ES[d.getUTCDay()]} ${d.getUTCDate()} de ${MONTHS_ES[d.getUTCMonth()]}`;
|
|
689
|
+
}
|
|
690
|
+
function esc(text) {
|
|
691
|
+
return text.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=panel.js.map
|