guardado y restaurado del estado del nameManager
This commit is contained in:
@@ -6,12 +6,23 @@
|
|||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="btn btn-export" @click="downloadCsvByRoom">Descargar por sala (CSV)</button>
|
<button class="btn btn-export" @click="downloadCsvByRoom">Descargar por sala (CSV)</button>
|
||||||
<button class="btn btn-export alt" @click="downloadCsvByUuid">Descargar por UUID (CSV)</button>
|
<button class="btn btn-export alt" @click="downloadCsvByUuid">Descargar por UUID (CSV)</button>
|
||||||
|
<button class="btn btn-save" @click="downloadNameManagerState">💾 Descargar Estado (.snatchSave)</button>
|
||||||
|
<div class="upload-container">
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".snatchSave"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-load" @click="triggerFileUpload">📂 Cargar Estado (.snatchSave)</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
interface PlayerRow {
|
interface PlayerRow {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -35,6 +46,7 @@ interface RoomDetail {
|
|||||||
|
|
||||||
const props = defineProps<{ rooms: any[]; roomDetails: { [key: string]: RoomDetail } }>();
|
const props = defineProps<{ rooms: any[]; roomDetails: { [key: string]: RoomDetail } }>();
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement>();
|
||||||
const gameRooms = computed(() => props.rooms.filter(r => r.name === 'game'));
|
const gameRooms = computed(() => props.rooms.filter(r => r.name === 'game'));
|
||||||
|
|
||||||
function csvEscape(v: any): string {
|
function csvEscape(v: any): string {
|
||||||
@@ -169,6 +181,83 @@ function triggerDownload(csv: string, suffix: string) {
|
|||||||
|
|
||||||
function downloadCsvByRoom() { triggerDownload(buildCsvByRoom(), 'by-room'); }
|
function downloadCsvByRoom() { triggerDownload(buildCsvByRoom(), 'by-room'); }
|
||||||
async function downloadCsvByUuid() { const csv = await buildCsvByUuid(); triggerDownload(csv, 'by-uuid'); }
|
async function downloadCsvByUuid() { const csv = await buildCsvByUuid(); triggerDownload(csv, 'by-uuid'); }
|
||||||
|
|
||||||
|
// NameManager save/load functions
|
||||||
|
async function downloadNameManagerState() {
|
||||||
|
try {
|
||||||
|
const apiBase = (import.meta as any).env?.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`;
|
||||||
|
const response = await fetch(`${apiBase}/admin/namemanager/export`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch nameManager state: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const jsonString = JSON.stringify(data, null, 2);
|
||||||
|
|
||||||
|
const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const now = new Date();
|
||||||
|
const pad = (n: number) => String(n).padStart(2, '0');
|
||||||
|
const fileName = `namemanager-${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.snatchSave`;
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading nameManager state:', error);
|
||||||
|
alert('Error al descargar el estado del nameManager');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerFileUpload() {
|
||||||
|
fileInput.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileUpload(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const file = target.files?.[0];
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.name.endsWith('.snatchSave')) {
|
||||||
|
alert('Por favor selecciona un archivo .snatchSave válido');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
|
||||||
|
const apiBase = (import.meta as any).env?.VITE_API_URL || `${window.location.protocol}//${window.location.host}/api`;
|
||||||
|
const response = await fetch(`${apiBase}/admin/namemanager/import`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to upload nameManager state: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
alert(`Estado cargado exitosamente. ${result.message || 'NameManager actualizado.'}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading nameManager state:', error);
|
||||||
|
alert('Error al cargar el estado del nameManager. Verifica que el archivo sea válido.');
|
||||||
|
} finally {
|
||||||
|
// Reset file input
|
||||||
|
target.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -183,8 +272,13 @@ async function downloadCsvByUuid() { const csv = await buildCsvByUuid(); trigger
|
|||||||
}
|
}
|
||||||
.section-header h2 { margin:0; font-size: 1.3rem; }
|
.section-header h2 { margin:0; font-size: 1.3rem; }
|
||||||
.buttons { display:flex; gap: 10px; flex-wrap: wrap; }
|
.buttons { display:flex; gap: 10px; flex-wrap: wrap; }
|
||||||
.btn { padding: 10px 14px; border:none; border-radius: 10px; font-weight:700; cursor:pointer; }
|
.btn { padding: 10px 14px; border:none; border-radius: 10px; font-weight:700; cursor:pointer; transition: all 0.3s ease; }
|
||||||
.btn-export { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color:white; }
|
.btn-export { background: linear-gradient(135deg, #10b981 0%, #059669 100%); color:white; }
|
||||||
.btn-export.alt { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
|
.btn-export.alt { background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); }
|
||||||
.btn-export:hover { filter: brightness(1.05); }
|
.btn-export:hover { filter: brightness(1.05); }
|
||||||
|
.btn-save { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color:white; }
|
||||||
|
.btn-save:hover { filter: brightness(1.05); transform: translateY(-1px); }
|
||||||
|
.btn-load { background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); color:white; }
|
||||||
|
.btn-load:hover { filter: brightness(1.05); transform: translateY(-1px); }
|
||||||
|
.upload-container { position: relative; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -959,4 +959,42 @@ async function sendPlayersActionsUpdate(client?: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NameManager export endpoint
|
||||||
|
adminRouter.get("/admin/namemanager/export", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const nameManager = NameManager.getInstance();
|
||||||
|
const state = nameManager.exportState();
|
||||||
|
res.json(state);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AdminAPI] Error exporting nameManager state:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to export nameManager state' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NameManager import endpoint
|
||||||
|
adminRouter.post("/admin/namemanager/import", async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const nameManager = NameManager.getInstance();
|
||||||
|
const state = req.body;
|
||||||
|
|
||||||
|
if (!state || !state.data) {
|
||||||
|
return res.status(400).json({ error: 'Invalid state format' });
|
||||||
|
}
|
||||||
|
|
||||||
|
nameManager.importState(state);
|
||||||
|
|
||||||
|
// Broadcast update to SSE clients after importing
|
||||||
|
await sendUuidsUpdate();
|
||||||
|
await sendPlayersActionsUpdate();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `NameManager state imported successfully. Version: ${state.version || 'unknown'}`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AdminAPI] Error importing nameManager state:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to import nameManager state', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export { adminRouter, broadcastDashboardUpdate };
|
export { adminRouter, broadcastDashboardUpdate };
|
||||||
|
|||||||
@@ -208,4 +208,101 @@ export class NameManager {
|
|||||||
clearSystemHistory(uuid: string): void {
|
clearSystemHistory(uuid: string): void {
|
||||||
this.uuidToSystemHistory.delete(uuid);
|
this.uuidToSystemHistory.delete(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export/Import methods for backup/restore functionality
|
||||||
|
exportState(): any {
|
||||||
|
return {
|
||||||
|
version: "1.0",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
data: {
|
||||||
|
uuidToName: Object.fromEntries(this.uuidToName),
|
||||||
|
uuidToColor: Object.fromEntries(this.uuidToColor),
|
||||||
|
uuidToShame: Object.fromEntries(this.uuidToShame),
|
||||||
|
uuidToSystemHistory: Object.fromEntries(this.uuidToSystemHistory),
|
||||||
|
uuidToCurrentRoom: Object.fromEntries(this.uuidToCurrentRoom),
|
||||||
|
uuidToReconnectToken: Object.fromEntries(this.uuidToReconnectToken),
|
||||||
|
roomAssignments: Object.fromEntries(this.roomAssignments),
|
||||||
|
shuffleInProgress: this.shuffleInProgress
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
importState(state: any): void {
|
||||||
|
if (!state || !state.data) {
|
||||||
|
throw new Error('Invalid state format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = state;
|
||||||
|
|
||||||
|
// Clear current state
|
||||||
|
this.uuidToName.clear();
|
||||||
|
this.uuidToColor.clear();
|
||||||
|
this.uuidToShame.clear();
|
||||||
|
this.uuidToSystemHistory.clear();
|
||||||
|
this.uuidToCurrentRoom.clear();
|
||||||
|
this.uuidToReconnectToken.clear();
|
||||||
|
this.roomAssignments.clear();
|
||||||
|
|
||||||
|
// Import data
|
||||||
|
if (data.uuidToName) {
|
||||||
|
for (const [uuid, name] of Object.entries(data.uuidToName)) {
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
this.uuidToName.set(uuid, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.uuidToColor) {
|
||||||
|
for (const [uuid, color] of Object.entries(data.uuidToColor)) {
|
||||||
|
if (typeof color === 'string') {
|
||||||
|
this.uuidToColor.set(uuid, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.uuidToShame) {
|
||||||
|
for (const [uuid, shame] of Object.entries(data.uuidToShame)) {
|
||||||
|
if (typeof shame === 'number') {
|
||||||
|
this.uuidToShame.set(uuid, shame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.uuidToSystemHistory) {
|
||||||
|
for (const [uuid, history] of Object.entries(data.uuidToSystemHistory)) {
|
||||||
|
if (Array.isArray(history)) {
|
||||||
|
this.uuidToSystemHistory.set(uuid, history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.uuidToCurrentRoom) {
|
||||||
|
for (const [uuid, roomId] of Object.entries(data.uuidToCurrentRoom)) {
|
||||||
|
if (typeof roomId === 'string') {
|
||||||
|
this.uuidToCurrentRoom.set(uuid, roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.uuidToReconnectToken) {
|
||||||
|
for (const [uuid, token] of Object.entries(data.uuidToReconnectToken)) {
|
||||||
|
if (typeof token === 'string') {
|
||||||
|
this.uuidToReconnectToken.set(uuid, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.roomAssignments) {
|
||||||
|
for (const [uuid, assignment] of Object.entries(data.roomAssignments)) {
|
||||||
|
if (assignment && typeof assignment === 'object' &&
|
||||||
|
'roomId' in assignment && 'role' in assignment) {
|
||||||
|
this.roomAssignments.set(uuid, assignment as { roomId: string; role: 'P1' | 'P2' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.shuffleInProgress === 'boolean') {
|
||||||
|
this.shuffleInProgress = data.shuffleInProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user