estela 0.3.3 → 0.3.4
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/dist/db/schema.js +9 -0
- package/dist/db/store.d.ts +18 -0
- package/dist/db/store.js +59 -4
- package/dist/i18n/en.js +5 -0
- package/dist/server.js +67 -8
- package/dist/setup.js +1 -1
- package/node_modules/@estela/shared/dist/types.d.ts +5 -0
- package/node_modules/@estela/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@estela/shared/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/web/app.css +4 -0
- package/web/app.js +67 -0
- package/web/i18n.js +24 -0
package/dist/db/schema.js
CHANGED
|
@@ -47,6 +47,8 @@ CREATE TABLE IF NOT EXISTS projects (
|
|
|
47
47
|
ai_budget_micro_usd INTEGER,
|
|
48
48
|
-- NULL = activo. Ver la migración 12 para por qué cerrar no bloquea nada.
|
|
49
49
|
closed_at TEXT,
|
|
50
|
+
-- NULL = visible. Archivado: fuera del panel y sin captar horas.
|
|
51
|
+
archived_at TEXT,
|
|
50
52
|
-- Gestor de tareas en JSON ({"system":"jira","prefixes":["PROJ"]}). NULL =
|
|
51
53
|
-- sin configurar: solo se reconocen los tickets inequívocos.
|
|
52
54
|
tracker TEXT
|
|
@@ -292,6 +294,13 @@ CREATE TABLE IF NOT EXISTS claude_plans (
|
|
|
292
294
|
* empezar no es una opción de mantenimiento.
|
|
293
295
|
*/
|
|
294
296
|
const MIGRATIONS = [
|
|
297
|
+
{
|
|
298
|
+
version: 17,
|
|
299
|
+
describe: "archivar proyectos",
|
|
300
|
+
run: (db) => {
|
|
301
|
+
addColumn(db, "projects", "archived_at", "TEXT");
|
|
302
|
+
},
|
|
303
|
+
},
|
|
295
304
|
{
|
|
296
305
|
version: 16,
|
|
297
306
|
describe: "rutas de Windows iguales vengan de donde vengan",
|
package/dist/db/store.d.ts
CHANGED
|
@@ -32,6 +32,19 @@ export declare function setProjectTracker(db: DatabaseSync, projectId: string, t
|
|
|
32
32
|
export declare function closeProject(db: DatabaseSync, id: string, at?: Date): void;
|
|
33
33
|
/** Reabre un proyecto cerrado. Sin efecto si ya estaba abierto. */
|
|
34
34
|
export declare function reopenProject(db: DatabaseSync, id: string): void;
|
|
35
|
+
/** Archiva un proyecto: sale del panel y deja de captar. No borra nada. */
|
|
36
|
+
export declare function archiveProject(db: DatabaseSync, id: string, at?: Date): void;
|
|
37
|
+
export declare function restoreProject(db: DatabaseSync, id: string): void;
|
|
38
|
+
export declare function listArchivedProjects(db: DatabaseSync): Project[];
|
|
39
|
+
/** Por qué no se puede eliminar un proyecto, o null si se puede. */
|
|
40
|
+
export declare function projectDeleteBlocker(db: DatabaseSync, id: string): "invoiced" | "shared" | null;
|
|
41
|
+
/**
|
|
42
|
+
* Elimina un proyecto y sus imputaciones, tarifas y vínculos. Los turnos y los
|
|
43
|
+
* commits se quedan: son de la máquina, no del proyecto, y si el repo se vuelve
|
|
44
|
+
* a registrar sus horas reaparecen. Lo facturado o compartido no se elimina.
|
|
45
|
+
*/
|
|
46
|
+
export declare function deleteProject(db: DatabaseSync, id: string): void;
|
|
47
|
+
/** Los proyectos a la vista. Los archivados, con `listArchivedProjects`. */
|
|
35
48
|
export declare function listProjects(db: DatabaseSync): Project[];
|
|
36
49
|
export declare function listClients(db: DatabaseSync): Client[];
|
|
37
50
|
export declare function getRates(db: DatabaseSync, projectId: string): RatePeriod[];
|
|
@@ -76,6 +89,11 @@ export declare function listPublications(db: DatabaseSync): Publication[];
|
|
|
76
89
|
export declare function authorsByRepo(db: DatabaseSync): Map<string, string[]>;
|
|
77
90
|
/** Encuentra el proyecto al que pertenece una ruta de repositorio. */
|
|
78
91
|
export declare function projectForRepo(db: DatabaseSync, repoPath: string): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Como `projectForRepo`, contando los archivados. Para lo que ofrece registrar
|
|
94
|
+
* un repo: uno archivado ya tiene dueño, y ofrecerlo otra vez lo resucitaría.
|
|
95
|
+
*/
|
|
96
|
+
export declare function ownerOfRepo(db: DatabaseSync, repoPath: string): string | null;
|
|
79
97
|
/**
|
|
80
98
|
* Todos los repositorios que ya están vinculados a algún proyecto, sin
|
|
81
99
|
* importar cómo llegaron ahí (un transcript de agente, `estela project add`,
|
package/dist/db/store.js
CHANGED
|
@@ -10,6 +10,11 @@ exports.getProject = getProject;
|
|
|
10
10
|
exports.setProjectTracker = setProjectTracker;
|
|
11
11
|
exports.closeProject = closeProject;
|
|
12
12
|
exports.reopenProject = reopenProject;
|
|
13
|
+
exports.archiveProject = archiveProject;
|
|
14
|
+
exports.restoreProject = restoreProject;
|
|
15
|
+
exports.listArchivedProjects = listArchivedProjects;
|
|
16
|
+
exports.projectDeleteBlocker = projectDeleteBlocker;
|
|
17
|
+
exports.deleteProject = deleteProject;
|
|
13
18
|
exports.listProjects = listProjects;
|
|
14
19
|
exports.listClients = listClients;
|
|
15
20
|
exports.getRates = getRates;
|
|
@@ -21,6 +26,7 @@ exports.forgetPublication = forgetPublication;
|
|
|
21
26
|
exports.listPublications = listPublications;
|
|
22
27
|
exports.authorsByRepo = authorsByRepo;
|
|
23
28
|
exports.projectForRepo = projectForRepo;
|
|
29
|
+
exports.ownerOfRepo = ownerOfRepo;
|
|
24
30
|
exports.allProjectRepoPaths = allProjectRepoPaths;
|
|
25
31
|
exports.saveTurns = saveTurns;
|
|
26
32
|
exports.quotaTurns = quotaTurns;
|
|
@@ -155,6 +161,7 @@ function getProject(db, id) {
|
|
|
155
161
|
aiCostPolicy: row["ai_cost_policy"],
|
|
156
162
|
kind: row["kind"] ?? "client",
|
|
157
163
|
closedAt: row["closed_at"] ? new Date(row["closed_at"]) : null,
|
|
164
|
+
archivedAt: row["archived_at"] ? new Date(row["archived_at"]) : null,
|
|
158
165
|
tracker: row["tracker"] ? JSON.parse(row["tracker"]) : null,
|
|
159
166
|
};
|
|
160
167
|
}
|
|
@@ -183,8 +190,45 @@ function closeProject(db, id, at = new Date()) {
|
|
|
183
190
|
function reopenProject(db, id) {
|
|
184
191
|
db.prepare("UPDATE projects SET closed_at = NULL WHERE id = ?").run(id);
|
|
185
192
|
}
|
|
193
|
+
/** Archiva un proyecto: sale del panel y deja de captar. No borra nada. */
|
|
194
|
+
function archiveProject(db, id, at = new Date()) {
|
|
195
|
+
db.prepare("UPDATE projects SET archived_at = ? WHERE id = ? AND archived_at IS NULL").run(iso(at), id);
|
|
196
|
+
}
|
|
197
|
+
function restoreProject(db, id) {
|
|
198
|
+
db.prepare("UPDATE projects SET archived_at = NULL WHERE id = ?").run(id);
|
|
199
|
+
}
|
|
200
|
+
function listArchivedProjects(db) {
|
|
201
|
+
const rows = db.prepare("SELECT id FROM projects WHERE archived_at IS NOT NULL ORDER BY name").all();
|
|
202
|
+
return rows.map((r) => getProject(db, r.id));
|
|
203
|
+
}
|
|
204
|
+
/** Por qué no se puede eliminar un proyecto, o null si se puede. */
|
|
205
|
+
function projectDeleteBlocker(db, id) {
|
|
206
|
+
const count = (sql) => db.prepare(sql).get(id).n;
|
|
207
|
+
if (count("SELECT COUNT(*) AS n FROM invoices WHERE project_id = ?") > 0)
|
|
208
|
+
return "invoiced";
|
|
209
|
+
if (count("SELECT COUNT(*) AS n FROM time_entries WHERE project_id = ? AND invoice_id IS NOT NULL") > 0)
|
|
210
|
+
return "invoiced";
|
|
211
|
+
// Lo que hay fuera de esta máquina se quedaría sin dueño aquí.
|
|
212
|
+
if (count("SELECT COUNT(*) AS n FROM publications WHERE project_id = ?") > 0)
|
|
213
|
+
return "shared";
|
|
214
|
+
if (count("SELECT COUNT(*) AS n FROM project_sync WHERE project_id = ?") > 0)
|
|
215
|
+
return "shared";
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Elimina un proyecto y sus imputaciones, tarifas y vínculos. Los turnos y los
|
|
220
|
+
* commits se quedan: son de la máquina, no del proyecto, y si el repo se vuelve
|
|
221
|
+
* a registrar sus horas reaparecen. Lo facturado o compartido no se elimina.
|
|
222
|
+
*/
|
|
223
|
+
function deleteProject(db, id) {
|
|
224
|
+
const blocker = projectDeleteBlocker(db, id);
|
|
225
|
+
if (blocker)
|
|
226
|
+
throw new Error(`project ${id} cannot be deleted: ${blocker}`);
|
|
227
|
+
db.prepare("DELETE FROM projects WHERE id = ?").run(id);
|
|
228
|
+
}
|
|
229
|
+
/** Los proyectos a la vista. Los archivados, con `listArchivedProjects`. */
|
|
186
230
|
function listProjects(db) {
|
|
187
|
-
const rows = db.prepare("SELECT id FROM projects ORDER BY name").all();
|
|
231
|
+
const rows = db.prepare("SELECT id FROM projects WHERE archived_at IS NULL ORDER BY name").all();
|
|
188
232
|
return rows.map((r) => getProject(db, r.id)).filter(Boolean);
|
|
189
233
|
}
|
|
190
234
|
function listClients(db) {
|
|
@@ -299,8 +343,18 @@ function authorsByRepo(db) {
|
|
|
299
343
|
}
|
|
300
344
|
/** Encuentra el proyecto al que pertenece una ruta de repositorio. */
|
|
301
345
|
function projectForRepo(db, repoPath) {
|
|
302
|
-
|
|
303
|
-
|
|
346
|
+
return matchRepo(db, repoPath, `SELECT r.project_id, r.repo_path FROM project_repos r
|
|
347
|
+
JOIN projects p ON p.id = r.project_id WHERE p.archived_at IS NULL`);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Como `projectForRepo`, contando los archivados. Para lo que ofrece registrar
|
|
351
|
+
* un repo: uno archivado ya tiene dueño, y ofrecerlo otra vez lo resucitaría.
|
|
352
|
+
*/
|
|
353
|
+
function ownerOfRepo(db, repoPath) {
|
|
354
|
+
return matchRepo(db, repoPath, "SELECT project_id, repo_path FROM project_repos");
|
|
355
|
+
}
|
|
356
|
+
function matchRepo(db, repoPath, sql) {
|
|
357
|
+
const rows = db.prepare(sql).all();
|
|
304
358
|
// El prefijo más largo gana: permite anidar un subproyecto dentro de un monorepo.
|
|
305
359
|
// isSameOrInside y no startsWith: en Windows la misma carpeta llega con \ o
|
|
306
360
|
// con /, y con la unidad en mayúscula o minúscula.
|
|
@@ -951,7 +1005,8 @@ function listTeamSyncedProjects(db) {
|
|
|
951
1005
|
function listPersonalSyncCandidates(db) {
|
|
952
1006
|
const rows = db.prepare(`
|
|
953
1007
|
SELECT id FROM projects
|
|
954
|
-
WHERE
|
|
1008
|
+
WHERE archived_at IS NULL
|
|
1009
|
+
AND id NOT IN (SELECT project_id FROM project_sync WHERE scope = 'team')
|
|
955
1010
|
`).all();
|
|
956
1011
|
return rows.map((r) => r.id);
|
|
957
1012
|
}
|
package/dist/i18n/en.js
CHANGED
|
@@ -544,6 +544,11 @@ exports.EN = {
|
|
|
544
544
|
" {0} · {1}: {2} recuperables. Revísalo con: estela entries --project {3}": " {0} · {1}: {2} recoverable. Review it with: estela entries --project {3}",
|
|
545
545
|
" {0} imputaciones retiradas: sus horas ya estaban en otra rama del mismo día": " {0} entries removed: their hours were already in another branch that day",
|
|
546
546
|
"\n{0} imputaciones aprobadas o ajustadas repiten horas de otra rama. Revísalas antes de facturar:": "\n{0} approved or adjusted entries repeat hours from another branch. Review them before invoicing:",
|
|
547
|
+
"Archiva el proyecto antes de eliminarlo.": "Archive the project before deleting it.",
|
|
548
|
+
"El nombre no coincide: no se ha eliminado nada.": "The name doesn't match: nothing was deleted.",
|
|
549
|
+
"Tiene horas facturadas: se puede archivar, pero no eliminar.": "It has invoiced hours: it can be archived, but not deleted.",
|
|
550
|
+
"Está publicado o sincronizado: se puede archivar, pero no eliminar.": "It's published or synced: it can be archived, but not deleted.",
|
|
551
|
+
"Ya hay un proyecto archivado con ese nombre: restáuralo.": "There's already an archived project with that name: restore it.",
|
|
547
552
|
" {0} recuperadas de sesiones en subcarpetas y worktrees": " {0} recovered from sessions in subfolders and worktrees",
|
|
548
553
|
"{0} (sin proyecto)": "{0} (no project)",
|
|
549
554
|
// ── publish: resumen antes de subir, y unpublish ──
|
package/dist/server.js
CHANGED
|
@@ -65,6 +65,8 @@ const assign_js_1 = require("./billing/assign.js");
|
|
|
65
65
|
const quota_js_1 = require("./metrics/quota.js");
|
|
66
66
|
const quota_js_2 = require("./quota.js");
|
|
67
67
|
const labels_js_1 = require("./i18n/labels.js");
|
|
68
|
+
/** Las imputaciones de un proyecto archivado no salen en ninguna vista del panel. */
|
|
69
|
+
const VISIBLE = "project_id NOT IN (SELECT id FROM projects WHERE archived_at IS NOT NULL)";
|
|
68
70
|
/**
|
|
69
71
|
* Servidor local del dashboard.
|
|
70
72
|
*
|
|
@@ -323,7 +325,7 @@ async function api(req, res, url, dbPath) {
|
|
|
323
325
|
const projectId = url.searchParams.get("project");
|
|
324
326
|
const rows = db.prepare(`
|
|
325
327
|
SELECT local_date AS d, COUNT(*) AS n FROM time_entries
|
|
326
|
-
WHERE approved = 0 AND invoice_id IS NULL
|
|
328
|
+
WHERE approved = 0 AND invoice_id IS NULL AND ${VISIBLE}
|
|
327
329
|
${projectId ? "AND project_id = ?" : ""}
|
|
328
330
|
GROUP BY local_date ORDER BY local_date DESC
|
|
329
331
|
`).all(...(projectId ? [projectId] : []));
|
|
@@ -352,6 +354,49 @@ async function api(req, res, url, dbPath) {
|
|
|
352
354
|
const body = await readJson(req);
|
|
353
355
|
return json(res, 200, { id: createProject(db, body) });
|
|
354
356
|
}
|
|
357
|
+
if (path === "/api/project-archive" && req.method === "POST") {
|
|
358
|
+
const body = await readJson(req);
|
|
359
|
+
const projectId = String(body["projectId"] ?? "");
|
|
360
|
+
if (!store.getProject(db, projectId)) {
|
|
361
|
+
return json(res, 404, { error: (0, index_js_2.tr) `No existe el proyecto "${projectId}".` });
|
|
362
|
+
}
|
|
363
|
+
store.archiveProject(db, projectId);
|
|
364
|
+
return json(res, 200, { ok: true });
|
|
365
|
+
}
|
|
366
|
+
if (path === "/api/project-restore" && req.method === "POST") {
|
|
367
|
+
const body = await readJson(req);
|
|
368
|
+
const projectId = String(body["projectId"] ?? "");
|
|
369
|
+
if (!store.getProject(db, projectId)) {
|
|
370
|
+
return json(res, 404, { error: (0, index_js_2.tr) `No existe el proyecto "${projectId}".` });
|
|
371
|
+
}
|
|
372
|
+
store.restoreProject(db, projectId);
|
|
373
|
+
// Lo trabajado mientras estuvo archivado sigue en los turnos: se imputa ya.
|
|
374
|
+
const status = await runImport(dbPath);
|
|
375
|
+
return json(res, 200, { ok: true, importing: status.pending === true });
|
|
376
|
+
}
|
|
377
|
+
/* Borrar no tiene vuelta atrás: se exige haberlo archivado antes y escribir
|
|
378
|
+
su nombre, también aquí y no solo en la página. */
|
|
379
|
+
if (path === "/api/project-delete" && req.method === "POST") {
|
|
380
|
+
const body = await readJson(req);
|
|
381
|
+
const projectId = String(body["projectId"] ?? "");
|
|
382
|
+
const project = store.getProject(db, projectId);
|
|
383
|
+
if (!project)
|
|
384
|
+
return json(res, 404, { error: (0, index_js_2.tr) `No existe el proyecto "${projectId}".` });
|
|
385
|
+
if (!project.archivedAt)
|
|
386
|
+
return json(res, 409, { error: (0, index_js_2.tr) `Archiva el proyecto antes de eliminarlo.` });
|
|
387
|
+
if (String(body["confirmName"] ?? "") !== project.name) {
|
|
388
|
+
return json(res, 400, { error: (0, index_js_2.tr) `El nombre no coincide: no se ha eliminado nada.` });
|
|
389
|
+
}
|
|
390
|
+
const blocker = store.projectDeleteBlocker(db, projectId);
|
|
391
|
+
if (blocker === "invoiced") {
|
|
392
|
+
return json(res, 409, { error: (0, index_js_2.tr) `Tiene horas facturadas: se puede archivar, pero no eliminar.` });
|
|
393
|
+
}
|
|
394
|
+
if (blocker === "shared") {
|
|
395
|
+
return json(res, 409, { error: (0, index_js_2.tr) `Está publicado o sincronizado: se puede archivar, pero no eliminar.` });
|
|
396
|
+
}
|
|
397
|
+
store.deleteProject(db, projectId);
|
|
398
|
+
return json(res, 200, { ok: true });
|
|
399
|
+
}
|
|
355
400
|
/* Con quién está compartido cada panel. El panel local no habla con la
|
|
356
401
|
base de la nube: pregunta a la API con el token de este dispositivo,
|
|
357
402
|
igual que hace para publicar. Sin cuenta vinculada no hay nada que
|
|
@@ -627,6 +672,7 @@ function buildOverview(db, from = "", to = "") {
|
|
|
627
672
|
SUM(ai_micro_usd) AS micro,
|
|
628
673
|
SUM(approved) AS approved
|
|
629
674
|
FROM time_entries
|
|
675
|
+
WHERE ${VISIBLE}
|
|
630
676
|
GROUP BY date
|
|
631
677
|
ORDER BY date DESC
|
|
632
678
|
LIMIT 30
|
|
@@ -649,7 +695,7 @@ function buildOverview(db, from = "", to = "") {
|
|
|
649
695
|
SELECT project_id, SUM(seconds) AS seconds, SUM(ai_micro_usd) AS micro, COUNT(*) AS blocks,
|
|
650
696
|
MIN(local_date) AS first_day, MAX(local_date) AS last_day
|
|
651
697
|
FROM time_entries
|
|
652
|
-
WHERE billable = 1 AND invoice_id IS NULL
|
|
698
|
+
WHERE billable = 1 AND invoice_id IS NULL AND ${VISIBLE}
|
|
653
699
|
${ranged ? "AND local_date >= ? AND local_date <= ?" : ""}
|
|
654
700
|
GROUP BY project_id
|
|
655
701
|
`).all(...(ranged ? [from, to] : []));
|
|
@@ -678,7 +724,7 @@ function buildOverview(db, from = "", to = "") {
|
|
|
678
724
|
? db.prepare(`
|
|
679
725
|
SELECT COALESCE(SUM(seconds),0) AS seconds, COUNT(*) AS blocks
|
|
680
726
|
FROM time_entries
|
|
681
|
-
WHERE billable = 1 AND invoice_id IS NULL
|
|
727
|
+
WHERE billable = 1 AND invoice_id IS NULL AND ${VISIBLE}
|
|
682
728
|
AND (local_date < ? OR local_date > ?)
|
|
683
729
|
`).get(from, to)
|
|
684
730
|
: { seconds: 0, blocks: 0 };
|
|
@@ -713,7 +759,7 @@ function buildSummary(db, from, to) {
|
|
|
713
759
|
MAX(local_date) AS last_day,
|
|
714
760
|
COUNT(DISTINCT local_date) AS days
|
|
715
761
|
FROM time_entries
|
|
716
|
-
WHERE local_date >= ? AND local_date <= ?
|
|
762
|
+
WHERE local_date >= ? AND local_date <= ? AND ${VISIBLE}
|
|
717
763
|
GROUP BY project_id
|
|
718
764
|
`).all(from, to);
|
|
719
765
|
const projects = new Map(store.listProjects(db).map((p) => [p.id, p]));
|
|
@@ -753,7 +799,7 @@ function buildSummary(db, from, to) {
|
|
|
753
799
|
const byDay = db.prepare(`
|
|
754
800
|
SELECT local_date AS date, SUM(seconds) AS seconds
|
|
755
801
|
FROM time_entries
|
|
756
|
-
WHERE local_date >= ? AND local_date <= ?
|
|
802
|
+
WHERE local_date >= ? AND local_date <= ? AND ${VISIBLE}
|
|
757
803
|
GROUP BY local_date ORDER BY local_date
|
|
758
804
|
`).all(from, to);
|
|
759
805
|
const totalSeconds = items.reduce((s, i) => s + i.seconds, 0);
|
|
@@ -820,7 +866,7 @@ async function buildTeam(db, from, to, projectId) {
|
|
|
820
866
|
// de qué fiarse, y ninguna otra cifra de la pantalla lo dice.
|
|
821
867
|
const composition = db.prepare(`
|
|
822
868
|
SELECT project_id, source, SUM(seconds) AS seconds
|
|
823
|
-
FROM time_entries WHERE local_date >= ? AND local_date <= ?
|
|
869
|
+
FROM time_entries WHERE local_date >= ? AND local_date <= ? AND ${VISIBLE}
|
|
824
870
|
GROUP BY project_id, source
|
|
825
871
|
`).all(from, to);
|
|
826
872
|
const byProject = new Map();
|
|
@@ -934,7 +980,7 @@ function buildDay(db, date) {
|
|
|
934
980
|
SELECT t.*, i.number AS invoice_number
|
|
935
981
|
FROM time_entries t
|
|
936
982
|
LEFT JOIN invoices i ON i.id = t.invoice_id
|
|
937
|
-
WHERE t.local_date = ?
|
|
983
|
+
WHERE t.local_date = ? AND t.${VISIBLE}
|
|
938
984
|
ORDER BY t.started_at
|
|
939
985
|
`).all(date);
|
|
940
986
|
const projects = new Map(store.listProjects(db).map((p) => [p.id, p]));
|
|
@@ -1174,6 +1220,14 @@ function buildProjects(db) {
|
|
|
1174
1220
|
staleBlocks: pub.staleBlocks,
|
|
1175
1221
|
daysAgo: Math.floor((Date.now() - pub.publishedAt.getTime()) / 86_400_000),
|
|
1176
1222
|
})),
|
|
1223
|
+
archived: store.listArchivedProjects(db).map((p) => {
|
|
1224
|
+
const totals = db.prepare("SELECT COUNT(*) AS blocks, SUM(seconds) AS seconds FROM time_entries WHERE project_id = ?").get(p.id);
|
|
1225
|
+
return {
|
|
1226
|
+
id: p.id, name: p.name, clientName: clients.get(p.clientId)?.name ?? "—",
|
|
1227
|
+
blocks: totals.blocks, seconds: totals.seconds ?? 0,
|
|
1228
|
+
deleteBlocker: store.projectDeleteBlocker(db, p.id),
|
|
1229
|
+
};
|
|
1230
|
+
}),
|
|
1177
1231
|
projects: store.listProjects(db).map((p) => {
|
|
1178
1232
|
const client = clients.get(p.clientId);
|
|
1179
1233
|
const rates = store.getRates(db, p.id);
|
|
@@ -1196,6 +1250,7 @@ function buildProjects(db) {
|
|
|
1196
1250
|
hourlyMinor: current?.amount ?? null,
|
|
1197
1251
|
blocks: totals.blocks,
|
|
1198
1252
|
seconds: totals.seconds ?? 0,
|
|
1253
|
+
unbilledSeconds: db.prepare("SELECT COALESCE(SUM(seconds), 0) AS s FROM time_entries WHERE project_id = ? AND billable = 1 AND invoice_id IS NULL").get(p.id).s,
|
|
1199
1254
|
// El historial completo: subir la tarifa no reescribe lo ya trabajado,
|
|
1200
1255
|
// así que conviene poder verlo.
|
|
1201
1256
|
rateHistory: rates.map((r) => ({
|
|
@@ -1216,7 +1271,7 @@ function unassignedRepos(db) {
|
|
|
1216
1271
|
GROUP BY repo_path ORDER BY turns DESC
|
|
1217
1272
|
`).all();
|
|
1218
1273
|
return rows
|
|
1219
|
-
.filter((r) => store.
|
|
1274
|
+
.filter((r) => store.ownerOfRepo(db, r.repo_path) === null)
|
|
1220
1275
|
.slice(0, 40)
|
|
1221
1276
|
.map((r) => ({
|
|
1222
1277
|
path: r.repo_path,
|
|
@@ -1247,6 +1302,10 @@ function createProject(db, body) {
|
|
|
1247
1302
|
}
|
|
1248
1303
|
}
|
|
1249
1304
|
const id = String(body["id"] ?? "").trim() || slug(name);
|
|
1305
|
+
// Volver a crearlo no lo sacaría del archivo: quedaría oculto y sin explicación.
|
|
1306
|
+
if (store.getProject(db, id)?.archivedAt) {
|
|
1307
|
+
throw new Error((0, index_js_2.tr) `Ya hay un proyecto archivado con ese nombre: restáuralo.`);
|
|
1308
|
+
}
|
|
1250
1309
|
const repo = String(body["repoPath"] ?? "").trim();
|
|
1251
1310
|
const kind = (String(body["kind"] ?? "client"));
|
|
1252
1311
|
store.upsertProject(db, {
|
package/dist/setup.js
CHANGED
|
@@ -96,7 +96,7 @@ async function planSetup(db, repoPaths, myEmailGuess) {
|
|
|
96
96
|
}
|
|
97
97
|
const plans = [];
|
|
98
98
|
for (const root of roots) {
|
|
99
|
-
const already = store.
|
|
99
|
+
const already = store.ownerOfRepo(db, root);
|
|
100
100
|
const { id, name } = projectFromRepo(root);
|
|
101
101
|
if (already) {
|
|
102
102
|
plans.push({ repoPath: root, projectId: already, name, emails: [], existing: true });
|
|
@@ -211,6 +211,11 @@ export interface Project {
|
|
|
211
211
|
* — `estela doctor` avisa en vez de tragárselo en silencio.
|
|
212
212
|
*/
|
|
213
213
|
readonly closedAt: Date | null;
|
|
214
|
+
/**
|
|
215
|
+
* Cuándo se archivó, o `null`/ausente si está activo. A diferencia de cerrar,
|
|
216
|
+
* archivar lo quita del panel y deja de captar horas. Se puede restaurar.
|
|
217
|
+
*/
|
|
218
|
+
readonly archivedAt?: Date | null;
|
|
214
219
|
/**
|
|
215
220
|
* El gestor de tareas del proyecto (Jira, Azure Boards, GitHub...), para
|
|
216
221
|
* reconocer sus tickets en ramas y commits. Sin él solo se reconoce lo
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AA+Ca,QAAA,WAAW,GAAe;IACrC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC;CACpE,CAAC;AAuKW,QAAA,mBAAmB,GAAgC;IAC9D,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,QAAQ;IACpB,QAAQ,EAAE,SAAS;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AA+Ca,QAAA,WAAW,GAAe;IACrC,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC;CACpE,CAAC;AAuKW,QAAA,mBAAmB,GAAgC;IAC9D,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,QAAQ;IACpB,QAAQ,EAAE,SAAS;CACpB,CAAC;AAmEW,QAAA,mBAAmB,GAAgC;IAC9D,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAAE,gBAAgB;CAC7E,CAAC;AAmBW,QAAA,gBAAgB,GAA6B;IACxD,WAAW,EAAE,YAAY;IACzB,OAAO,EAAE,SAAS;IAClB,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,UAAU;IAClB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,MAAM;CACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "estela",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Time tracking you never fill in: rebuilds your billable hours from Claude Code, Codex and GitHub Copilot sessions and your Git history, and splits what the AI cost per project.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"time-tracking",
|
package/web/app.css
CHANGED
|
@@ -639,6 +639,10 @@ main.wide { max-width: 1180px; }
|
|
|
639
639
|
font-variant-numeric: tabular-nums; white-space: nowrap;
|
|
640
640
|
}
|
|
641
641
|
.pcard-warn { font-size: .85rem; color: var(--amber); margin-top: 10px; }
|
|
642
|
+
.pcard-actions { display: flex; justify-content: flex-end; margin-top: 10px; }
|
|
643
|
+
.archived { margin: 16px 0; }
|
|
644
|
+
.archived summary { cursor: pointer; color: var(--muted, inherit); }
|
|
645
|
+
.archived-row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 10px; padding: 10px 0; }
|
|
642
646
|
.repos { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
|
|
643
647
|
.repos code {
|
|
644
648
|
font-family: ui-monospace, Menlo, monospace; font-size: .76rem; color: var(--ink-3);
|
package/web/app.js
CHANGED
|
@@ -1010,6 +1010,10 @@ function renderProjects(data, orphans) {
|
|
|
1010
1010
|
<input type="date" class="rate-from" value="${localToday()}">
|
|
1011
1011
|
<button type="submit" class="chip">${tr("common.save")}</button>
|
|
1012
1012
|
</form>
|
|
1013
|
+
<div class="pcard-actions">
|
|
1014
|
+
<button type="button" class="chip project-archive" data-project="${esc(p.id)}"
|
|
1015
|
+
data-name="${esc(p.name)}" data-unbilled="${p.unbilledSeconds}">${tr("projects.archive")}</button>
|
|
1016
|
+
</div>
|
|
1013
1017
|
</div>`);
|
|
1014
1018
|
}
|
|
1015
1019
|
|
|
@@ -1018,6 +1022,27 @@ function renderProjects(data, orphans) {
|
|
|
1018
1022
|
<p>${tr("projects.empty.body")}</p></div>`);
|
|
1019
1023
|
}
|
|
1020
1024
|
|
|
1025
|
+
// Lo archivado queda a mano y plegado: no estorba, pero se puede recuperar.
|
|
1026
|
+
if ((data.archived || []).length) {
|
|
1027
|
+
parts.push(`<details class="archived">
|
|
1028
|
+
<summary>${tr("projects.archived.title", { n: data.archived.length })}</summary>
|
|
1029
|
+
${data.archived.map((p) => `<div class="archived-row">
|
|
1030
|
+
<div>
|
|
1031
|
+
<div class="pcard-name">${esc(p.name)}</div>
|
|
1032
|
+
<div class="pcard-meta">${tr("projects.meta", {
|
|
1033
|
+
client: esc(p.clientName), duration: fmtDuration(p.seconds), blocks: tr("blocks", { n: p.blocks }),
|
|
1034
|
+
})}</div>
|
|
1035
|
+
${p.deleteBlocker ? `<div class="addform-hint">${tr(`projects.delete.blocked.${p.deleteBlocker}`)}</div>` : ""}
|
|
1036
|
+
</div>
|
|
1037
|
+
<div class="row-inline">
|
|
1038
|
+
<button type="button" class="chip project-restore" data-project="${esc(p.id)}">${tr("projects.restore")}</button>
|
|
1039
|
+
${p.deleteBlocker ? "" : `<button type="button" class="chip project-delete" data-project="${esc(p.id)}"
|
|
1040
|
+
data-name="${esc(p.name)}" data-blocks="${p.blocks}" data-seconds="${p.seconds}">${tr("projects.delete")}</button>`}
|
|
1041
|
+
</div>
|
|
1042
|
+
</div>`).join("")}
|
|
1043
|
+
</details>`);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1021
1046
|
// Repos con trabajo capturado que no pertenecen a nadie. Es la lista más
|
|
1022
1047
|
// útil de la pantalla: son horas que ya tienes pero no puedes facturar.
|
|
1023
1048
|
// Se presentan como tarjetas y no como un desplegable porque rellenar un
|
|
@@ -1153,6 +1178,48 @@ function wireProjects() {
|
|
|
1153
1178
|
});
|
|
1154
1179
|
|
|
1155
1180
|
// Copiar el enlace, sin salir de aquí.
|
|
1181
|
+
document.querySelectorAll(".project-archive").forEach((btn) => {
|
|
1182
|
+
btn.addEventListener("click", async () => {
|
|
1183
|
+
const unbilled = Number(btn.dataset.unbilled);
|
|
1184
|
+
const aviso = unbilled > 0 ? tr("projects.archive.unbilled", { duration: fmtDuration(unbilled) }) : "";
|
|
1185
|
+
if (!window.confirm(tr("projects.archive.confirm", { name: btn.dataset.name }) + aviso)) return;
|
|
1186
|
+
try {
|
|
1187
|
+
await api("/api/project-archive", { method: "POST", body: JSON.stringify({ projectId: btn.dataset.project }) });
|
|
1188
|
+
await loadProjects();
|
|
1189
|
+
toast(tr("toast.projectArchived"));
|
|
1190
|
+
} catch (error) { toast(error.message); }
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
document.querySelectorAll(".project-restore").forEach((btn) => {
|
|
1195
|
+
btn.addEventListener("click", async () => {
|
|
1196
|
+
btn.disabled = true;
|
|
1197
|
+
try {
|
|
1198
|
+
await api("/api/project-restore", { method: "POST", body: JSON.stringify({ projectId: btn.dataset.project }) });
|
|
1199
|
+
await loadProjects();
|
|
1200
|
+
toast(tr("toast.projectRestored"));
|
|
1201
|
+
} catch (error) { toast(error.message); btn.disabled = false; }
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// Eliminar no tiene vuelta atrás: se escribe el nombre, como en cualquier
|
|
1206
|
+
// herramienta donde un clic de más cuesta datos.
|
|
1207
|
+
document.querySelectorAll(".project-delete").forEach((btn) => {
|
|
1208
|
+
btn.addEventListener("click", async () => {
|
|
1209
|
+
const escrito = window.prompt(tr("projects.delete.confirm", {
|
|
1210
|
+
name: btn.dataset.name, duration: fmtDuration(Number(btn.dataset.seconds)),
|
|
1211
|
+
blocks: tr("blocks", { n: Number(btn.dataset.blocks) }),
|
|
1212
|
+
}));
|
|
1213
|
+
if (escrito === null) return;
|
|
1214
|
+
try {
|
|
1215
|
+
await api("/api/project-delete", { method: "POST",
|
|
1216
|
+
body: JSON.stringify({ projectId: btn.dataset.project, confirmName: escrito }) });
|
|
1217
|
+
await loadProjects();
|
|
1218
|
+
toast(tr("toast.projectDeleted"));
|
|
1219
|
+
} catch (error) { toast(error.message); }
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1156
1223
|
// Vincular un repositorio ya detectado a un proyecto que existe. Es lo que
|
|
1157
1224
|
// le faltaba a quien acepta una invitación: el proyecto queda creado pero
|
|
1158
1225
|
// vacío, y la única salida era la terminal.
|
package/web/i18n.js
CHANGED
|
@@ -226,6 +226,18 @@
|
|
|
226
226
|
"projects.noRepo": "Sin repositorio: no se le imputa nada todavía.",
|
|
227
227
|
"projects.orphanOption": "{name} — {turns} turnos, hasta {last}",
|
|
228
228
|
"projects.link": "Vincular",
|
|
229
|
+
"projects.archive": "Archivar",
|
|
230
|
+
"projects.archive.confirm": "¿Archivar \"{name}\"? Saldrá del panel y dejará de captar horas. No se borra nada y puedes restaurarlo.",
|
|
231
|
+
"projects.archive.unbilled": "\n\nTiene {duration} sin facturar: se conservan, pero no podrás facturarlas hasta restaurarlo.",
|
|
232
|
+
"projects.archived.title": "Archivados ({n})",
|
|
233
|
+
"projects.restore": "Restaurar",
|
|
234
|
+
"projects.delete": "Eliminar",
|
|
235
|
+
"projects.delete.confirm": "Se eliminarán \"{name}\" y sus {blocks} ({duration}), y no se puede deshacer. Las sesiones y los commits se conservan.\n\nEscribe el nombre del proyecto para confirmar:",
|
|
236
|
+
"projects.delete.blocked.invoiced": "Tiene horas facturadas: no se puede eliminar.",
|
|
237
|
+
"projects.delete.blocked.shared": "Está publicado o sincronizado: no se puede eliminar.",
|
|
238
|
+
"toast.projectArchived": "Proyecto archivado",
|
|
239
|
+
"toast.projectRestored": "Proyecto restaurado",
|
|
240
|
+
"toast.projectDeleted": "Proyecto eliminado",
|
|
229
241
|
"projects.noOrphans": "No hemos detectado ningún repositorio sin asignar. Trabaja un rato con tu agente y vuelve, o vincúlalo con <code>estela team repo --project {id} --add <ruta></code>.",
|
|
230
242
|
"projects.empty.title": "Todavía no hay proyectos",
|
|
231
243
|
"projects.empty.body": "Crea el primero abajo y asígnale el repositorio donde trabajas.",
|
|
@@ -532,6 +544,18 @@
|
|
|
532
544
|
"projects.noRepo": "No repository: nothing is assigned to it yet.",
|
|
533
545
|
"projects.orphanOption": "{name} — {turns} turns, until {last}",
|
|
534
546
|
"projects.link": "Link",
|
|
547
|
+
"projects.archive": "Archive",
|
|
548
|
+
"projects.archive.confirm": "Archive \"{name}\"? It will leave the panel and stop capturing hours. Nothing is deleted and you can restore it.",
|
|
549
|
+
"projects.archive.unbilled": "\n\nIt has {duration} not invoiced: they're kept, but you can't invoice them until you restore it.",
|
|
550
|
+
"projects.archived.title": "Archived ({n})",
|
|
551
|
+
"projects.restore": "Restore",
|
|
552
|
+
"projects.delete": "Delete",
|
|
553
|
+
"projects.delete.confirm": "This deletes \"{name}\" and its {blocks} ({duration}), and can't be undone. Sessions and commits are kept.\n\nType the project name to confirm:",
|
|
554
|
+
"projects.delete.blocked.invoiced": "It has invoiced hours: it can't be deleted.",
|
|
555
|
+
"projects.delete.blocked.shared": "It's published or synced: it can't be deleted.",
|
|
556
|
+
"toast.projectArchived": "Project archived",
|
|
557
|
+
"toast.projectRestored": "Project restored",
|
|
558
|
+
"toast.projectDeleted": "Project deleted",
|
|
535
559
|
"projects.noOrphans": "We haven't detected any unassigned repository. Work with your agent for a while and come back, or link it with <code>estela team repo --project {id} --add <path></code>.",
|
|
536
560
|
"projects.empty.title": "No projects yet",
|
|
537
561
|
"projects.empty.body": "Create the first one below and assign it the repository you work in.",
|