Errores por pantalla + logs en archivo

This commit is contained in:
2026-03-16 16:08:13 +01:00
parent ed4866ad28
commit f076ee1b83
14 changed files with 351 additions and 98 deletions

View File

@@ -11,12 +11,14 @@ export default defineConfig({
"@renderer": resolve("src/renderer/src"),
},
},
plugins: [vue({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: tag => tag === "nfc-reader",
isCustomElement: (tag) => tag === "nfc-reader",
},
},
})],
}),
],
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 53 KiB

4
src/config/base.json Normal file
View File

@@ -0,0 +1,4 @@
{
"defultServer": "http://localhost:3000",
"defaultPrinter": "192.168.1.254"
}

74
src/main/LogService.ts Normal file
View File

@@ -0,0 +1,74 @@
import { app } from "electron";
import * as fs from "fs";
import * as path from "path";
export class LogService {
private static instance: LogService;
private logPath: string;
private constructor() {
// Create logs directory in userData
const userDataPath = app.getPath("userData");
const logsDir = path.join(userDataPath, "logs");
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Monthly log file
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const monthString = `${year}-${month}`; // YYYY-MM
this.logPath = path.join(logsDir, `app_${monthString}.log`);
this.info(`--- Log initialized at ${now.toISOString()} ---`);
console.log(`Log file: ${this.logPath}`);
}
/**
* Garantizar que sea singleton
* @returns
*/
public static getInstance(): LogService {
if (!LogService.instance) {
LogService.instance = new LogService();
}
return LogService.instance;
}
public info(message: string): void {
this.write("INFO", message);
}
public error(message: string, error?: unknown): void {
const errorDetail = error ? ` | Details: ${JSON.stringify(error)}` : "";
this.write("ERROR", `${message}${errorDetail}`);
}
public logOperation(
operation: string,
data: unknown,
result?: unknown,
): void {
const entry = {
operation,
data,
result,
timestamp: new Date().toISOString(),
};
this.write("OP", JSON.stringify(entry));
}
private write(level: string, message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] [${level}] ${message}\n`;
try {
fs.appendFileSync(this.logPath, logEntry, "utf8");
} catch (err) {
console.error("Failed to write to log file:", err);
}
}
}
export const logger = LogService.getInstance();

115
src/main/handlers.ts Normal file
View File

@@ -0,0 +1,115 @@
import { IpcMainInvokeEvent } from "electron";
import { Result } from "../types/Result";
import { CodeRequest, CodeResponse, PrinterRequest } from "../preload/index.d";
import { createConnection } from "net";
import { logger } from "./LogService";
/**
* LLamada al servidor para obtener la etiqueta
* @param event --
* @param data
* @returns
*/
export async function labelReqHandler(
event: IpcMainInvokeEvent,
data: CodeRequest,
): Promise<Result<string, CodeResponse>> {
logger.logOperation("labelReq", data);
console.log("nfc:labelReq", data);
try {
const endpoint = "/nfc/generate";
const url = data.serverURL + endpoint;
console.log("fullUrl = ", url);
const response = await fetch(data.serverURL + endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
card_id: data.card_id,
override: data.override,
}),
});
console.log("Response", response.status);
const body: CodeResponse = await response.json();
console.log("Codigos:", body);
logger.info(`labelReq success: ${JSON.stringify(body)}`);
// body puede tener otro error dentro
return <Result<string, CodeResponse>>{
data: body,
};
} catch (error: unknown) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const err = error as any;
logger.error("labelReq error", err);
console.error("IPC ERROR", err);
let errorStr = "";
if (String(err.cause).includes("ECONNRESET")) {
errorStr = "Conexión perdida con el servidor";
} else {
errorStr = "Error de red";
}
return <Result<string, CodeResponse>>{
error: errorStr,
};
}
}
/**
* Llamada principa a la impresora
* TODO:
* - Mejora de control de errores
*/
export async function printReqHandler(
_event: IpcMainInvokeEvent,
data: PrinterRequest,
): Promise<string> {
logger.logOperation("printReq", data);
return new Promise((res, rej) => {
console.log("nfc:printReq", data);
const port = 9100;
let finished = false;
const socket = createConnection(port, data.printerURL);
socket.setTimeout(3 * 1000);
socket.on("connect", (e) => {
console.log("Conectado!", e);
logger.info("Printer connected, sending label...");
socket.write(data.label, "utf-8", () => {
socket.end();
});
});
socket.on("close", () => {
if (finished) return;
finished = true;
logger.info("Print job sent successfully");
console.log("Print job sent and connection closed.");
res("ok");
});
socket.on("timeout", (err) => {
if (finished) return;
finished = true;
logger.error("Print timeout", err);
console.error("Timeout", err);
socket.destroy();
rej(new Error("timeout"));
});
socket.on("error", (err) => {
if (finished) return;
finished = true;
logger.error("Printer connection/transmission error", err);
let errorStr = "";
if (err.message.includes("ETIMEDOUT")) {
errorStr = "Error de conexión con la impresora";
rej(new Error(errorStr));
}
console.error("Printer Error:", err.message);
socket.destroy();
rej(err);
});
});
}

View File

@@ -3,7 +3,9 @@ import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import icon from "../../resources/icon.png?asset";
import { NfcService } from "../services/NfcService";
import net, { createConnection } from "net";
import { labelReqHandler, printReqHandler } from "./handlers";
import { logger } from "./LogService";
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
@@ -18,12 +20,16 @@ function createWindow(): void {
},
});
new NfcService((event) => {
const nfcService = new NfcService((event) => {
if (!mainWindow.isDestroyed()) {
mainWindow.webContents.send(`nfc:${event.type}`, event);
}
});
ipcMain.handle("nfc:getReaderName", () => {
return nfcService.getReaderName();
});
mainWindow.on("ready-to-show", () => {
mainWindow.show();
});
@@ -60,64 +66,13 @@ app.whenReady().then(() => {
ipcMain.on("ping", () => console.log("pong"));
// HANDLE es bidireccionar ON es unidireccional
ipcMain.handle("nfc:labelReq", async (_event, data) => {
console.log("nfc:labelReq", data);
try {
const endpoint = "/nfc/generate";
const url = data.serverURL + endpoint;
console.log("fullUrl = ", url);
const response = await fetch(data.serverURL + endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
card_id: data.card_id,
override: data.override,
}),
});
console.log("Response", response.status);
const body = await response.json();
console.log("Codigos:", body);
return body;
} catch (error) {
console.error(error);
return false;
}
return data;
});
ipcMain.handle("nfc:printReq", async (_event, data) => {
console.log("nfc:printReq", data);
const client = new net.Socket();
const port = 9100;
const host = data.printerURL + port;
const socket = createConnection(port, data.printerURL);
socket.setTimeout(10 * 1000);
socket.on("connect", (e) => {
console.log("Conectado!", e);
socket.write(data.label);
socket.end();
});
/*
client.connect(data.printerURL + port, () => {
console.log("Connected to ZSim Printer");
const res = client.write(data.label, (res) => {
console.log("resultado de la impresion", res);
});
console.log("Resultado", res);
});
/**
* LLamada principal
* Cli - card_id -> Server - etiqueta/activacion -> Cli
*/
socket.on("close", () =>
console.log("Print job sent and connection closed."),
);
socket.on("error", (err) =>
console.error("Printer Error:", err.message),
);
return data;
});
ipcMain.handle("nfc:labelReq", labelReqHandler);
ipcMain.handle("nfc:printReq", printReqHandler);
ipcMain.handle("ping:url", async (_event, url: string) => {
try {

View File

@@ -1,4 +1,5 @@
import { ElectronAPI } from "@electron-toolkit/preload";
import { Result } from "../types/Result.js";
export type CodeResponse = {
error?: string;
@@ -13,6 +14,7 @@ export type CodeResponse = {
export type CodeRequest = {
card_id: string; // UUIDv7
override?: boolean;
serverURL: string;
};
export type PrinterRequest = {
@@ -23,16 +25,14 @@ export type PrinterRequest = {
export type PrinterResponse = unknown;
export interface NfcAPI {
getReaderName: () => Promise<string>;
onReader: (callback: (event: { name: string }) => void) => void;
onTag: (callback: (event: { uid: string }) => void) => void;
onRemoved: (callback: (event: { uid: string }) => void) => void;
onError: (callback: (event: { message: string }) => void) => void;
ping: (url: string) => Promise<boolean>;
removeAllListeners: () => void;
labelReq: (args: {
serverURL: string;
card_id: string;
override?: boolean;
}) => Promise<CodeResponse>;
labelReq: (args: CodeRequest) => Promise<Result<string, CodeResponse>>;
printReq: (args: {
printerURL: string;
label: string;

View File

@@ -5,6 +5,12 @@ import { CodeRequest, PrinterRequest } from "./index.d.js";
// Custom APIs for renderer
const api = {
nfc: {
getReaderName: (): Promise<string> => {
return ipcRenderer.invoke("nfc:getReaderName");
},
onReader: (callback: (event: { name: string }) => void): void => {
ipcRenderer.on("nfc:reader", (_event, value) => callback(value));
},
onTag: (callback: (event: { uid: string }) => void): void => {
ipcRenderer.on("nfc:tag", (_event, value) => callback(value));
},
@@ -24,6 +30,7 @@ const api = {
return ipcRenderer.invoke("ping:url", url);
},
removeAllListeners: (): void => {
ipcRenderer.removeAllListeners("nfc:reader");
ipcRenderer.removeAllListeners("nfc:tag");
ipcRenderer.removeAllListeners("nfc:removed");
ipcRenderer.removeAllListeners("nfc:error");

View File

@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<title>Lector NFC</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"

View File

@@ -30,3 +30,8 @@ body {
);
background-size: 20px 20px;
}
.error {
color: red;
font-style: italic;
}

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { type CodeResponse } from "src/preload/index.d.js";
import { tryCatch } from "../../../types/Result.js";
import { ref, onMounted, onUnmounted } from "vue";
/**
* waiting = esperando a leer una tarjeta (igual es mejor ready)
@@ -10,10 +11,18 @@ import { ref, onMounted, onUnmounted } from "vue";
const readerState = ref<"waiting" | "reading" | "success" | "error">("waiting");
const uid = ref<string | null>(null);
const errorMsg = ref<string | null>(null);
const labelOutput = ref<string>("");
const labelOutputStructured = ref<CodeResponse | null>(null);
const readerName = ref<string>("OFFLINE");
const errors = ref<string[]>([]);
onMounted(async () => {
// Get initial reader name
readerName.value = await window.api.nfc.getReaderName();
window.api.nfc.onReader((event) => {
readerName.value = event.name;
});
onMounted(() => {
window.api.nfc.onTag((event) => {
uid.value = event.uid;
readerState.value = "success";
@@ -64,43 +73,62 @@ const mockReadCard = (): void => {
const readCard = async (): Promise<void> => {
console.log("test readcard");
// Reset de los errores anteriores
errors.value = [];
if (readerState.value !== "waiting") return;
readerState.value = "reading";
const serverURL =
localStorage.getItem("serverUrl") || "http://localhost:3000";
const printerURL =
localStorage.getItem("printerUrl") || "192.168.1.254";
const printerURL = localStorage.getItem("printerUrl") || "192.168.1.254";
// TODO: De momento está hardcodeado porque no se puede leer la tarjeta
const card_id = "019cdd39-fc08-7417-b16d-a78794a24c01";
const override = false;
try {
const res: CodeResponse = await window.api.nfc.labelReq({
const codePromise = window.api.nfc.labelReq({
serverURL,
card_id,
override,
});
console.log("Res IPC", res);
labelOutput.value = res.data.label;
labelOutputStructured.value = res;
console.log("Imprimiendo", res);
const impRes = await window.api.nfc.printReq({
const res = await tryCatch(codePromise);
console.log("RES IPC:", res);
// Error de la llamada -> sin conexion al servidor
if (res.error != undefined) {
errors.value = [...errors.value, String(res.error)];
delayWaiting();
return;
}
// Error que viene del servidor
if (res.data?.error != undefined) {
errors.value = [...errors.value, String(res.data.error)];
delayWaiting();
return;
}
const labelData = res.data.data.data;
labelOutputStructured.value = res.data.data;
console.log("label data", labelData);
const impPromise = window.api.nfc.printReq({
printerURL,
label: res.data.label,
label: labelData.label,
});
console.log("Impreso", impRes);
readerState.value = "waiting";
} catch (e) {
console.error(e);
readerState.value = "error";
const resPrint = await tryCatch(impPromise);
console.log("print", resPrint);
if (resPrint.error != undefined) {
let errorStr = "";
if (resPrint.error.message.includes("timeout")) {
errorStr = "Error de conexión con la impresora";
}
errors.value = [...errors.value, errorStr];
delayWaiting();
}
delayWaiting();
};
const delayWaiting = (): void => {
@@ -125,8 +153,8 @@ const triggerError = (): void => {
<div class="top-meta">
<div class="meta-item">
<span class="label">MODULE</span>
<span class="value">NFC_RD_01</span>
<span class="label">LECTOR</span>
<span class="value">{{ readerName }}</span>
</div>
</div>
@@ -134,7 +162,7 @@ const triggerError = (): void => {
<div v-if="readerState === 'waiting'" class="waiting-state">
<div class="pulse-ring"></div>
<div class="pulse-inner"></div>
<h1 class="status-msg">TAP CARD TO INITIALIZE</h1>
<h1 class="status-msg">ACERCA UN NFC PARA EMPEZAR</h1>
</div>
<div
@@ -171,7 +199,22 @@ const triggerError = (): void => {
</div>
<!-- Salida por pantalla de las respuestas del servidor y la impresora -->
<div class="display-container bg-dots">
<!-- ERRORES -->
<div class="error">
<span v-for="(error, idx) in errors" :key="idx">
<span>Error: {{ error }}</span>
</span>
</div>
<div v-if="labelOutputStructured != undefined" class="flex-col">
<!-- Errores -->
<span
v-if="labelOutputStructured.error != undefined"
class="error"
>
{{ JSON.stringify(labelOutputStructured.error) }}
</span>
<!-- Datos -->
<span> Código {{ labelOutputStructured.data.code }} </span>
<span>
Etiqueta {{ labelOutputStructured.data.label }}

View File

@@ -1,13 +1,16 @@
import PCSC, { Tag, Reader } from "@tockawa/nfc-pcsc";
import { logger } from "../main/LogService";
export type NfcEvent =
| { type: "tag"; uid: string }
| { type: "removed"; uid: string }
| { type: "reader"; name: string }
| { type: "error"; message: string };
export class NfcService {
private nfc: PCSC;
private onEvent?: (event: NfcEvent) => void;
private currentReaderName: string = "OFFLINE";
constructor(onEvent?: (event: NfcEvent) => void) {
this.nfc = new PCSC.default();
@@ -15,12 +18,22 @@ export class NfcService {
this.init();
}
public getReaderName(): string {
return this.currentReaderName;
}
private init(): void {
this.nfc.on("reader", (reader: Reader) => {
console.log(`Lector detectado: ${reader.name}`);
logger.info(`NFC Reader detected: ${reader.name}`);
this.currentReaderName = reader.name;
if (this.onEvent) {
this.onEvent({ type: "reader", name: reader.name });
}
reader.on("card", async (card: Tag) => {
console.log(`Tarjeta detectada! UID: ${card.uid}`);
logger.info(`NFC Tag detected: ${card.uid}`);
if (this.onEvent) {
this.onEvent({ type: "tag", uid: card.uid });
}
@@ -28,6 +41,7 @@ export class NfcService {
reader.on("card.off", async (card: Tag) => {
console.log(`Tarjeta retirada: ${card.uid}`);
logger.info(`NFC Tag removed: ${card.uid}`);
if (this.onEvent) {
this.onEvent({ type: "removed", uid: card.uid });
}
@@ -45,6 +59,11 @@ export class NfcService {
reader.on("end", () => {
console.log(`Lector desconectado: ${reader.name}`);
logger.info(`NFC Reader disconnected: ${reader.name}`);
this.currentReaderName = "OFFLINE";
if (this.onEvent) {
this.onEvent({ type: "reader", name: "OFFLINE" });
}
});
});

27
src/types/Result.ts Normal file
View File

@@ -0,0 +1,27 @@
export type Success<D> = {
error?: undefined | null;
data: D;
};
export type Failure<E = Error> = {
data?: undefined | null;
error: E;
};
/**
* Result<Error,Data>
*/
export type Result<E, D> = Failure<E> | Success<D>;
export async function tryCatch<T>(func: Promise<T>): Promise<Result<Error, T>> {
try {
const res = await func;
return {
data: res,
};
} catch (e: unknown) {
return {
error: e as Error,
};
}
}

View File

@@ -1,6 +1,8 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": [
"src/config/**/*",
"src/types/**/*",
"electron.vite.config.*",
"src/main/**/*",
"src/preload/**/*",