Files
Rotatory/server/store.js
T
ferdi2goandClaude Fable 5.1 4ec23ccd25 Fehler melden mit Screenshot und Markierung, Sammlung für Bearbeiter
Käfer-Button unten rechts: nimmt den sichtbaren Bildschirm mit html2canvas
(selbst gehostet) auf, Vorschau mit roten Rechtecken, Beschreibung, Name
optional, Senden ohne Anmeldung. Aus offenen Dialogen heraus wird der
Dialog selbst aufgenommen. Bearbeiter sehen die Meldungen unter „Fehler"
mit Zähler, kopieren einen fertigen Claude-Prompt und den Screenshot.

Serverseitig: Pfade nur in exakter Schreibweise, /api standardmäßig nur
für Bearbeiter mit ausdrücklicher Freigabeliste, Bremse zählt jeden
Versuch vor dem Lesen großer Bodies, X-Forwarded-For nur von vertrauten
Proxys, Nutzertext im Prompt eingezäunt. HTTP-Tests für die Sperren.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 08:19:46 +00:00

73 lines
2.6 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { DEFAULT_PROMPT_TEMPLATE } from './bugs.js';
const DEFAULT_STATE = () => ({
persons: [],
absences: [],
holidays: [],
assignments: {},
requests: [],
bugs: [],
exported: {},
settings: {
meetingTime: '09:30',
durationMinutes: 15,
bundesland: 'BY',
calendarDisplayName: 'Morning Meeting',
inviteAttendees: false,
historyWindowDays: 60,
holidaysSeeded: false,
bugPromptTemplate: DEFAULT_PROMPT_TEMPLATE,
targetCalendarHref: '',
targetCalendarName: '',
absenceCalendarHref: '',
absenceCalendarName: '',
},
});
/** Bildtyp anhand der ersten Bytes erkennen. */
export function imageMime(buffer) {
if (!buffer || buffer.length < 12) return null;
if (buffer[0] === 0xff && buffer[1] === 0xd8) return 'image/jpeg';
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return 'image/png';
if (buffer.subarray(0, 4).toString('ascii') === 'RIFF' && buffer.subarray(8, 12).toString('ascii') === 'WEBP') return 'image/webp';
if (buffer.subarray(0, 4).toString('ascii') === 'GIF8') return 'image/gif';
return null;
}
/** Einfacher JSON-Speicher mit atomarem Schreiben. Fotos liegen daneben im Ordner photos/. */
export function createStore(dataDir) {
const file = path.join(dataDir, 'rotatory.json');
const photoDir = path.join(dataDir, 'photos');
const bugDir = path.join(dataDir, 'bugs');
fs.mkdirSync(photoDir, { recursive: true });
fs.mkdirSync(bugDir, { recursive: true });
let state = DEFAULT_STATE();
if (fs.existsSync(file)) {
const loaded = JSON.parse(fs.readFileSync(file, 'utf8'));
state = { ...state, ...loaded, settings: { ...state.settings, ...(loaded.settings || {}) } };
}
function save() {
const tmp = file + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(state, null, 2));
fs.renameSync(tmp, file);
}
const photoPath = (id) => path.join(photoDir, `${id}.jpg`);
return {
get state() { return state; },
save,
photoPath,
hasPhoto: (id) => fs.existsSync(photoPath(id)),
readPhoto: (id) => (fs.existsSync(photoPath(id)) ? fs.readFileSync(photoPath(id)) : null),
savePhoto: (id, buffer) => fs.writeFileSync(photoPath(id), buffer),
deletePhoto: (id) => { try { fs.unlinkSync(photoPath(id)); } catch {} },
saveBug: (id, buffer) => fs.writeFileSync(path.join(bugDir, `${id}.png`), buffer),
readBug: (id) => (fs.existsSync(path.join(bugDir, `${id}.png`)) ? fs.readFileSync(path.join(bugDir, `${id}.png`)) : null),
deleteBug: (id) => { try { fs.unlinkSync(path.join(bugDir, `${id}.png`)); } catch {} },
};
}