fix: statistiques vides en production — normalise les dates migrées
Les données migrées depuis "Minuteur Dragodinde" stockaient les dates comme timestamps numériques, causant un crash sur `.slice()`. Ajout de normalizeDate(), gardes défensives, migration robuste et fallback d'erreur visible dans l'UI. Bump v1.1.7. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
61bbac0adc
commit
3555242c84
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidienne",
|
"name": "obsidienne",
|
||||||
"version": "1.1.6",
|
"version": "1.1.7",
|
||||||
"description": "Obsidienne — Minuteur d'élevage Dragodinde pour Dofus 3",
|
"description": "Obsidienne — Minuteur d'élevage Dragodinde pour Dofus 3",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"author": "Mickael",
|
"author": "Mickael",
|
||||||
|
|||||||
@ -88,6 +88,15 @@ function toISO(d: Date): string {
|
|||||||
return d.toISOString().slice(0, 10);
|
return d.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Normalise une date (string ISO, timestamp number, Date object) en string "YYYY-MM-DD". */
|
||||||
|
function normalizeDate(raw: unknown): string {
|
||||||
|
if (!raw) return '';
|
||||||
|
if (typeof raw === 'string') return raw.slice(0, 10);
|
||||||
|
if (typeof raw === 'number') return new Date(raw).toISOString().slice(0, 10);
|
||||||
|
if (raw instanceof Date) return raw.toISOString().slice(0, 10);
|
||||||
|
return String(raw).slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
function aggregate(entries: AccEntry[]) {
|
function aggregate(entries: AccEntry[]) {
|
||||||
let couples = 0, babies = 0;
|
let couples = 0, babies = 0;
|
||||||
const races = new Set<string>();
|
const races = new Set<string>();
|
||||||
@ -102,37 +111,61 @@ function aggregate(entries: AccEntry[]) {
|
|||||||
|
|
||||||
const WEEKDAY_NAMES = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
|
const WEEKDAY_NAMES = ['Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'];
|
||||||
|
|
||||||
|
function emptyResult(days: number): StatisticsResult {
|
||||||
|
const kpi0: KpiDelta = { value: 0, delta: null };
|
||||||
|
const missingRaces: MissingRace[] = Object.entries(RACE_GEN)
|
||||||
|
.filter(([, gen]) => gen !== 1)
|
||||||
|
.map(([name, gen]) => ({ name, gen }))
|
||||||
|
.sort((a, b) => a.gen - b.gen || a.name.localeCompare(b.name));
|
||||||
|
return {
|
||||||
|
totalBabies: kpi0, totalCouples: kpi0, successRate: kpi0, racesCount: kpi0,
|
||||||
|
dailyBirths: [], raceShares: [], raceSuccessRates: [], bestCouples: [],
|
||||||
|
genBreakdown: [], missingRaces, weekdayActivity: [], days,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createGetStatisticsHandler(state: AppState) {
|
export function createGetStatisticsHandler(state: AppState) {
|
||||||
return (query: GetStatisticsQuery): StatisticsResult => {
|
return (query: GetStatisticsQuery): StatisticsResult => {
|
||||||
const days = query.days ?? 30;
|
const days = query.days ?? 30;
|
||||||
|
try {
|
||||||
|
return computeStatistics(state, days);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('GetStatistics handler error:', e);
|
||||||
|
return emptyResult(days);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeStatistics(state: AppState, days: number): StatisticsResult {
|
||||||
// Collecter toutes les entrées normalisées (exclure Gen 1 : non créables, seulement capturables)
|
// Collecter toutes les entrées normalisées (exclure Gen 1 : non créables, seulement capturables)
|
||||||
const all: AccEntry[] = [];
|
const all: AccEntry[] = [];
|
||||||
for (const acc of state.accouplements) {
|
const accouplements = Array.isArray(state.accouplements) ? state.accouplements : [];
|
||||||
|
for (const acc of accouplements) {
|
||||||
|
if (!acc || !acc.baby) continue;
|
||||||
if (acc.gen === 1 || (RACE_GEN[acc.baby] ?? 0) === 1) continue;
|
if (acc.gen === 1 || (RACE_GEN[acc.baby] ?? 0) === 1) continue;
|
||||||
all.push({
|
all.push({
|
||||||
parentA: acc.parentA, parentB: acc.parentB,
|
parentA: acc.parentA ?? '', parentB: acc.parentB ?? '',
|
||||||
baby: acc.baby, gen: acc.gen,
|
baby: acc.baby, gen: acc.gen ?? (RACE_GEN[acc.baby] ?? 0),
|
||||||
couples: acc.couples, babiesObtained: acc.babiesObtained,
|
couples: Number(acc.couples) || 0, babiesObtained: Number(acc.babiesObtained) || 0,
|
||||||
date: acc.date,
|
date: normalizeDate(acc.date),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const arch of state.archivedStats as Array<{
|
const archivedStats = Array.isArray(state.archivedStats) ? state.archivedStats : [];
|
||||||
|
for (const arch of archivedStats as Array<{
|
||||||
parentA?: string; parentB?: string; baby?: string; gen?: number;
|
parentA?: string; parentB?: string; baby?: string; gen?: number;
|
||||||
couples?: number; babiesObtained?: number; date?: string;
|
couples?: number; babiesObtained?: number; date?: string;
|
||||||
}>) {
|
}>) {
|
||||||
if (arch.baby) {
|
if (!arch || !arch.baby) continue;
|
||||||
const gen = arch.gen ?? (RACE_GEN[arch.baby] ?? 0);
|
const gen = arch.gen ?? (RACE_GEN[arch.baby] ?? 0);
|
||||||
if (gen === 1) continue;
|
if (gen === 1) continue;
|
||||||
all.push({
|
all.push({
|
||||||
parentA: arch.parentA ?? '', parentB: arch.parentB ?? '',
|
parentA: arch.parentA ?? '', parentB: arch.parentB ?? '',
|
||||||
baby: arch.baby, gen,
|
baby: arch.baby, gen,
|
||||||
couples: arch.couples ?? 0,
|
couples: Number(arch.couples) || 0,
|
||||||
babiesObtained: arch.babiesObtained ?? 0,
|
babiesObtained: Number(arch.babiesObtained) || 0,
|
||||||
date: arch.date ?? '',
|
date: normalizeDate(arch.date),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const todayISO = toISO(now);
|
const todayISO = toISO(now);
|
||||||
@ -284,5 +317,4 @@ export function createGetStatisticsHandler(state: AppState) {
|
|||||||
weekdayActivity,
|
weekdayActivity,
|
||||||
days,
|
days,
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,7 +34,17 @@ if (process.env.ELECTRON_USER_DATA_DIR) {
|
|||||||
if (app.isPackaged) {
|
if (app.isPackaged) {
|
||||||
const oldDataFile = path.join(app.getPath('appData'), 'Minuteur Dragodinde', 'dd-timer-data.json');
|
const oldDataFile = path.join(app.getPath('appData'), 'Minuteur Dragodinde', 'dd-timer-data.json');
|
||||||
const newDataFile = path.join(app.getPath('userData'), 'dd-timer-data.json');
|
const newDataFile = path.join(app.getPath('userData'), 'dd-timer-data.json');
|
||||||
if (!fs.existsSync(newDataFile) && fs.existsSync(oldDataFile)) {
|
const shouldMigrate = (() => {
|
||||||
|
if (!fs.existsSync(oldDataFile)) return false;
|
||||||
|
if (!fs.existsSync(newDataFile)) return true;
|
||||||
|
// Si le nouveau fichier est quasi-vide (<200 octets = état par défaut), on remigre
|
||||||
|
try {
|
||||||
|
const newSize = fs.statSync(newDataFile).size;
|
||||||
|
const oldSize = fs.statSync(oldDataFile).size;
|
||||||
|
return newSize < 200 && oldSize > newSize;
|
||||||
|
} catch { return false; }
|
||||||
|
})();
|
||||||
|
if (shouldMigrate) {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(newDataFile), { recursive: true });
|
fs.mkdirSync(path.dirname(newDataFile), { recursive: true });
|
||||||
fs.copyFileSync(oldDataFile, newDataFile);
|
fs.copyFileSync(oldDataFile, newDataFile);
|
||||||
|
|||||||
@ -43,6 +43,25 @@ export class StatistiquesView {
|
|||||||
|
|
||||||
private renderAll(): void {
|
private renderAll(): void {
|
||||||
if (!this.el) return;
|
if (!this.el) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.renderAllInner();
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : String(e);
|
||||||
|
const stack = e instanceof Error ? e.stack ?? '' : '';
|
||||||
|
console.error('Erreur statistiques:', e);
|
||||||
|
this.el.innerHTML = `<div class="stats-hero">
|
||||||
|
<div>
|
||||||
|
<h2 class="stats-hero-title">Statistiques d'\u00c9levage</h2>
|
||||||
|
<p class="stats-hero-sub" style="color:#f87171">Erreur : ${esc(msg)}</p>
|
||||||
|
<pre style="color:#f87171;font-size:11px;white-space:pre-wrap;max-height:200px;overflow:auto;margin-top:8px">${esc(stack)}</pre>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAllInner(): void {
|
||||||
|
if (!this.el) return;
|
||||||
const data = this.getData();
|
const data = this.getData();
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user