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>
73 lines
2.6 KiB
JavaScript
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 {} },
|
|
};
|
|
}
|