├── .gitconfig ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── app.js ├── eventmodel.drawio ├── fake-events ├── sc-close-registration-fail │ ├── 0000-unique_id_generated-conf_id:1111-2222-3333-event.json │ └── 0001-close_registration--event.json ├── sc-close-registration-success │ └── 0000-unique_id_generated-conf_id:1111-2222-3333-event.json ├── sv-join-conference │ ├── 0000-unique_id_requested--event.json │ └── 0001-unique_id_generated-conf_id:5555-6666-7777-event.json ├── sv-register │ ├── 0000-conference_named-Event Modeling Open Spaces-event.json │ ├── 0001-conference_id_generated-1234-5678-9012-event.json │ └── 0002-registered-John Smith,abcd-efgh-ijkl-event.json ├── sv-registration-name-for-suggestion │ ├── 0000-unique_id_generated--event.json │ ├── 0001-registered-Alice Johnson,REG-001-event.json │ ├── 0002-registered-Bob Smith,REG-002-event.json │ └── 0003-registered-Carol Davis,REG-003-event.json ├── sv-rooms │ ├── 0000-room_added-Auditorium-event.json │ ├── 0001-room_added-CS100-event.json │ ├── 0002-room_renamed-oldname:Auditorium,newname:Main Hall-event.json │ ├── 0003-room_added-CS200-event.json │ └── 0004-room_deleted-CS100-event.json ├── sv-set-dates │ └── 0000-conference_dates_set-start_date2024-06-06,end_date:2024-06-07-event.json ├── sv-time-slots │ ├── 0000-time_slot_added-1st_session-event.json │ └── 0001-time_slot_added-2nd_session-event.json ├── sv-topics │ ├── 0000-registered-Alice Johnson,REG-001-event.json │ ├── 0001-session_submitted-Workshop:Event Storming Basics,REG-001-event.json │ ├── 0002-registered-Bob Smith,REG-002-event.json │ ├── 0003-session_submitted-Discussion:Domain-Driven Design Patterns,REG-002-event.json │ ├── 0004-session_submitted-Presentation:CQRS Implementation Strategies,REG-001-event.json │ ├── 0005-registered-Carol Williams,REG-003-event.json │ ├── 0006-session_submitted-Workshop:Event Sourcing Best Practices,REG-003-event.json │ ├── 0007-session_submitted-Discussion:Microservices Communication Patterns,REG-002-event.json │ └── 0008-session_submitted-Workshop:Testing Event-Driven Systems,REG-003-event.json └── sv-voting │ ├── 0000-conference_id_generated-8765-4321-event.json │ ├── 0001-registered-Alice Johnson,REG-001-event.json │ ├── 0002-registered-Bob Smith,REG-002-event.json │ ├── 0003-registered-Carol Williams,REG-003-event.json │ ├── 0004-session_submitted-Workshop,Event Modeling Basics,REG-001-event.json │ ├── 0005-session_submitted-Discussion,Domain-Driven Design Patterns,REG-002-event.json │ ├── 0006-session_submitted-Presentation,CQRS Implementation Strategies,REG-001-event.json │ ├── 0007-session_submitted-Workshop,Event Sourcing Best Practices,REG-003-event.json │ ├── 0008-voted_for_sessions-REG-001,Event Modeling Basics;Domain-Driven Design Patterns-event.json │ ├── 0009-voted_for_sessions-REG-002,Event Modeling Basics;CQRS Implementation Strategies-event.json │ └── 0010-voted_for_sessions-REG-003,Domain-Driven Design Patterns;Event Sourcing Best Practices-event.json ├── package-lock.json ├── package.json ├── public └── styles │ ├── error.css │ └── main.css ├── scripts ├── events-abbreviations.fish ├── events-tabcomplete.fish ├── events.fish ├── make_fake_event.sh └── update-fake-events.sh └── views ├── error.mustache ├── generate-conf-id.mustache ├── join-conference.mustache ├── register-success.mustache ├── register.mustache ├── rooms.mustache ├── set-conference-name-confirmation.mustache ├── set-conference-name.mustache ├── set-dates-confirmation.mustache ├── set-dates.mustache ├── submit-session.mustache ├── time-slots.mustache ├── todo-gen-conf-ids.mustache ├── topics.mustache └── voting.mustache /.gitconfig: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | [remote "origin"] 7 | url = git@github.com:event-modeling/open-spaces-comocamp.git 8 | fetch = +refs/heads/bpf*:refs/remotes/origin/bpf* 9 | [branch "bpf-design"] 10 | remote = origin 11 | merge = refs/heads/bpf-design 12 | [branch "bpf/slice/sc/time-slot-add/node"] 13 | vscode-merge-base = origin/main 14 | remote = origin 15 | merge = refs/heads/bpf/slice/sc/time-slot-add/node 16 | [branch "bpf-dev"] 17 | vscode-merge-base = origin/bpf/slice/sc/time-slot-add/node 18 | remote = origin 19 | merge = refs/heads/bpf-dev 20 | [branch "bpf-qa"] 21 | vscode-merge-base = origin/bpf/slice/sc/time-slot-add/node 22 | remote = origin 23 | merge = refs/heads/bpf-qa 24 | [branch "bpf/slice/sv/todo-gen-conf-id/node"] 25 | vscode-merge-base = origin/main 26 | remote = origin 27 | merge = refs/heads/bpf/slice/sv/todo-gen-conf-id/node 28 | [branch "bpf/slice/sv/rooms/dotnet"] 29 | vscode-merge-base = origin/bpf/slice/sv/todo-gen-conf-id 30 | remote = origin 31 | merge = refs/heads/bpf/slice/sv/rooms/dotnet 32 | [branch "bpf/slice/sv/rooms/node"] 33 | remote = origin 34 | merge = refs/heads/bpf/slice/sv/rooms/node 35 | [branch "bpf/slice/sc/set-name/node"] 36 | vscode-merge-base = origin/bpf-qa 37 | [branch "main"] 38 | vscode-merge-base = origin/bpf-qa 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | event-stream/ 2 | node_modules/ 3 | bin/* 4 | obj/ 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md. 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net8.0/open-spaces-comocamp.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/open-spaces-comocamp.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/open-spaces-comocamp.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary;ForceNoAlign" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/open-spaces-comocamp.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | let port = 3002; 2 | let slice_tests = []; 3 | const sync_time = 0; 4 | let eventstore = "./event-stream"; 5 | const event_seq_padding = '0000'; 6 | 7 | let run_tests = process.argv.includes('--test'); 8 | let long_ids = process.argv.includes('--long-ids'); 9 | 10 | const { v4: uuidv4 } = require('uuid'); function generate_id() { return long_ids ? uuidv4() : uuidv4().slice(0, 8); } 11 | const express = require("express"); 12 | const app = express(); 13 | const fs = require("fs"); 14 | const multer = require("multer"); 15 | const upload = multer(); 16 | app.set("view engine", "mustache"); 17 | app.engine("mustache", require("mustache-express")()); 18 | app.use(express.static('public')); 19 | app.use(express.json()); 20 | app.use('/error.css', express.static('public/styles/error.css')); 21 | 22 | function strip_summary(event) { if (event && event.meta) { delete event.meta.summary; } return event; } 23 | function get_events() { 24 | if (!fs.existsSync(eventstore)) fs.mkdirSync(eventstore); 25 | return fs.readdirSync(eventstore).sort().map(file => { return JSON.parse(fs.readFileSync(`${eventstore}/${file}`, "utf8")); }); } 26 | function push_event(event) { 27 | let event_type = event.meta.type; 28 | let summary = event.meta.summary ? event.meta.summary : ""; 29 | event = strip_summary(event); 30 | if (!fs.existsSync(eventstore)) fs.mkdirSync(eventstore); 31 | const event_count = fs.readdirSync(eventstore).filter(file => file.endsWith('-event.json')).length; 32 | const event_seq = event_seq_padding.slice(0, event_seq_padding.length - event_count.toString().length) + event_count; 33 | fs.writeFileSync(`${eventstore}/${event_seq}-${event_type}-${summary}-event.json`, JSON.stringify(event)); 34 | if (sync_time === 0 ) notify_processors(event); } 35 | 36 | if (sync_time > 0) setInterval(notify_processors, sync_time); 37 | function notify_processors(event = null) { 38 | if (event === null) { processors.forEach(processor => processor.function(get_events())); return;} 39 | processors.forEach(processor => { if (processor.events.includes(event.meta.type)) processor.function(get_events()); });} 40 | const processors = []; 41 | 42 | function change_state_http_wrapper(command_handler, command, error_next, success_action) { 43 | let events, result_event = undefined; 44 | try { events = get_events(); console.log("events count for state change: " + events.length); 45 | } catch (error) { console.error("Error getting events: " + error.message); 46 | const new_error = new Error(error.message); new_error.status = 500; return error_next(new_error); } 47 | try { result_event = command_handler(events, command); console.log("result_event: ", JSON.stringify(result_event)); 48 | } catch (error) { console.error("Error changing state: " + error.message); 49 | const new_error = new Error(error.message); new_error.status = 422; return error_next(new_error); } 50 | try { push_event(result_event); 51 | } catch (error) { console.error("Error pushing event: " + error.message); 52 | const new_error = new Error(error.message); new_error.status = 500; return error_next(new_error); } 53 | if (success_action !== undefined) success_action(result_event); 54 | return result_event; 55 | } // change_state_via_http 56 | 57 | function get_state_http_wrapper(state_view, error_next, success_action) { 58 | let events = null; 59 | try { events = get_events(); console.log("events count for state view: " + events.length); 60 | } catch (error) { console.error("Error getting events: " + error.message); 61 | const new_error = new Error(error.message); new_error.status = 500; return error_next(new_error); } 62 | let state = null; 63 | try { state = state_view(events); console.log("state: ", JSON.stringify(state)); 64 | } catch (error) { console.error("Error getting state: " + error.message); 65 | const new_error = new Error(error.message); new_error.status = 500; return error_next(new_error); } 66 | if (success_action !== undefined) success_action(state); 67 | return state; 68 | } // get_state_via_http 69 | 70 | function get_access_token_http_wrapper(request, error_next, success_action) { 71 | const registration_id = request.query.registration_id || request.params.registration_id; 72 | get_state_http_wrapper(registrations_state_view, error_next, (state) => { 73 | const name = state.registrations[registration_id]; 74 | if (name === undefined) { const new_error = new Error("Forbidden"); new_error.status = 403; return error_next(new_error); } 75 | const token = { name: name, registration_id: registration_id }; 76 | if (success_action !== undefined) success_action(token); 77 | return token; 78 | }); 79 | } // get_access_token 80 | 81 | app.get("/set-conference-name", (req, res) => { res.render("set-conference-name", { name: "" }); }); 82 | 83 | app.post('/set-conference-name', upload.none(), (req, res, error_next) => { 84 | change_state_http_wrapper(set_conference_name, { name: req.body.conferenceName }, error_next, () => { res.redirect('/set-conference-name-confirmation'); }); 85 | }); // set_conference_name 86 | 87 | const exception_conference_named_with_no_change = new Error("You didn't change the name. No change registered."); 88 | function set_conference_name(history, command) { 89 | const current_name = history.reduce((acc, event) => { 90 | switch(event.meta.type) { 91 | case "conference_named": return event.name; 92 | default: return acc; } 93 | }, ""); 94 | if (current_name === command.name) throw exception_conference_named_with_no_change; 95 | return { name: command.name, meta: { type: "conference_named", summary: command.name }}; 96 | } 97 | 98 | slice_tests.push({ test_function: set_conference_name, 99 | timelines: [ 100 | { 101 | timeline_name: "Happy Path", 102 | checkpoints: [ 103 | { 104 | event: { 105 | name: "EM Open Spaces", 106 | meta: { type: "conference_named" }, 107 | }, 108 | command: { 109 | name: "EM Open Spaces", 110 | }, 111 | purpose: "test that the conference name is set to the new name" 112 | } 113 | ] 114 | }, 115 | { 116 | timeline_name: "Renames allowed", 117 | checkpoints: [ 118 | { 119 | event: { 120 | name: "EM Open Spaces", 121 | meta: { type: "conference_named" }, 122 | } 123 | }, 124 | { 125 | event: { 126 | name: "Event Modeling Space", 127 | meta: { type: "conference_named" }, 128 | }, 129 | command: { 130 | name: "Event Modeling Space", 131 | }, 132 | purpose: "name should be changeable" 133 | } 134 | ] 135 | }, 136 | { 137 | timeline_name: "Renames allowed multiple times", 138 | checkpoints: [ 139 | { 140 | event: { 141 | name: "EM Open Spaces", 142 | meta: { type: "conference_named" }, 143 | } 144 | }, 145 | { 146 | event: { 147 | name: "Event Modeling Space", 148 | meta: { type: "conference_named" }, 149 | } 150 | }, 151 | { 152 | event: { 153 | name: "Event Modeling Open Spaces", 154 | meta: { type: "conference_named" }, 155 | }, 156 | command: { 157 | name: "Event Modeling Open Spaces", 158 | }, 159 | purpose: "name should be changeable multiple times" 160 | } 161 | ] 162 | }, 163 | { 164 | timeline_name: "Renames not allowed if new name is the same", 165 | checkpoints: [ 166 | { 167 | event: { 168 | name: "EM Open Spaces", 169 | meta: { type: "conference_named" }, 170 | } 171 | }, 172 | { 173 | exception: exception_conference_named_with_no_change, 174 | command: { 175 | name: "EM Open Spaces", 176 | }, 177 | purpose: "exception should be thrown if the conference name is not changed" 178 | } 179 | ] 180 | } 181 | ] 182 | }); // test: Set Conference Name State Change 183 | 184 | app.get("/set-conference-name-confirmation", (req, res, error_next) => { 185 | get_state_http_wrapper(conference_name_state_view, error_next, (conference_name) => { 186 | res.render("set-conference-name-confirmation", { conference_name }); 187 | }); 188 | }); // app.get("/set-conference-name-confirmation") 189 | 190 | function conference_name_state_view(history) { 191 | return history.reduce((name, event) => { if (event.meta.type === "conference_named") { return event.name; } return name; }, ""); } // conference_name_state_view 192 | 193 | app.get("/set-dates", (req, res, next) => { res.render("set-dates", { dates: [] }); }); 194 | 195 | app.post("/set-dates", upload.none(), (req, res, error_next) => { 196 | change_state_http_wrapper(set_dates, { startDate: req.body.startDate, endDate: req.body.endDate }, error_next, () => { res.redirect('/set-dates-confirmation'); }); 197 | }); // app.post("/set-dates") 198 | 199 | const exception_dates_have_invalid_range = new Error("Start date must be before end date"); 200 | function set_dates(history, command) { 201 | const start_date = new Date(command.startDate); 202 | const end_date = new Date(command.endDate); 203 | if (start_date > end_date) throw exception_dates_have_invalid_range; 204 | return { start_date: command.startDate, end_date: command.endDate, meta: { type: "set_dates", summary: command.startDate + " to " + command.endDate } }; 205 | } // set_dates 206 | 207 | app.get("/set-dates-confirmation",(_, res, next)=>{ 208 | get_state_http_wrapper(conference_dates_state_view, next, (conference_dates) => { 209 | res.render("set-dates-confirmation", conference_dates); 210 | }); 211 | }); 212 | 213 | function conference_dates_state_view(history) { 214 | const conference_dates_event = history.findLast(event => event.meta.type === "set_dates"); 215 | if (conference_dates_event === undefined) return { start_date: "", end_date: "" }; 216 | return { start_date: conference_dates_event.start_date, end_date: conference_dates_event.end_date }; 217 | } // conference_dates_state_view 218 | 219 | app.get("/rooms", (req, res, error_next) => { 220 | get_state_http_wrapper(rooms_state_view, error_next, (rooms) => { 221 | res.render("rooms", { rooms }); 222 | }); 223 | }); 224 | 225 | function rooms_state_view(history) { 226 | return history.reduce((acc, event) => { 227 | switch(event.meta.type) { 228 | case "room_added": acc.push(event.room_name); break; 229 | case "room_renamed": 230 | const index = acc.indexOf(event.old_name); 231 | if (index !== -1) acc[index] = event.new_name; 232 | break; 233 | case "room_deleted": 234 | const deleteIndex = acc.indexOf(event.room_name); 235 | if (deleteIndex !== -1) acc.splice(deleteIndex, 1); 236 | break; 237 | } 238 | return acc; 239 | }, []); 240 | } // rooms_state_view 241 | slice_tests.push({ test_function: rooms_state_view, 242 | timelines: [ 243 | { timeline_name: "happy path", 244 | checkpoints: [ 245 | { event: { room_name: "Auditorium", meta: { type: "room_added" } }, 246 | state: [], 247 | purpose: "no rooms should be returned when no events have occurred" 248 | }, 249 | { event: { room_name: "CS100", meta: { type: "room_added" }}, 250 | state: ["Auditorium"], 251 | purpose: "one room should be returned when one room has been added" 252 | }, 253 | { progress_marker: "at this point, the initial room reserves the name" }, 254 | { event: { room_name: "CS200", meta: { type: "room_added" } }, 255 | state: ["Auditorium", "CS100"], 256 | purpose: "two rooms should be returned when two rooms have been added" 257 | }, 258 | { event: { old_name: "Auditorium", new_name: "Main Hall", meta: { type: "room_renamed" } }, 259 | state: ["Auditorium", "CS100", "CS200"], 260 | purpose: "three rooms should be returned when three rooms have been added" 261 | }, 262 | { event: { room_name: "CS300", meta: { type: "room_added" } }, 263 | state: ["Main Hall", "CS100", "CS200"], 264 | purpose: "renamed room should show new name in correct position" 265 | }, 266 | { event: { room_name: "CS200", meta: { type: "room_deleted" } } }, 267 | { 268 | state: ["Main Hall", "CS100", "CS300"], 269 | purpose: "deleted room should not be in the result" 270 | } // checkpoint 271 | ] // checkpoints 272 | } // timeline 273 | ] // timelines 274 | } // slice 275 | ); // test: rooms state view 276 | 277 | app.post("/rooms", upload.none(), (req, res, error_next) => { 278 | change_state_http_wrapper(add_room, { room_name: req.body.roomName }, error_next, () => { res.redirect("/rooms"); }); 279 | }); // app.post("/rooms") 280 | 281 | const exception_room_already_exists = new Error("Room already exists"); 282 | function add_room(events, command) { 283 | if (events.some(event => event.meta.type === "room_added" && event.room_name === command.room_name)) 284 | throw exception_room_already_exists; 285 | return { room_name: command.room_name, meta: { type: "room_added", summary: command.room_name } }; 286 | } // add_room 287 | 288 | app.post("/time-slots", upload.none(), (req, res, error_next) => { 289 | change_state_http_wrapper(add_time_slot, { start_time: req.body.startTime, end_time: req.body.endTime, name: req.body.name }, error_next, () => { res.redirect("/time-slots"); }); 290 | }); // app.post("/time-slots") 291 | 292 | const exception_time_slot_required_fields_missing = new Error("Start time, end time, and name are required"); 293 | const exception_time_slot_time_order_invalid = new Error("End time must be after start time"); 294 | const exception_time_slot_overlapping = new Error("Time slot is overlapping with others that are already defined"); 295 | function add_time_slot(history, command) { 296 | function timeToMinutes(timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); 297 | return hours * 60 + minutes;} 298 | if (!command.start_time || !command.end_time || !command.name) throw exception_time_slot_required_fields_missing; 299 | 300 | const newStart = timeToMinutes(command.start_time); 301 | const newEnd = timeToMinutes(command.end_time); 302 | if (newStart >= newEnd) throw exception_time_slot_time_order_invalid; 303 | 304 | const hasOverlap = history 305 | .filter(event => event.meta.type === "time_slot_added") 306 | .some(event => { 307 | const existingStart = timeToMinutes(event.start_time); 308 | const existingEnd = timeToMinutes(event.end_time); 309 | return (newStart < existingEnd && newEnd > existingStart); 310 | }); 311 | if (hasOverlap) throw exception_time_slot_overlapping; 312 | 313 | return { start_time: command.start_time, end_time: command.end_time, name: command.name, 314 | meta: { type: "time_slot_added", summary: command.start_time + " to " + command.end_time + " - " + command.name } 315 | }; 316 | } // add_time_slot 317 | 318 | // each checkpoint is a test if a command is there 319 | slice_tests.push({ test_function: add_time_slot, 320 | timelines: [ 321 | { 322 | timeline_name: "Happy Path", 323 | checkpoints: [ 324 | { 325 | event: { start_time: "09:30", end_time: "10:25", name: "1st Session", 326 | meta: { type: "time_slot_added" } 327 | }, 328 | command: { start_time: "09:30", end_time: "10:25", name: "1st Session" }, 329 | purpose: "first time slot should be added when valid" 330 | }, 331 | { 332 | event: { start_time: "10:30", end_time: "11:25", name: "2nd Session", 333 | meta: { type: "time_slot_added" } 334 | }, 335 | command: { start_time: "10:30", end_time: "11:25", name: "2nd Session" }, 336 | purpose: "second time slot should be added when valid" 337 | }, 338 | { 339 | exception: exception_time_slot_overlapping, 340 | command: { start_time: "11:00", end_time: "12:00", name: "1st Session" }, 341 | purpose: "overlapping at the end of the time slot should be rejected" 342 | }, 343 | { 344 | exception: exception_time_slot_overlapping, 345 | command: { start_time: "10:00", end_time: "11:00", name: "1st Session" }, 346 | purpose: "overlapping at the start of the time slot should be rejected" 347 | }, 348 | { 349 | exception: exception_time_slot_overlapping, 350 | command: { start_time: "10:45", end_time: "11:10", name: "1st Session" }, 351 | purpose: "overlapping time slot entirely within an existing time slot should be rejected" 352 | } 353 | ] 354 | } 355 | ] 356 | }); // test: Add Time Slot State Change 357 | 358 | app.get("/time-slots",(_,res,error_next)=>{ 359 | get_state_http_wrapper(time_slots_state_view, error_next, (time_slots) => { res.render("time-slots", { time_slots });}); 360 | }); 361 | 362 | function time_slots_state_view(history) { 363 | return history.reduce((acc, event) => { 364 | if (event.meta.type === "time_slot_added") acc.push({ ...event, meta: undefined }); 365 | return acc; 366 | }, []); } // time_slots_state_view 367 | 368 | app.get("/generate-conf-id", (_, res) => { res.render("generate-conf-id"); }); 369 | 370 | app.post("/generate-conf-id", (_, res, error_next) => { change_state_http_wrapper(request_unique_id, {}, error_next, () => { res.redirect('/join-conference'); }); }); 371 | 372 | const exception_unique_id_already_requested = new Error("A request already exists"); 373 | function request_unique_id(history, command) { 374 | if (history.length > 0 && history[history.length - 1].meta.type === "conference_id_requested") throw exception_unique_id_already_requested; 375 | return { meta: { type: "conference_id_requested" } }; 376 | } // request_unique_id 377 | 378 | slice_tests.push({ test_function: request_unique_id, 379 | timelines: [ 380 | { 381 | timeline_name: "Happy Path", 382 | checkpoints: [ 383 | { 384 | event: { meta: { type: "conference_id_requested" } }, 385 | command: {}, 386 | purpose: "request unique ID should be added when requested", 387 | }, 388 | { 389 | exception: exception_unique_id_already_requested, 390 | command: {}, 391 | purpose: "request unique ID should throw an error when request already exists", 392 | }, 393 | { 394 | event: { conference_id: "1111-2222-3333", meta: { type: "conference_id_generated" } } 395 | }, 396 | { 397 | event: { meta: { type: "conference_id_requested" } }, 398 | command: {}, 399 | purpose: "request unique ID event should be added when requested after a conference ID has been generated" 400 | } 401 | ] 402 | } 403 | ] 404 | }); // test: request_unique_id_sc 405 | 406 | app.get("/todo-gen-conf-ids",(_, res, error_next)=>{ 407 | get_state_http_wrapper(todo_gen_conference_id_sv, error_next, (conference_ids) => { res.render("todo-gen-conf-ids", { conference_ids }); }); 408 | }); 409 | 410 | function todo_gen_conference_id_sv(history) { 411 | return history.reduce((acc, current_event) => { 412 | switch(current_event.meta.type) { 413 | case "conference_id_requested": 414 | if (acc.last_event !== null && acc.last_event.meta.type === "conference_id_requested") break; 415 | acc.todos.push({ conference_id: "" }); 416 | break; 417 | case "conference_id_generated": 418 | if ( acc.last_event === null 419 | || acc.last_event.meta.type !== "conference_id_requested" 420 | || acc.todos.length === 0 421 | || acc.todos[acc.todos.length - 1].conference_id !== "") 422 | break; 423 | acc.todos[acc.todos.length - 1].conference_id = current_event.conference_id; 424 | break; 425 | } 426 | acc.last_event = current_event; 427 | return acc; 428 | }, { todos: [], last_event: null }).todos; 429 | } // todo_gen_conference_id_sv 430 | 431 | 432 | slice_tests.push({ test_function: todo_gen_conference_id_sv, 433 | timelines: [ 434 | { 435 | timeline_name: "Happy Path", 436 | checkpoints: [ 437 | { 438 | event: { meta: { type: "conference_id_requested" } }, 439 | state: [], 440 | purpose: "empty array should be returned when no events exist" 441 | }, 442 | { 443 | event: { conference_id: "1111-2222-3333", meta: { type: "conference_id_generated" }}, 444 | state: [{ conference_id: "" }], 445 | purpose: "empty conf ID should be added on request" 446 | }, 447 | { 448 | event: { meta: { type: "some_other_event" } }, 449 | state: [{ conference_id: "1111-2222-3333" }], 450 | purpose: "conf ID should be updated when generated" 451 | }, 452 | { 453 | progress_marker: "Second Request behaves the same way" 454 | }, 455 | { 456 | event: { meta: { type: "conference_id_requested" } } 457 | }, 458 | { 459 | event: { conference_id: "2222-3333-4444", meta: { type: "conference_id_generated" }}, 460 | state: [{ conference_id: "1111-2222-3333" }, { conference_id: "" }], 461 | purpose: "second request should add new empty conf ID" 462 | }, 463 | { 464 | state: [{ conference_id: "1111-2222-3333" }, { conference_id: "2222-3333-4444" }], 465 | purpose: "second conf ID should be updated when generated" 466 | } 467 | ] 468 | }, 469 | { 470 | timeline_name: "A processor is idempotent", 471 | checkpoints: [ 472 | { 473 | event: { meta: { type: "conference_id_requested" } } 474 | }, 475 | { 476 | progress_marker: "A duplicate request of an ID will be ignored" 477 | }, 478 | { 479 | event: { meta: { type: "conference_id_requested" } }, 480 | state: [{ conference_id: "" }], 481 | purpose: "duplicate request should be ignored" 482 | }, 483 | { 484 | event: { conference_id: "3333-4444-5555", meta: { type: "conference_id_generated" } } 485 | }, 486 | { 487 | progress_marker: "A duplicate provision of an ID will be ignored" 488 | }, 489 | { 490 | event: { conference_id: "4444-5555-6666", meta: { type: "conference_id_generated" } }, 491 | state: [{ conference_id: "3333-4444-5555" }] , 492 | purpose: "duplicate generation should be ignored" 493 | } 494 | ] 495 | }, 496 | { 497 | timeline_name: "If no requests appear in the TODO list, a provided ID is ignored", 498 | checkpoints: [ 499 | { 500 | event: { meta: { type: "conference_id_generated" }, conference_id: "1111-2222-3333" }, 501 | state: [] , 502 | purpose: "generated ID should be ignored without request" 503 | } 504 | ] 505 | } 506 | ] 507 | }); // test: todo_gen_conference_id_sv 508 | 509 | function generate_conference_id_processor(history) { 510 | console.log("Looking for conf ID request in:"); 511 | const conference_ids = todo_gen_conference_id_sv(history); 512 | console.log(JSON.stringify(conference_ids, null, 2)); 513 | if ( conference_ids.length === 0 514 | || conference_ids[conference_ids.length - 1].conference_id !== "") { 515 | console.log("No conf ID request found."); 516 | return; 517 | } 518 | console.log("Found conf ID request."); 519 | if (conference_ids[conference_ids.length - 1].conference_id === "") generate_conference_id(); 520 | } // gen_conference_id_processor 521 | processors.push({ function: generate_conference_id_processor, events: ["conference_id_requested"] }); 522 | 523 | function generate_conference_id() { 524 | const conference_id = generate_id(); 525 | const conference_id_generated_event = provide_conference_id(get_events(), { conference_id: conference_id }); 526 | push_event(conference_id_generated_event, 'id:' + conference_id); 527 | console.log("Generated unique ID: " + conference_id); 528 | } // generate_conference_id 529 | 530 | const error_no_request_found = new Error("No conf ID request found."); 531 | function provide_conference_id(unfiltered_events, command) { 532 | const events = unfiltered_events.filter(event => event.meta.type === "conference_id_requested" || event.meta.type === "conference_id_generated"); 533 | if (events.length === 0 || events[events.length - 1].meta.type !== "conference_id_requested") { 534 | console.log("No conf ID request found."); 535 | throw error_no_request_found; 536 | } 537 | return { conference_id: command.conference_id, meta: { type: "conference_id_generated" } }; 538 | } // provide_conference_id 539 | 540 | slice_tests.push({ test_function: provide_conference_id, 541 | timelines: [ 542 | { 543 | timeline_name: "All scenarios in one timeline", 544 | checkpoints: [ 545 | { 546 | progress_marker: "Test trying to generate an ID with no events at all in history" 547 | }, 548 | { 549 | exception: error_no_request_found, 550 | command: { conference_id: "1111-2222-3333" }, 551 | purpose: "provide unique ID should throw an error when no request exists" 552 | }, 553 | { 554 | event: { meta: { type: "conference_id_requested" } } 555 | }, 556 | { 557 | event: { meta: { type: "some_other_event" } } 558 | }, 559 | { 560 | progress_marker: "Test the happy path" 561 | }, 562 | { 563 | event: { conference_id: "1111-2222-3333", meta: { type: "conference_id_generated" } }, 564 | command: { conference_id: "1111-2222-3333" }, 565 | purpose: "provide unique ID should be added when requested" 566 | }, 567 | { 568 | event: { meta: { type: "conference_id_requested" } } 569 | }, 570 | { 571 | event: { conference_id: "2222-3333-4444", meta: { type: "conference_id_generated" }} 572 | }, 573 | { 574 | exception: error_no_request_found, 575 | command: { conference_id: "3333-4444-5555" }, 576 | purpose: "provide unique ID should throw an error when no request exists" 577 | } 578 | ] 579 | } 580 | ] 581 | }); // test: generate_conference_id_sc 582 | 583 | app.get("/join-conference", (_, res, error_next) => { 584 | get_state_http_wrapper(join_conference_sv, error_next, (state) => { res.render("join-conference", { conference_id: state.conference_id || "1234" }); }); 585 | }); 586 | 587 | function join_conference_sv(history) { 588 | return history.reduce((acc, event) => { 589 | switch(event.meta.type) { case "conference_id_generated": acc.conference_id = event.conference_id; break; } 590 | return acc; 591 | }, { conference_id: null }); 592 | } // join_conference_sv 593 | 594 | app.get("/register/:conference_id", (req, res, error_next) => { 595 | get_state_http_wrapper(conference_name_state_view, error_next, (conference_name) => { res.render("register", { conference_name, conference_id: req.params.conference_id }); }); 596 | }); 597 | 598 | app.post("/register/:conference_id", multer().none(), (req, res, error_next) => { 599 | const id = req.params.conference_id; 600 | const name = req.body.participantName; 601 | const registration_id = generate_id(); 602 | change_state_http_wrapper(register_state_change,{ conference_id: id, registration_id: registration_id, name: name }, error_next, () => { res.redirect(`/register-success/${registration_id}`); }); 603 | }); // app.post("/register/:id") 604 | 605 | app.post("/close-registration", (_, r, error_next) => { 606 | change_state_http_wrapper(close_registration_state_change, {}, error_next, () => { r.redirect("/sessions"); }); 607 | }); 608 | 609 | app.get("/register-success/:registration_id", (req, res, error_next) => { 610 | const registration_id = req.params.registration_id; 611 | get_state_http_wrapper(registrations_state_view, error_next, (state) => { res.render("register-success", { conference_name: state.conference_name, registration_id: registration_id, name: state.registrations[registration_id] }); }); 612 | }); 613 | 614 | function registrations_state_view(history) { 615 | return history.reduce((acc, event) => { 616 | switch(event.meta.type) { 617 | case "conference_id_generated": 618 | acc.conference_id = event.conference_id; 619 | acc.registrations = {}; 620 | break; 621 | case "conference_named": 622 | acc.conference_name = event.name; 623 | break; 624 | case "registered": 625 | acc.registrations[event.registration_id] = event.name; 626 | break; 627 | default: break; } 628 | return acc; 629 | }, { conference_id: null, conference_name: "-- not named yet --", registrations: {} }); 630 | } // registration_state_view 631 | 632 | 633 | const error_registration_closed = new Error("Registration is closed."); 634 | const error_already_registered = new Error("You are already registered."); 635 | function register_state_change(history, command) { 636 | const registration_state = history.reduce((acc, event) => { 637 | switch(event.meta.type) { 638 | case "registration_closed": 639 | acc.conference_id = null; 640 | acc.names = new Set(); 641 | break; 642 | case "registered": 643 | if (acc.conference_id === null) break; // this should not happen 644 | acc.names.add(event.name); 645 | break; 646 | case "conference_id_generated": 647 | acc.conference_id = event.conference_id; 648 | acc.names = new Set(); 649 | break; 650 | default: 651 | break; 652 | } 653 | return acc; 654 | }, { conference_id: null, names: new Set() }); 655 | 656 | if ( registration_state.conference_id === null 657 | || registration_state.conference_id !== command.conference_id) throw error_registration_closed; 658 | if (registration_state.names?.has(command.name)) throw error_already_registered; 659 | 660 | return { 661 | name: command.name, 662 | registration_id: command.registration_id, 663 | conference_id: command.conference_id, 664 | meta: { type: "registered", summary: command.name + "," + command.registration_id } 665 | }; 666 | } // register_state_change 667 | 668 | slice_tests.push({ test_function: register_state_change, 669 | timelines: [ 670 | { 671 | timeline_name: "First Timeline", 672 | checkpoints: [ 673 | { 674 | exception: error_registration_closed, 675 | command: { 676 | name: "Adam", 677 | registration_id: "eeee-ffff-00000", 678 | conference_id: "1111-2222-3333" 679 | }, 680 | purpose: "Should reject registration when conference doesn't exist" 681 | }, 682 | { 683 | event: {conference_id: "1111-2222-3333", meta: { type: "conference_id_generated" }} 684 | }, 685 | { 686 | event: { 687 | name: "Adam", 688 | registration_id: "eeee-ffff-00000", 689 | conference_id: "1111-2222-3333", 690 | meta: { type: "registered" } 691 | }, 692 | command: { 693 | name: "Adam", 694 | registration_id: "eeee-ffff-00000", 695 | conference_id: "1111-2222-3333" 696 | }, 697 | purpose: "Should allow first registration" 698 | }, 699 | { 700 | exception: error_already_registered, 701 | command: { 702 | name: "Adam", 703 | registration_id: "cccc-dddd-1111", 704 | conference_id: "1111-2222-3333" 705 | }, 706 | purpose: "Should reject duplicate registration" 707 | }, 708 | { 709 | event: { 710 | conference_id: "1111-2222-3333", 711 | meta: { type: "registration_closed" } 712 | } 713 | }, 714 | { 715 | progress_marker: "A second conference is started" 716 | }, 717 | { 718 | event: {conference_id: "2222-3333-4444", meta: { type: "conference_id_generated" }} 719 | }, 720 | { 721 | exception: error_registration_closed, 722 | command: { 723 | name: "Adam", 724 | registration_id: "eeee-ffff-00000", 725 | conference_id: "1111-2222-3333" 726 | }, 727 | purpose: "Should reject registration for old conference" 728 | }, 729 | { 730 | event: { 731 | name: "Adam", 732 | registration_id: "aaaa-bbbb-00000", 733 | conference_id: "2222-3333-4444", 734 | meta: { type: "registered" } 735 | }, 736 | command: { 737 | name: "Adam", 738 | registration_id: "aaaa-bbbb-00000", 739 | conference_id: "2222-3333-4444" 740 | }, 741 | purpose: "Should allow registration for new conference" 742 | } 743 | ] 744 | }, 745 | { 746 | timeline_name: "Registration is closed", 747 | checkpoints: [ 748 | { 749 | event: { 750 | conference_id: "1111-2222-3333", 751 | meta: { type: "conference_id_generated" } 752 | } 753 | }, 754 | { 755 | event: { 756 | conference_id: "1111-2222-3333", 757 | meta: { type: "registration_closed" } 758 | } 759 | }, 760 | { 761 | exception: error_registration_closed, 762 | command: { 763 | name: "Adam", 764 | registration_id: "eeee-ffff-00000", 765 | conference_id: "1111-2222-3333" 766 | }, 767 | purpose: "Should reject registration when closed" 768 | } 769 | ] 770 | } 771 | ] 772 | }); // test: registration_state_change 773 | 774 | function close_registration_state_change(history, command) { 775 | const state = history.reduce((acc, event) => { 776 | switch(event.meta.type) { 777 | case "conference_id_generated": acc.closed = false; break; 778 | case "registration_closed": acc.closed = true; break; } 779 | return acc; 780 | }, { closed: true }); 781 | if (state.closed) throw new Error("Registration is already closed"); 782 | return { meta: { type: "registration_closed" } }; 783 | } // close_registration_state_change 784 | 785 | app.get("/topic-suggestion", (req, res, error_next) => { 786 | get_access_token_http_wrapper(req, error_next, (token) => { res.render("submit-session", { name: token.name, registration_id: token.registration_id }); }); 787 | }); // app.get("/topic-suggestion", (req, res) => { 788 | 789 | app.post("/topic-suggestion", multer().none(), (req, res, error_next) => { 790 | get_access_token_http_wrapper(req, error_next, (token) => { 791 | change_state_http_wrapper(submit_session, { 792 | topic: req.body.topic, 793 | facilitation: req.body.facilitation, 794 | registration_id: token.registration_id 795 | }, error_next, () => { res.redirect("/topics?registration_id=" + token.registration_id); }); 796 | }); 797 | }); // app.post("/topic-suggestion", (req, res) => { 798 | 799 | const error_session_already_submitted = new Error("A session with this topic has already been suggested"); 800 | function submit_session(events, command) { 801 | const existingTopics = events.reduce((acc, event) => { 802 | switch(event.meta.type) { 803 | case "unique_id_generated_event": acc.topics = new Set(); break; 804 | case "session_submitted_event": acc.topics.add(event.topic.toLowerCase()); break; 805 | } 806 | return acc; 807 | }, { topics: new Set() }).topics; 808 | 809 | if (existingTopics.has(command.topic.toLowerCase())) throw error_session_already_submitted; 810 | return { topic: command.topic, facilitation: command.facilitation, registration_id: command.registration_id, meta: { type: "session_submitted", summary: command.facilitation + "," + command.topic + "," + command.registration_id }}; 811 | } // function submit_session(events, command) 812 | 813 | app.get("/topics", (req, res, error_next) => { 814 | get_state_http_wrapper(topics_state_view, error_next, (state) => { res.render("topics", { topics: state, registration_id: req.query.registration_id }); }); 815 | }); // sessions 816 | 817 | function topics_state_view(history) { 818 | const state = history.reduce((acc, event) => { 819 | switch(event.meta.type) { 820 | case "conference_id_generated": 821 | acc.registrations = {}; 822 | acc.topics = []; 823 | break; 824 | case "registered": 825 | acc.registrations[event.registration_id] = event.name; 826 | break; 827 | case "session_submitted": 828 | try { 829 | acc.topics.push({ topic: event.topic, facilitation: event.facilitation, name: acc.registrations[event.registration_id] }); 830 | } catch (error) { console.log("Error adding topic: " + error.message); } 831 | break; 832 | default: break; 833 | } 834 | return acc; 835 | }, { registrations: {}, topics: [] }); 836 | return state.topics; 837 | } // topics_state_view 838 | 839 | app.get("/voting", (req, res, error_next) => { 840 | get_access_token_http_wrapper(req, error_next, (token) => { 841 | get_state_http_wrapper(voting_state_view, error_next, (state) => { res.render("voting", { sessions: state.map(topic => ({ ...topic, voted: topic.voters.includes(token.registration_id), votes: undefined })), registration_id: token.registration_id }); }); 842 | }); 843 | }); // voting 844 | 845 | function voting_state_view(history) { 846 | function default_state() { return { registrations: {}, topics: [] }; } 847 | const state = history.reduce((acc, event) => { 848 | switch(event.meta.type) { 849 | case "conference_id_generated": acc = default_state(); break; 850 | case "registered": acc.registrations[event.registration_id] = event.name; break; 851 | case "session_submitted": acc.topics.push({ topic: event.topic, facilitation: event.facilitation, name: acc.registrations[event.registration_id], votes: new Set() }); break; 852 | case "voted_for_sessions": 853 | acc.topics.forEach(topic => { topic.votes.delete(event.registration_id); }); 854 | event.topics.forEach(topic => { acc.topics.forEach(t => { if (t.topic === topic) t.votes.add(event.registration_id); }); }); 855 | break; 856 | default: break; 857 | } 858 | return acc; 859 | }, default_state()); 860 | return state.topics.map(topic => ( 861 | { topic: topic.topic, facilitation: topic.facilitation, name: topic.name, vote_count: topic.votes.size, voters: Array.from(topic.votes) })); 862 | } // voting_state_view 863 | 864 | // Custom error handler for 404s 865 | app.use((req, res, next) => { 866 | // skip favicon.ico requests 867 | if (req.path === "/favicon.ico") return; 868 | console.log("404 error handler: " + req.path); 869 | const err = new Error('Not Found'); 870 | err.status = 404; 871 | next(err); 872 | }); 873 | 874 | // Global error handler 875 | app.use((err, req, res, next) => { 876 | console.log("Error " + err.status + ", message: " + err.message); 877 | console.error(err.stack); 878 | const statusCode = err.status || 500; 879 | 880 | // Check if the request accepts HTML 881 | if (req.accepts('html')) { 882 | res.status(statusCode); 883 | res.render('error', { 884 | message: err.message || 'Something went wrong!', 885 | error: statusCode >= 500 ? { 886 | status: statusCode, 887 | stack: err.stack 888 | } : undefined, 889 | errorStylesheet: '' 890 | }); 891 | } else { 892 | // API error response 893 | res.status(statusCode).json({ 894 | error: { 895 | message: err.message || 'Something went wrong!', 896 | status: statusCode 897 | } 898 | }); 899 | } 900 | }); // Global error handler 901 | 902 | function assert(condition, message) { if (!condition) throw new Error(message); } 903 | function run_with_expected_error(command_handler, events, command) { 904 | let caught_error = null; 905 | try { 906 | command_handler(events, command); 907 | } catch (error) { 908 | console.log("Caught error: " + JSON.stringify(error, null, 2)); 909 | caught_error = error.message; 910 | } 911 | return caught_error; } 912 | function tests() { 913 | let summary = ""; 914 | console.log("🧪 Tests are running..."); 915 | slice_tests.forEach(slice => { 916 | const slice_name = slice.slice_name !== undefined ? slice.slice_name : slice.test_function.name.replaceAll("_", " "); 917 | summary += `🍰 Testing slice: ${slice_name}\n`; 918 | slice.timelines.forEach(timeline => { 919 | summary += ` ⏱️ Testing timeline: ${timeline.timeline_name}\n`; 920 | timeline.checkpoints.reduce((acc, checkpoint) => { 921 | summary += checkpoint.progress_marker ? ` 🦉 ${checkpoint.progress_marker}\n` : ''; 922 | if (checkpoint.purpose !== undefined || checkpoint.test !== undefined) { 923 | try { 924 | if (checkpoint.command) { // state change test 925 | if (checkpoint.event && !checkpoint.exception) { // testing success of a command 926 | const result = slice.test_function(acc.events, checkpoint.command); 927 | assert(JSON.stringify(strip_summary(result)) === JSON.stringify(strip_summary(checkpoint.event)), "Should be equal to " + JSON.stringify(strip_summary(checkpoint.event)) + " but was: " + JSON.stringify(strip_summary(result))); 928 | } else if (checkpoint.exception && !checkpoint.event) { // testing exception 929 | console.log("running exception test auto-runner"); 930 | const result = run_with_expected_error(slice.test_function, acc.events, checkpoint.command); 931 | assert(result !== null, "Should throw '" + checkpoint.exception.message + "' error but did not throw an exception"); 932 | assert(result === checkpoint.exception.message, "Should throw " + checkpoint.exception.message + " but threw: " + result); 933 | } else { // bad chckpoint structure 934 | console.log("bad checkpoint structure: command but no event/exception"); 935 | throw new Error("Bad checkpoint structure: command but no event/exception"); 936 | } 937 | } else if (checkpoint.state) { // state view test 938 | const result = slice.test_function(acc.events); 939 | assert (JSON.stringify(strip_summary(result)) === JSON.stringify(strip_summary(checkpoint.state)), "Should be equal to " + JSON.stringify(strip_summary(checkpoint.state)) + " but was: " + JSON.stringify(strip_summary(result))); 940 | } 941 | console.log("test passed"); 942 | summary += ` ✅ Test passed: ${checkpoint.purpose !== undefined ? checkpoint.purpose : checkpoint.test.name} \n`; 943 | 944 | } catch (error) { 945 | console.log("test failed"); 946 | summary += ` ❌ Test failed: ${checkpoint.purpose !== undefined ? checkpoint.purpose : checkpoint.test.name} due to: ${error.message}\n`; 947 | console.log("💥 Test failed in Slice '" + slice_name + "' with test '" + (checkpoint.test !== undefined ? checkpoint.test.name : "auto-runner") + "'"); 948 | console.error(error); 949 | } 950 | } 951 | if (checkpoint.event) acc.events.push(checkpoint.event); 952 | return acc; 953 | }, { events: []}); 954 | }); 955 | }); 956 | console.log("🧪 Tests are finished"); 957 | console.log("📊 Tests summary:"); 958 | console.log(summary); 959 | const failed = (summary.match(/^.*❌/gm) || []).length; 960 | const passed = (summary.match(/^.*✅/gm) || []).length; 961 | console.log("\x1b[" + (failed > 0 ? "91" : "92") + "m 🧪 Tests summary: Failed: " + failed + " Passed: " + passed + " \x1b[0m"); 962 | process.exit(0); 963 | } 964 | 965 | if (run_tests) tests(); 966 | else app.listen(port, () => { 967 | console.log("Server is running on port " + port + " click on http://localhost:" + port + "/"); 968 | app._router.stack 969 | .filter(r => r.route) // Filter out middleware and focus on routes 970 | .map(r => r.route) 971 | .reduce((acc, route) => { if (acc.find(r => r.path === route.path)) return acc; acc.push(route); return acc; }, []) 972 | .forEach(route => { 973 | console.log(` http://localhost:${port}${route.path}`); 974 | }); 975 | }); 976 | -------------------------------------------------------------------------------- /fake-events/sc-close-registration-fail/0000-unique_id_generated-conf_id:1111-2222-3333-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "conference_id": "1111-2222-3333", 3 | "meta": { 4 | "type": "unique_id_generated" 5 | } 6 | } -------------------------------------------------------------------------------- /fake-events/sc-close-registration-fail/0001-close_registration--event.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "type": "registration_closed" 4 | } 5 | } -------------------------------------------------------------------------------- /fake-events/sc-close-registration-success/0000-unique_id_generated-conf_id:1111-2222-3333-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "conference_id": "1111-2222-3333", 3 | "meta": { 4 | "type": "unique_id_generated" 5 | } 6 | } -------------------------------------------------------------------------------- /fake-events/sv-join-conference/0000-unique_id_requested--event.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "type": "conference_id_requested" 4 | } 5 | } -------------------------------------------------------------------------------- /fake-events/sv-join-conference/0001-unique_id_generated-conf_id:5555-6666-7777-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "conference_id": "5555-6666-7777", 3 | "meta": { 4 | "type": "conference_id_generated" 5 | } 6 | } -------------------------------------------------------------------------------- /fake-events/sv-register/0000-conference_named-Event Modeling Open Spaces-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Event Modeling Open Spaces", 3 | "meta": { 4 | "type": "conference_named" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fake-events/sv-register/0001-conference_id_generated-1234-5678-9012-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "conference_id": "1234-5678-9012", 3 | "meta": { 4 | "type": "conference_id_generated" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fake-events/sv-register/0002-registered-John Smith,abcd-efgh-ijkl-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "abcd-efgh-ijkl", 3 | "name": "John Smith", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-registration-name-for-suggestion/0000-unique_id_generated--event.json: -------------------------------------------------------------------------------- 1 | { 2 | "conference_id": "8888-9999-0000", 3 | "meta": { 4 | "type": "unique_id_generated" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fake-events/sv-registration-name-for-suggestion/0001-registered-Alice Johnson,REG-001-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-001", 3 | "name": "Alice Johnson", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-registration-name-for-suggestion/0002-registered-Bob Smith,REG-002-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-002", 3 | "name": "Bob Smith", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-registration-name-for-suggestion/0003-registered-Carol Davis,REG-003-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-003", 3 | "name": "Carol Davis", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-rooms/0000-room_added-Auditorium-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "room_name": "Auditorium", 3 | "meta": { 4 | "type": "room_added" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fake-events/sv-rooms/0001-room_added-CS100-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "room_name": "CS100", 3 | "meta": { 4 | "type": "room_added" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fake-events/sv-rooms/0002-room_renamed-oldname:Auditorium,newname:Main Hall-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "old_name": "Auditorium", 3 | "new_name": "Main Hall", 4 | "meta": { 5 | "type": "room_renamed" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-rooms/0003-room_added-CS200-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "room_name": "CS200", 3 | "meta": { 4 | "type": "room_added" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fake-events/sv-rooms/0004-room_deleted-CS100-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "room_name": "CS100", 3 | "meta": { 4 | "type": "room_deleted" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /fake-events/sv-set-dates/0000-conference_dates_set-start_date2024-06-06,end_date:2024-06-07-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_date": "2024-06-06", 3 | "end_date": "2024-06-07", 4 | "meta": { 5 | "type": "conference_dates_set" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-time-slots/0000-time_slot_added-1st_session-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_time": "09:30", 3 | "end_time": "10:25", 4 | "name": "1st Session", 5 | "meta": { 6 | "type": "time_slot_added" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-time-slots/0001-time_slot_added-2nd_session-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_time": "10:30", 3 | "end_time": "11:25", 4 | "name": "2nd Session", 5 | "meta": { 6 | "type": "time_slot_added" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0000-registered-Alice Johnson,REG-001-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-001", 3 | "name": "Alice Johnson", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0001-session_submitted-Workshop:Event Storming Basics,REG-001-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Event Storming Basics", 3 | "facilitation": "Workshop", 4 | "registration_id": "REG-001", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0002-registered-Bob Smith,REG-002-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-002", 3 | "name": "Bob Smith", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0003-session_submitted-Discussion:Domain-Driven Design Patterns,REG-002-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Domain-Driven Design Patterns", 3 | "facilitation": "Discussion", 4 | "registration_id": "REG-002", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0004-session_submitted-Presentation:CQRS Implementation Strategies,REG-001-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "CQRS Implementation Strategies", 3 | "facilitation": "Presentation", 4 | "registration_id": "REG-001", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0005-registered-Carol Williams,REG-003-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-003", 3 | "name": "Carol Williams", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0006-session_submitted-Workshop:Event Sourcing Best Practices,REG-003-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Event Sourcing Best Practices", 3 | "facilitation": "Workshop", 4 | "registration_id": "REG-003", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0007-session_submitted-Discussion:Microservices Communication Patterns,REG-002-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Microservices Communication Patterns", 3 | "facilitation": "Discussion", 4 | "registration_id": "REG-002", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-topics/0008-session_submitted-Workshop:Testing Event-Driven Systems,REG-003-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Testing Event-Driven Systems", 3 | "facilitation": "Workshop", 4 | "registration_id": "REG-003", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /fake-events/sv-voting/0000-conference_id_generated-8765-4321-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "conference_id": "8765-4321", 3 | "meta": { 4 | "type": "conference_id_generated" 5 | } 6 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0001-registered-Alice Johnson,REG-001-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-001", 3 | "name": "Alice Johnson", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0002-registered-Bob Smith,REG-002-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-002", 3 | "name": "Bob Smith", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0003-registered-Carol Williams,REG-003-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-003", 3 | "name": "Carol Williams", 4 | "meta": { 5 | "type": "registered" 6 | } 7 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0004-session_submitted-Workshop,Event Modeling Basics,REG-001-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Event Modeling Basics", 3 | "facilitation": "Workshop", 4 | "registration_id": "REG-001", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0005-session_submitted-Discussion,Domain-Driven Design Patterns,REG-002-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Domain-Driven Design Patterns", 3 | "facilitation": "Discussion", 4 | "registration_id": "REG-002", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0006-session_submitted-Presentation,CQRS Implementation Strategies,REG-001-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "CQRS Implementation Strategies", 3 | "facilitation": "Presentation", 4 | "registration_id": "REG-001", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0007-session_submitted-Workshop,Event Sourcing Best Practices,REG-003-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "topic": "Event Sourcing Best Practices", 3 | "facilitation": "Workshop", 4 | "registration_id": "REG-003", 5 | "meta": { 6 | "type": "session_submitted" 7 | } 8 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0008-voted_for_sessions-REG-001,Event Modeling Basics;Domain-Driven Design Patterns-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-001", 3 | "topics": ["Event Modeling Basics", "Domain-Driven Design Patterns"], 4 | "meta": { 5 | "type": "voted_for_sessions" 6 | } 7 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0009-voted_for_sessions-REG-002,Event Modeling Basics;CQRS Implementation Strategies-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-002", 3 | "topics": ["Event Modeling Basics", "CQRS Implementation Strategies"], 4 | "meta": { 5 | "type": "voted_for_sessions" 6 | } 7 | } -------------------------------------------------------------------------------- /fake-events/sv-voting/0010-voted_for_sessions-REG-003,Domain-Driven Design Patterns;Event Sourcing Best Practices-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "registration_id": "REG-003", 3 | "topics": ["Domain-Driven Design Patterns", "Event Sourcing Best Practices"], 4 | "meta": { 5 | "type": "voted_for_sessions" 6 | } 7 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-spaces-comocamp", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "express": "^4.21.1", 9 | "multer": "^1.4.5-lts.1", 10 | "mustache-express": "^1.3.2", 11 | "uuid": "^11.0.3" 12 | } 13 | }, 14 | "node_modules/accepts": { 15 | "version": "1.3.8", 16 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 17 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 18 | "license": "MIT", 19 | "dependencies": { 20 | "mime-types": "~2.1.34", 21 | "negotiator": "0.6.3" 22 | }, 23 | "engines": { 24 | "node": ">= 0.6" 25 | } 26 | }, 27 | "node_modules/append-field": { 28 | "version": "1.0.0", 29 | "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", 30 | "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", 31 | "license": "MIT" 32 | }, 33 | "node_modules/array-flatten": { 34 | "version": "1.1.1", 35 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 36 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 37 | "license": "MIT" 38 | }, 39 | "node_modules/async": { 40 | "version": "3.2.6", 41 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", 42 | "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", 43 | "license": "MIT" 44 | }, 45 | "node_modules/body-parser": { 46 | "version": "1.20.3", 47 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 48 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 49 | "license": "MIT", 50 | "dependencies": { 51 | "bytes": "3.1.2", 52 | "content-type": "~1.0.5", 53 | "debug": "2.6.9", 54 | "depd": "2.0.0", 55 | "destroy": "1.2.0", 56 | "http-errors": "2.0.0", 57 | "iconv-lite": "0.4.24", 58 | "on-finished": "2.4.1", 59 | "qs": "6.13.0", 60 | "raw-body": "2.5.2", 61 | "type-is": "~1.6.18", 62 | "unpipe": "1.0.0" 63 | }, 64 | "engines": { 65 | "node": ">= 0.8", 66 | "npm": "1.2.8000 || >= 1.4.16" 67 | } 68 | }, 69 | "node_modules/buffer-from": { 70 | "version": "1.1.2", 71 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 72 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", 73 | "license": "MIT" 74 | }, 75 | "node_modules/busboy": { 76 | "version": "1.6.0", 77 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 78 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 79 | "dependencies": { 80 | "streamsearch": "^1.1.0" 81 | }, 82 | "engines": { 83 | "node": ">=10.16.0" 84 | } 85 | }, 86 | "node_modules/bytes": { 87 | "version": "3.1.2", 88 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 89 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 90 | "license": "MIT", 91 | "engines": { 92 | "node": ">= 0.8" 93 | } 94 | }, 95 | "node_modules/call-bind": { 96 | "version": "1.0.7", 97 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", 98 | "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", 99 | "license": "MIT", 100 | "dependencies": { 101 | "es-define-property": "^1.0.0", 102 | "es-errors": "^1.3.0", 103 | "function-bind": "^1.1.2", 104 | "get-intrinsic": "^1.2.4", 105 | "set-function-length": "^1.2.1" 106 | }, 107 | "engines": { 108 | "node": ">= 0.4" 109 | }, 110 | "funding": { 111 | "url": "https://github.com/sponsors/ljharb" 112 | } 113 | }, 114 | "node_modules/concat-stream": { 115 | "version": "1.6.2", 116 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 117 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 118 | "engines": [ 119 | "node >= 0.8" 120 | ], 121 | "license": "MIT", 122 | "dependencies": { 123 | "buffer-from": "^1.0.0", 124 | "inherits": "^2.0.3", 125 | "readable-stream": "^2.2.2", 126 | "typedarray": "^0.0.6" 127 | } 128 | }, 129 | "node_modules/content-disposition": { 130 | "version": "0.5.4", 131 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 132 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 133 | "license": "MIT", 134 | "dependencies": { 135 | "safe-buffer": "5.2.1" 136 | }, 137 | "engines": { 138 | "node": ">= 0.6" 139 | } 140 | }, 141 | "node_modules/content-type": { 142 | "version": "1.0.5", 143 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 144 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 145 | "license": "MIT", 146 | "engines": { 147 | "node": ">= 0.6" 148 | } 149 | }, 150 | "node_modules/cookie": { 151 | "version": "0.7.1", 152 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 153 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 154 | "license": "MIT", 155 | "engines": { 156 | "node": ">= 0.6" 157 | } 158 | }, 159 | "node_modules/cookie-signature": { 160 | "version": "1.0.6", 161 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 162 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 163 | "license": "MIT" 164 | }, 165 | "node_modules/core-util-is": { 166 | "version": "1.0.3", 167 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 168 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", 169 | "license": "MIT" 170 | }, 171 | "node_modules/debug": { 172 | "version": "2.6.9", 173 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 174 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 175 | "license": "MIT", 176 | "dependencies": { 177 | "ms": "2.0.0" 178 | } 179 | }, 180 | "node_modules/define-data-property": { 181 | "version": "1.1.4", 182 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", 183 | "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", 184 | "license": "MIT", 185 | "dependencies": { 186 | "es-define-property": "^1.0.0", 187 | "es-errors": "^1.3.0", 188 | "gopd": "^1.0.1" 189 | }, 190 | "engines": { 191 | "node": ">= 0.4" 192 | }, 193 | "funding": { 194 | "url": "https://github.com/sponsors/ljharb" 195 | } 196 | }, 197 | "node_modules/depd": { 198 | "version": "2.0.0", 199 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 200 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 201 | "license": "MIT", 202 | "engines": { 203 | "node": ">= 0.8" 204 | } 205 | }, 206 | "node_modules/destroy": { 207 | "version": "1.2.0", 208 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 209 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 210 | "license": "MIT", 211 | "engines": { 212 | "node": ">= 0.8", 213 | "npm": "1.2.8000 || >= 1.4.16" 214 | } 215 | }, 216 | "node_modules/ee-first": { 217 | "version": "1.1.1", 218 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 219 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 220 | "license": "MIT" 221 | }, 222 | "node_modules/encodeurl": { 223 | "version": "2.0.0", 224 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 225 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 226 | "license": "MIT", 227 | "engines": { 228 | "node": ">= 0.8" 229 | } 230 | }, 231 | "node_modules/es-define-property": { 232 | "version": "1.0.0", 233 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", 234 | "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", 235 | "license": "MIT", 236 | "dependencies": { 237 | "get-intrinsic": "^1.2.4" 238 | }, 239 | "engines": { 240 | "node": ">= 0.4" 241 | } 242 | }, 243 | "node_modules/es-errors": { 244 | "version": "1.3.0", 245 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 246 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 247 | "license": "MIT", 248 | "engines": { 249 | "node": ">= 0.4" 250 | } 251 | }, 252 | "node_modules/escape-html": { 253 | "version": "1.0.3", 254 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 255 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 256 | "license": "MIT" 257 | }, 258 | "node_modules/etag": { 259 | "version": "1.8.1", 260 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 261 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 262 | "license": "MIT", 263 | "engines": { 264 | "node": ">= 0.6" 265 | } 266 | }, 267 | "node_modules/express": { 268 | "version": "4.21.1", 269 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", 270 | "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", 271 | "license": "MIT", 272 | "dependencies": { 273 | "accepts": "~1.3.8", 274 | "array-flatten": "1.1.1", 275 | "body-parser": "1.20.3", 276 | "content-disposition": "0.5.4", 277 | "content-type": "~1.0.4", 278 | "cookie": "0.7.1", 279 | "cookie-signature": "1.0.6", 280 | "debug": "2.6.9", 281 | "depd": "2.0.0", 282 | "encodeurl": "~2.0.0", 283 | "escape-html": "~1.0.3", 284 | "etag": "~1.8.1", 285 | "finalhandler": "1.3.1", 286 | "fresh": "0.5.2", 287 | "http-errors": "2.0.0", 288 | "merge-descriptors": "1.0.3", 289 | "methods": "~1.1.2", 290 | "on-finished": "2.4.1", 291 | "parseurl": "~1.3.3", 292 | "path-to-regexp": "0.1.10", 293 | "proxy-addr": "~2.0.7", 294 | "qs": "6.13.0", 295 | "range-parser": "~1.2.1", 296 | "safe-buffer": "5.2.1", 297 | "send": "0.19.0", 298 | "serve-static": "1.16.2", 299 | "setprototypeof": "1.2.0", 300 | "statuses": "2.0.1", 301 | "type-is": "~1.6.18", 302 | "utils-merge": "1.0.1", 303 | "vary": "~1.1.2" 304 | }, 305 | "engines": { 306 | "node": ">= 0.10.0" 307 | } 308 | }, 309 | "node_modules/finalhandler": { 310 | "version": "1.3.1", 311 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 312 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 313 | "license": "MIT", 314 | "dependencies": { 315 | "debug": "2.6.9", 316 | "encodeurl": "~2.0.0", 317 | "escape-html": "~1.0.3", 318 | "on-finished": "2.4.1", 319 | "parseurl": "~1.3.3", 320 | "statuses": "2.0.1", 321 | "unpipe": "~1.0.0" 322 | }, 323 | "engines": { 324 | "node": ">= 0.8" 325 | } 326 | }, 327 | "node_modules/forwarded": { 328 | "version": "0.2.0", 329 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 330 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 331 | "license": "MIT", 332 | "engines": { 333 | "node": ">= 0.6" 334 | } 335 | }, 336 | "node_modules/fresh": { 337 | "version": "0.5.2", 338 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 339 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 340 | "license": "MIT", 341 | "engines": { 342 | "node": ">= 0.6" 343 | } 344 | }, 345 | "node_modules/function-bind": { 346 | "version": "1.1.2", 347 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 348 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 349 | "license": "MIT", 350 | "funding": { 351 | "url": "https://github.com/sponsors/ljharb" 352 | } 353 | }, 354 | "node_modules/get-intrinsic": { 355 | "version": "1.2.4", 356 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", 357 | "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", 358 | "license": "MIT", 359 | "dependencies": { 360 | "es-errors": "^1.3.0", 361 | "function-bind": "^1.1.2", 362 | "has-proto": "^1.0.1", 363 | "has-symbols": "^1.0.3", 364 | "hasown": "^2.0.0" 365 | }, 366 | "engines": { 367 | "node": ">= 0.4" 368 | }, 369 | "funding": { 370 | "url": "https://github.com/sponsors/ljharb" 371 | } 372 | }, 373 | "node_modules/gopd": { 374 | "version": "1.0.1", 375 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 376 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 377 | "license": "MIT", 378 | "dependencies": { 379 | "get-intrinsic": "^1.1.3" 380 | }, 381 | "funding": { 382 | "url": "https://github.com/sponsors/ljharb" 383 | } 384 | }, 385 | "node_modules/has-property-descriptors": { 386 | "version": "1.0.2", 387 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", 388 | "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", 389 | "license": "MIT", 390 | "dependencies": { 391 | "es-define-property": "^1.0.0" 392 | }, 393 | "funding": { 394 | "url": "https://github.com/sponsors/ljharb" 395 | } 396 | }, 397 | "node_modules/has-proto": { 398 | "version": "1.0.3", 399 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", 400 | "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", 401 | "license": "MIT", 402 | "engines": { 403 | "node": ">= 0.4" 404 | }, 405 | "funding": { 406 | "url": "https://github.com/sponsors/ljharb" 407 | } 408 | }, 409 | "node_modules/has-symbols": { 410 | "version": "1.0.3", 411 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 412 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 413 | "license": "MIT", 414 | "engines": { 415 | "node": ">= 0.4" 416 | }, 417 | "funding": { 418 | "url": "https://github.com/sponsors/ljharb" 419 | } 420 | }, 421 | "node_modules/hasown": { 422 | "version": "2.0.2", 423 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 424 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 425 | "license": "MIT", 426 | "dependencies": { 427 | "function-bind": "^1.1.2" 428 | }, 429 | "engines": { 430 | "node": ">= 0.4" 431 | } 432 | }, 433 | "node_modules/http-errors": { 434 | "version": "2.0.0", 435 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 436 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 437 | "license": "MIT", 438 | "dependencies": { 439 | "depd": "2.0.0", 440 | "inherits": "2.0.4", 441 | "setprototypeof": "1.2.0", 442 | "statuses": "2.0.1", 443 | "toidentifier": "1.0.1" 444 | }, 445 | "engines": { 446 | "node": ">= 0.8" 447 | } 448 | }, 449 | "node_modules/iconv-lite": { 450 | "version": "0.4.24", 451 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 452 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 453 | "license": "MIT", 454 | "dependencies": { 455 | "safer-buffer": ">= 2.1.2 < 3" 456 | }, 457 | "engines": { 458 | "node": ">=0.10.0" 459 | } 460 | }, 461 | "node_modules/inherits": { 462 | "version": "2.0.4", 463 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 464 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 465 | "license": "ISC" 466 | }, 467 | "node_modules/ipaddr.js": { 468 | "version": "1.9.1", 469 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 470 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 471 | "license": "MIT", 472 | "engines": { 473 | "node": ">= 0.10" 474 | } 475 | }, 476 | "node_modules/isarray": { 477 | "version": "1.0.0", 478 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 479 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", 480 | "license": "MIT" 481 | }, 482 | "node_modules/lru-cache": { 483 | "version": "5.1.1", 484 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", 485 | "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", 486 | "license": "ISC", 487 | "dependencies": { 488 | "yallist": "^3.0.2" 489 | } 490 | }, 491 | "node_modules/media-typer": { 492 | "version": "0.3.0", 493 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 494 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 495 | "license": "MIT", 496 | "engines": { 497 | "node": ">= 0.6" 498 | } 499 | }, 500 | "node_modules/merge-descriptors": { 501 | "version": "1.0.3", 502 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 503 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 504 | "license": "MIT", 505 | "funding": { 506 | "url": "https://github.com/sponsors/sindresorhus" 507 | } 508 | }, 509 | "node_modules/methods": { 510 | "version": "1.1.2", 511 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 512 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 513 | "license": "MIT", 514 | "engines": { 515 | "node": ">= 0.6" 516 | } 517 | }, 518 | "node_modules/mime": { 519 | "version": "1.6.0", 520 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 521 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 522 | "license": "MIT", 523 | "bin": { 524 | "mime": "cli.js" 525 | }, 526 | "engines": { 527 | "node": ">=4" 528 | } 529 | }, 530 | "node_modules/mime-db": { 531 | "version": "1.52.0", 532 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 533 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 534 | "license": "MIT", 535 | "engines": { 536 | "node": ">= 0.6" 537 | } 538 | }, 539 | "node_modules/mime-types": { 540 | "version": "2.1.35", 541 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 542 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 543 | "license": "MIT", 544 | "dependencies": { 545 | "mime-db": "1.52.0" 546 | }, 547 | "engines": { 548 | "node": ">= 0.6" 549 | } 550 | }, 551 | "node_modules/minimist": { 552 | "version": "1.2.8", 553 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 554 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 555 | "license": "MIT", 556 | "funding": { 557 | "url": "https://github.com/sponsors/ljharb" 558 | } 559 | }, 560 | "node_modules/mkdirp": { 561 | "version": "0.5.6", 562 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 563 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 564 | "license": "MIT", 565 | "dependencies": { 566 | "minimist": "^1.2.6" 567 | }, 568 | "bin": { 569 | "mkdirp": "bin/cmd.js" 570 | } 571 | }, 572 | "node_modules/ms": { 573 | "version": "2.0.0", 574 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 575 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 576 | "license": "MIT" 577 | }, 578 | "node_modules/multer": { 579 | "version": "1.4.5-lts.1", 580 | "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", 581 | "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", 582 | "license": "MIT", 583 | "dependencies": { 584 | "append-field": "^1.0.0", 585 | "busboy": "^1.0.0", 586 | "concat-stream": "^1.5.2", 587 | "mkdirp": "^0.5.4", 588 | "object-assign": "^4.1.1", 589 | "type-is": "^1.6.4", 590 | "xtend": "^4.0.0" 591 | }, 592 | "engines": { 593 | "node": ">= 6.0.0" 594 | } 595 | }, 596 | "node_modules/mustache": { 597 | "version": "4.2.0", 598 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 599 | "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 600 | "license": "MIT", 601 | "bin": { 602 | "mustache": "bin/mustache" 603 | } 604 | }, 605 | "node_modules/mustache-express": { 606 | "version": "1.3.2", 607 | "resolved": "https://registry.npmjs.org/mustache-express/-/mustache-express-1.3.2.tgz", 608 | "integrity": "sha512-yAdGHctEq9ubUH7h9O6Z6kW35fwfE+7LpW/TBrcfVibZuiVRHDcK8DEydgiU5nlJmJUY5VC3jv2lzaPUL+Arkw==", 609 | "license": "MIT", 610 | "dependencies": { 611 | "async": "~3.2.0", 612 | "lru-cache": "~5.1.1", 613 | "mustache": "^4.2.0" 614 | }, 615 | "engines": { 616 | "node": ">= 0.8.0" 617 | } 618 | }, 619 | "node_modules/negotiator": { 620 | "version": "0.6.3", 621 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 622 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 623 | "license": "MIT", 624 | "engines": { 625 | "node": ">= 0.6" 626 | } 627 | }, 628 | "node_modules/object-assign": { 629 | "version": "4.1.1", 630 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 631 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 632 | "license": "MIT", 633 | "engines": { 634 | "node": ">=0.10.0" 635 | } 636 | }, 637 | "node_modules/object-inspect": { 638 | "version": "1.13.3", 639 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", 640 | "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", 641 | "license": "MIT", 642 | "engines": { 643 | "node": ">= 0.4" 644 | }, 645 | "funding": { 646 | "url": "https://github.com/sponsors/ljharb" 647 | } 648 | }, 649 | "node_modules/on-finished": { 650 | "version": "2.4.1", 651 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 652 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 653 | "license": "MIT", 654 | "dependencies": { 655 | "ee-first": "1.1.1" 656 | }, 657 | "engines": { 658 | "node": ">= 0.8" 659 | } 660 | }, 661 | "node_modules/parseurl": { 662 | "version": "1.3.3", 663 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 664 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 665 | "license": "MIT", 666 | "engines": { 667 | "node": ">= 0.8" 668 | } 669 | }, 670 | "node_modules/path-to-regexp": { 671 | "version": "0.1.10", 672 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", 673 | "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", 674 | "license": "MIT" 675 | }, 676 | "node_modules/process-nextick-args": { 677 | "version": "2.0.1", 678 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 679 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 680 | "license": "MIT" 681 | }, 682 | "node_modules/proxy-addr": { 683 | "version": "2.0.7", 684 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 685 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 686 | "license": "MIT", 687 | "dependencies": { 688 | "forwarded": "0.2.0", 689 | "ipaddr.js": "1.9.1" 690 | }, 691 | "engines": { 692 | "node": ">= 0.10" 693 | } 694 | }, 695 | "node_modules/qs": { 696 | "version": "6.13.0", 697 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 698 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 699 | "license": "BSD-3-Clause", 700 | "dependencies": { 701 | "side-channel": "^1.0.6" 702 | }, 703 | "engines": { 704 | "node": ">=0.6" 705 | }, 706 | "funding": { 707 | "url": "https://github.com/sponsors/ljharb" 708 | } 709 | }, 710 | "node_modules/range-parser": { 711 | "version": "1.2.1", 712 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 713 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 714 | "license": "MIT", 715 | "engines": { 716 | "node": ">= 0.6" 717 | } 718 | }, 719 | "node_modules/raw-body": { 720 | "version": "2.5.2", 721 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 722 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 723 | "license": "MIT", 724 | "dependencies": { 725 | "bytes": "3.1.2", 726 | "http-errors": "2.0.0", 727 | "iconv-lite": "0.4.24", 728 | "unpipe": "1.0.0" 729 | }, 730 | "engines": { 731 | "node": ">= 0.8" 732 | } 733 | }, 734 | "node_modules/readable-stream": { 735 | "version": "2.3.8", 736 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 737 | "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 738 | "license": "MIT", 739 | "dependencies": { 740 | "core-util-is": "~1.0.0", 741 | "inherits": "~2.0.3", 742 | "isarray": "~1.0.0", 743 | "process-nextick-args": "~2.0.0", 744 | "safe-buffer": "~5.1.1", 745 | "string_decoder": "~1.1.1", 746 | "util-deprecate": "~1.0.1" 747 | } 748 | }, 749 | "node_modules/readable-stream/node_modules/safe-buffer": { 750 | "version": "5.1.2", 751 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 752 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 753 | "license": "MIT" 754 | }, 755 | "node_modules/safe-buffer": { 756 | "version": "5.2.1", 757 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 758 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 759 | "funding": [ 760 | { 761 | "type": "github", 762 | "url": "https://github.com/sponsors/feross" 763 | }, 764 | { 765 | "type": "patreon", 766 | "url": "https://www.patreon.com/feross" 767 | }, 768 | { 769 | "type": "consulting", 770 | "url": "https://feross.org/support" 771 | } 772 | ], 773 | "license": "MIT" 774 | }, 775 | "node_modules/safer-buffer": { 776 | "version": "2.1.2", 777 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 778 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 779 | "license": "MIT" 780 | }, 781 | "node_modules/send": { 782 | "version": "0.19.0", 783 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 784 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 785 | "license": "MIT", 786 | "dependencies": { 787 | "debug": "2.6.9", 788 | "depd": "2.0.0", 789 | "destroy": "1.2.0", 790 | "encodeurl": "~1.0.2", 791 | "escape-html": "~1.0.3", 792 | "etag": "~1.8.1", 793 | "fresh": "0.5.2", 794 | "http-errors": "2.0.0", 795 | "mime": "1.6.0", 796 | "ms": "2.1.3", 797 | "on-finished": "2.4.1", 798 | "range-parser": "~1.2.1", 799 | "statuses": "2.0.1" 800 | }, 801 | "engines": { 802 | "node": ">= 0.8.0" 803 | } 804 | }, 805 | "node_modules/send/node_modules/encodeurl": { 806 | "version": "1.0.2", 807 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 808 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 809 | "license": "MIT", 810 | "engines": { 811 | "node": ">= 0.8" 812 | } 813 | }, 814 | "node_modules/send/node_modules/ms": { 815 | "version": "2.1.3", 816 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 817 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 818 | "license": "MIT" 819 | }, 820 | "node_modules/serve-static": { 821 | "version": "1.16.2", 822 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 823 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 824 | "license": "MIT", 825 | "dependencies": { 826 | "encodeurl": "~2.0.0", 827 | "escape-html": "~1.0.3", 828 | "parseurl": "~1.3.3", 829 | "send": "0.19.0" 830 | }, 831 | "engines": { 832 | "node": ">= 0.8.0" 833 | } 834 | }, 835 | "node_modules/set-function-length": { 836 | "version": "1.2.2", 837 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", 838 | "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", 839 | "license": "MIT", 840 | "dependencies": { 841 | "define-data-property": "^1.1.4", 842 | "es-errors": "^1.3.0", 843 | "function-bind": "^1.1.2", 844 | "get-intrinsic": "^1.2.4", 845 | "gopd": "^1.0.1", 846 | "has-property-descriptors": "^1.0.2" 847 | }, 848 | "engines": { 849 | "node": ">= 0.4" 850 | } 851 | }, 852 | "node_modules/setprototypeof": { 853 | "version": "1.2.0", 854 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 855 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 856 | "license": "ISC" 857 | }, 858 | "node_modules/side-channel": { 859 | "version": "1.0.6", 860 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", 861 | "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", 862 | "license": "MIT", 863 | "dependencies": { 864 | "call-bind": "^1.0.7", 865 | "es-errors": "^1.3.0", 866 | "get-intrinsic": "^1.2.4", 867 | "object-inspect": "^1.13.1" 868 | }, 869 | "engines": { 870 | "node": ">= 0.4" 871 | }, 872 | "funding": { 873 | "url": "https://github.com/sponsors/ljharb" 874 | } 875 | }, 876 | "node_modules/statuses": { 877 | "version": "2.0.1", 878 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 879 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 880 | "license": "MIT", 881 | "engines": { 882 | "node": ">= 0.8" 883 | } 884 | }, 885 | "node_modules/streamsearch": { 886 | "version": "1.1.0", 887 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 888 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 889 | "engines": { 890 | "node": ">=10.0.0" 891 | } 892 | }, 893 | "node_modules/string_decoder": { 894 | "version": "1.1.1", 895 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 896 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 897 | "license": "MIT", 898 | "dependencies": { 899 | "safe-buffer": "~5.1.0" 900 | } 901 | }, 902 | "node_modules/string_decoder/node_modules/safe-buffer": { 903 | "version": "5.1.2", 904 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 905 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 906 | "license": "MIT" 907 | }, 908 | "node_modules/toidentifier": { 909 | "version": "1.0.1", 910 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 911 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 912 | "license": "MIT", 913 | "engines": { 914 | "node": ">=0.6" 915 | } 916 | }, 917 | "node_modules/type-is": { 918 | "version": "1.6.18", 919 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 920 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 921 | "license": "MIT", 922 | "dependencies": { 923 | "media-typer": "0.3.0", 924 | "mime-types": "~2.1.24" 925 | }, 926 | "engines": { 927 | "node": ">= 0.6" 928 | } 929 | }, 930 | "node_modules/typedarray": { 931 | "version": "0.0.6", 932 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 933 | "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", 934 | "license": "MIT" 935 | }, 936 | "node_modules/unpipe": { 937 | "version": "1.0.0", 938 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 939 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 940 | "license": "MIT", 941 | "engines": { 942 | "node": ">= 0.8" 943 | } 944 | }, 945 | "node_modules/util-deprecate": { 946 | "version": "1.0.2", 947 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 948 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 949 | "license": "MIT" 950 | }, 951 | "node_modules/utils-merge": { 952 | "version": "1.0.1", 953 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 954 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 955 | "license": "MIT", 956 | "engines": { 957 | "node": ">= 0.4.0" 958 | } 959 | }, 960 | "node_modules/uuid": { 961 | "version": "11.0.3", 962 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", 963 | "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", 964 | "funding": [ 965 | "https://github.com/sponsors/broofa", 966 | "https://github.com/sponsors/ctavan" 967 | ], 968 | "license": "MIT", 969 | "bin": { 970 | "uuid": "dist/esm/bin/uuid" 971 | } 972 | }, 973 | "node_modules/vary": { 974 | "version": "1.1.2", 975 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 976 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 977 | "license": "MIT", 978 | "engines": { 979 | "node": ">= 0.8" 980 | } 981 | }, 982 | "node_modules/xtend": { 983 | "version": "4.0.2", 984 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 985 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", 986 | "license": "MIT", 987 | "engines": { 988 | "node": ">=0.4" 989 | } 990 | }, 991 | "node_modules/yallist": { 992 | "version": "3.1.1", 993 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 994 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", 995 | "license": "ISC" 996 | } 997 | } 998 | } 999 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "express": "^4.21.1", 4 | "multer": "^1.4.5-lts.1", 5 | "mustache-express": "^1.3.2", 6 | "uuid": "^11.0.3" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /public/styles/error.css: -------------------------------------------------------------------------------- 1 | /* Inherit theme variables from main.css */ 2 | @import url('/styles/main.css'); 3 | 4 | /* Style the error page */ 5 | body { 6 | background-color: var(--color-background); 7 | color: var(--color-text); 8 | font-family: system-ui, -apple-system, sans-serif; 9 | margin: 0; 10 | padding: 2rem; 11 | } 12 | 13 | .container { 14 | max-width: 800px; 15 | margin: 0 auto; 16 | } 17 | 18 | pre { 19 | color: var(--color-text); 20 | background-color: var(--color-background-alt); 21 | padding: 1rem; 22 | border-radius: 4px; 23 | overflow-x: auto; 24 | } 25 | 26 | h1 { 27 | color: var(--color-primary); 28 | margin-bottom: 2rem; 29 | } -------------------------------------------------------------------------------- /public/styles/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Light theme */ 3 | --color-primary: #1a73e8; 4 | --color-secondary: #5f6368; 5 | --color-background: #ffffff; 6 | --color-surface: #f8f9fa; 7 | --color-text: #202124; 8 | --color-border: #dadce0; 9 | --color-active: #e8f0fe; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | :root { 14 | --color-primary: #8ab4f8; 15 | --color-secondary: #9aa0a6; 16 | --color-background: #202124; 17 | --color-surface: #292a2d; 18 | --color-text: #e8eaed; 19 | --color-border: #5f6368; 20 | --color-active: #3c4043; 21 | } 22 | } 23 | 24 | body { 25 | margin: 0; 26 | padding: 0; 27 | font-family: system-ui, -apple-system, sans-serif; 28 | background-color: var(--color-background); 29 | color: var(--color-text); 30 | line-height: 1.5; 31 | } 32 | 33 | .header { 34 | padding: 1rem; 35 | border-bottom: 1px solid var(--color-border); 36 | } 37 | 38 | .primary-nav, 39 | .secondary-nav { 40 | padding: 0.5rem 0; 41 | } 42 | 43 | .nav-item { 44 | padding: 0.5rem 1rem; 45 | cursor: pointer; 46 | } 47 | 48 | .nav-item.active { 49 | background-color: var(--color-active); 50 | border-radius: 4px; 51 | } 52 | 53 | .container { 54 | max-width: 800px; 55 | margin: 0 auto; 56 | padding: 2rem; 57 | } 58 | 59 | .form-section { 60 | background-color: var(--color-surface); 61 | padding: 2rem; 62 | border-radius: 8px; 63 | border: 1px solid var(--color-border); 64 | } 65 | 66 | .input-group { 67 | display: flex; 68 | gap: 1rem; 69 | margin-bottom: 1rem; 70 | } 71 | 72 | .input-field { 73 | flex: 1; 74 | padding: 0.5rem; 75 | border: 1px solid var(--color-border); 76 | border-radius: 4px; 77 | background-color: var(--color-background); 78 | color: var(--color-text); 79 | } 80 | 81 | .list { 82 | list-style: none; 83 | padding: 0; 84 | margin: 1rem 0; 85 | } 86 | 87 | .list-item { 88 | padding: 0.5rem; 89 | border: 1px solid var(--color-border); 90 | border-radius: 4px; 91 | margin-bottom: 0.5rem; 92 | background-color: var(--color-background); 93 | } 94 | 95 | .button-group { 96 | display: flex; 97 | justify-content: flex-end; 98 | gap: 1rem; 99 | margin-top: 2rem; 100 | } 101 | 102 | .btn { 103 | padding: 0.5rem 1rem; 104 | border: none; 105 | border-radius: 4px; 106 | cursor: pointer; 107 | font-weight: 500; 108 | } 109 | 110 | .btn-primary { 111 | background-color: var(--color-primary); 112 | color: white; 113 | } 114 | 115 | .btn-secondary { 116 | background-color: var(--color-secondary); 117 | color: white; 118 | } 119 | 120 | /* table styling with some background colour that goes well together with the rest of the theme. Adds a bit of padding and margin. 121 | an empty table should have a light grey background and the headers should stand out more */ 122 | .table { 123 | background-color: var(--color-surface); 124 | padding: 1rem; 125 | margin: 1rem 0; 126 | border: 1px solid var(--color-border); 127 | border-collapse: collapse; 128 | } 129 | 130 | .table th, 131 | .table td { 132 | border: 1px solid var(--color-border); 133 | padding: 0.75rem; 134 | } 135 | .table th { 136 | background-color: var(--color-surface); 137 | } 138 | 139 | .time-slots-table { 140 | width: 100%; 141 | border-collapse: collapse; 142 | margin: 1rem 0; 143 | } 144 | 145 | .time-slots-table th, 146 | .time-slots-table td { 147 | padding: 0.75rem; 148 | text-align: left; 149 | border-bottom: 1px solid var(--border-color); 150 | } 151 | 152 | .time-slots-table th { 153 | background-color: var(--secondary-color); 154 | color: var(--text-light); 155 | } 156 | 157 | .time-slots-table tbody tr:hover { 158 | background-color: var(--background-hover); 159 | } 160 | 161 | .empty-message { 162 | text-align: center; 163 | color: var(--text-muted); 164 | padding: 1rem; 165 | } 166 | 167 | .qr-section { 168 | display: flex; 169 | flex-direction: column; 170 | align-items: center; 171 | gap: 2rem; 172 | padding: 2rem; 173 | background-color: var(--color-surface); 174 | border-radius: 8px; 175 | border: 1px solid var(--color-border); 176 | } 177 | 178 | #qrcode { 179 | padding: 1rem; 180 | background: white; 181 | border-radius: 4px; 182 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 183 | } 184 | 185 | .conference-url { 186 | word-break: break-all; 187 | text-align: center; 188 | color: var(--color-primary); 189 | font-family: monospace; 190 | padding: 1rem; 191 | background-color: var(--color-background); 192 | border: 1px solid var(--color-border); 193 | border-radius: 4px; 194 | width: 100%; 195 | max-width: 500px; 196 | } 197 | -------------------------------------------------------------------------------- /scripts/events-abbreviations.fish: -------------------------------------------------------------------------------- 1 | # add abbreviations for events command in fish 2 | abbr -a ev events 3 | abbr -a evs events scenarios 4 | abbr -a evc events cat 5 | abbr -a evw events watch -------------------------------------------------------------------------------- /scripts/events-tabcomplete.fish: -------------------------------------------------------------------------------- 1 | # Tab completion for the events command 2 | complete -c events -f 3 | complete -c events -n "__fish_use_subcommand" -a "scenarios" -d "List available scenarios" 4 | complete -c events -n "__fish_use_subcommand" -a "cat" -d "Show event categories" 5 | complete -c events -n "__fish_use_subcommand" -a "watch" -d "Watch the event-stream for changes" 6 | # Complete directories under fake-events for the scenarios subcommand 7 | complete -c events -n "__fish_seen_subcommand_from scenarios" -a "(ls -d fake-events/* 2>/dev/null | string replace 'fake-events/' '')" -d "Scenario" 8 | # complete options for watch 9 | complete -c events -n "__fish_seen_subcommand_from watch; and not __fish_contains_opt A; and not string match -q '*--all*' (commandline -p)" -a "-A" -d "Show all event data" 10 | complete -c events -n "__fish_seen_subcommand_from watch; and not __fish_contains_opt A; and not string match -q '*--all*' (commandline -p)" -a "--all" -d "Show all event data" 11 | -------------------------------------------------------------------------------- /scripts/events.fish: -------------------------------------------------------------------------------- 1 | function events 2 | # Check if an argument was provided 3 | if test (count $argv) -eq 0 4 | echo "Usage: events [command]" 5 | echo "Available commands: scenarios, cat" 6 | return 1 7 | end 8 | 9 | switch $argv[1] 10 | case "scenarios" 11 | # copy the events from the scenario to the event-stream 12 | set dir fake-events/$argv[2] 13 | if test -d $dir 14 | rm -rf event-stream 15 | mkdir -p event-stream 16 | # if dir has no files, exit 17 | if test (find $dir -type f | wc -l) -eq 0 18 | return 0 19 | end 20 | cp $dir/* event-stream/ 21 | else 22 | echo "Scenario $argv[2] not found" 23 | return 1 24 | end 25 | case "cat" 26 | # Handle cat command 27 | echo "Showing events listed" 28 | # Add your categories logic here 29 | 30 | case "watch" 31 | # get options from arg 2 32 | set all_event_data false 33 | switch $argv[2] 34 | case "--all" "-A" 35 | set all_event_data true 36 | end 37 | 38 | # watch the event-stream for changes 39 | set last_processed "-1" 40 | while true 41 | # Get all files, sort them numerically, and process only new ones 42 | for file in (ls event-stream 2>/dev/null | sort) 43 | set file "event-stream/$file" 44 | set file_parts (string split -n "-" (basename $file)) 45 | set file_num $file_parts[1] 46 | set event_type $file_parts[2] 47 | if test -n "$file_num" -a "$file_num" -gt "$last_processed" 48 | echo -n "$file_num $event_type " 49 | if test $all_event_data = true 50 | cat $file | jq -C --compact-output 51 | else 52 | cat $file | jq -C --compact-output 'del(.meta)' 53 | end 54 | set last_processed $file_num 55 | end 56 | end 57 | sleep 1 58 | end 59 | case '*' 60 | # Handle invalid commands 61 | echo "Unknown command: $argv[1]" 62 | echo "Available commands: scenarios, cat" 63 | return 1 64 | end 65 | end 66 | 67 | -------------------------------------------------------------------------------- /scripts/make_fake_event.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # make a fake event 4 | # $1 is the sequence number 5 | echo "seq_num: $1" 6 | # $2 is the type of event 7 | echo "event_type: $2" 8 | # $3 is the summary 9 | echo "summary: $3" 10 | # $4 is the destination directory 11 | echo "destination directory: $4" 12 | 13 | # stdin is the data of the event 14 | 15 | 16 | # create a temporary file 17 | temp_file=$(mktemp) 18 | 19 | # read the data from stdin 20 | cat > $temp_file 21 | # use jq to get the meta data property called summary, if it doesn't exist, use empty string 22 | summary=$(jq -r '.meta.summary // ""' $temp_file) 23 | 24 | # create the file name --event.json 25 | file_name="${1}-${summary}-event.json" 26 | echo "file_name: $file_name" 27 | # move the temporary file to the file name 28 | mv $temp_file $4/$file_name 29 | -------------------------------------------------------------------------------- /scripts/update-fake-events.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # update the fake events 4 | # $1 is the directory name 5 | 6 | cd "$1" || exit 1 7 | 8 | # Use find with a while loop to properly handle filenames with spaces 9 | find . -name "*.json" -type f | sort | while read -r file; do 10 | #create a temp file 11 | temp_file=$(mktemp) 12 | echo "processing $file" 13 | # get the sequence number 14 | seq_num=$(basename "$file" | cut -d- -f1) 15 | echo "seq_num: $seq_num" 16 | # get the type of event 17 | event_type=$(basename "$file" | cut -d- -f2 | sed 's/_event$//') 18 | echo "event_type: $event_type" 19 | # get the summary, it's all the text after the second dash using -event.json at the end 20 | summary=$(basename "$file" | cut -d- -f3- | sed 's/event.json$//' | sed 's/_$//' | sed 's/-$//') 21 | echo "summary: $summary" 22 | 23 | # use jq to remove timestamp at root and type at root and make meta with the event type 24 | jq --arg event_type "$event_type" 'del(.timestamp) | .meta = { "type": $event_type } | del(.type)' "$file" > "$temp_file" 25 | echo "--------------------------------" 26 | newfilename="$seq_num-$event_type-$summary-event.json" 27 | echo "new filename: $newfilename" 28 | cat "$temp_file" 29 | echo "--------------------------------" 30 | 31 | rm "$file" 32 | mv "$temp_file" "$newfilename" 33 | #rm "$temp_file" 34 | 35 | done 36 | -------------------------------------------------------------------------------- /views/error.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Error - {{status}} 7 | {{{errorStylesheet}}} 8 | 9 | 10 |
11 |

{{message}}

12 | {{#error}} 13 |
{{stack}}
14 | {{/error}} 15 |
16 | 17 | -------------------------------------------------------------------------------- /views/generate-conf-id.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Create Conference URL and QR code 7 | 8 | 9 | 10 |
11 | 17 |
18 | 19 |
20 |

Create Conference URL and QR code

21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /views/join-conference.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Join Conference 7 | 8 | 9 | 10 | 11 |
12 | 18 | 25 |
26 | 27 |
28 |
29 |

Show this to the participants

30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 | 62 | 63 | -------------------------------------------------------------------------------- /views/register-success.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Registration Successful 7 | 8 | 9 | 10 |
11 | 16 |
17 |
18 |
19 |

Thank you, {{name}}

20 |

for registering at {{conference_name}}!

21 | 22 |
23 | Continue 24 |
25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /views/register.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Register for Open Spaces 7 | 8 | 9 | 10 |
11 | 16 |
17 | 18 |
19 |
20 | {{#not_found}} 21 |

Conference Not Found

22 |

This conference doesn't exist.

23 | {{/not_found}} 24 | 25 | {{^not_found}} 26 |

Welcome to {{ conference_name }}!

27 | 28 |
29 |
30 | 31 | 36 |
37 | 38 |
39 | 40 |
41 |
42 | {{/not_found}} 43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /views/rooms.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Room Setup 7 | 8 | 9 | 10 |
11 | 17 | 24 |
25 | 26 |
27 |

Add a room to your event

28 | 29 |
30 |

Rooms:

31 |
32 | 33 | 34 | 35 |
36 | 37 |
    38 | {{#rooms}} 39 |
  • {{.}}
  • 40 | {{/rooms}} 41 |
42 | 43 |
44 | Continue 45 |
46 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /views/set-conference-name-confirmation.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Name Confirmed 7 | 8 | 9 | 10 |
11 | 17 | 24 |
25 | 26 |
27 |

Name Set Successfully!

28 |
29 |

Congratulations!

30 |

Your event name has been set. You can now proceed to set up the dates for your event.

31 |
32 |

Event Name: {{ conference_name }}

33 |
34 |
35 | Continue to Dates 36 |
37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /views/set-conference-name.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Set Conference Name 7 | 8 | 9 | 10 |
11 | 17 | 24 |
25 | 26 |
27 |

What do you want to call your Open Spaces?

28 | 29 |
30 |
31 |
32 | 33 | 39 |
40 | 41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /views/set-dates-confirmation.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dates Set Successfully 7 | 8 | 9 | 10 |
11 | 17 | 24 |
25 | 26 |
27 |
28 |

Congratulations, you set the dates for your event

29 | 30 |
31 | 32 |
33 | {{start_date}} - {{end_date}} 34 |
35 |
36 | 37 |
38 | Continue 39 |
40 |
41 |
42 | 43 | 62 | 63 | -------------------------------------------------------------------------------- /views/set-dates.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Date Setup 7 | 8 | 9 | 10 |
11 | 17 | 24 |
25 | 26 |
27 |

Set the dates for your event

28 | 29 |
30 |
31 |
32 |
33 | 34 | 40 |
41 |
42 | 43 | 49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 |
58 | 59 | 98 | 99 | -------------------------------------------------------------------------------- /views/submit-session.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Submit Session Proposal 7 | 8 | 9 | 10 |
11 | 17 |
18 | 19 |
20 |

Submit Your Session Proposal

21 | 22 |
23 |
24 |
25 |
{{name}}
26 |
27 | 28 |
29 | 30 | 36 |
37 | 38 |
39 | 40 | 46 |
47 | 48 |
49 | 50 | Skip 51 |
52 |
53 |
54 |
55 | 56 | -------------------------------------------------------------------------------- /views/time-slots.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Time Slots Setup 7 | 8 | 9 | 10 |
11 | 17 | 24 |
25 | 26 |
27 |

Add a time slot

28 | 29 |
30 |

Time slots:

31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {{#time_slots}} 42 | 43 | 44 | 45 | 46 | 47 | {{/time_slots}} 48 | {{^time_slots}} 49 | 50 | 51 | 52 | {{/time_slots}} 53 | 54 |
StartEndName
{{start_time}}{{end_time}}{{name}}
No time slots added yet
55 |
56 | 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 | 70 |
71 |
72 | 73 |
74 | Continue 75 |
76 |
77 |
78 | 79 | -------------------------------------------------------------------------------- /views/todo-gen-conf-ids.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Conference IDs to Generate 7 | 8 | 9 | 10 | 11 |
12 |

Conference IDs to Generate

13 | 14 |
15 | 16 |
17 |

Generated IDs:

18 |
    19 | {{#conference_ids}} 20 |
  • 21 | {{#conference_id}} 22 | {{conference_id}} 23 | {{/conference_id}} 24 | {{^conference_id}} 25 | Pending generation... 26 | {{/conference_id}} 27 |
  • 28 | {{/conference_ids}} 29 |
30 |
31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /views/topics.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Current Sessions 7 | 8 | 9 | 10 |
11 | 17 |
18 | 19 |
20 |

Current Sessions

21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{#topics}} 33 | 34 | 35 | 36 | 37 | 38 | {{/topics}} 39 | {{^topics}} 40 | 41 | 42 | 43 | {{/topics}} 44 | 45 |
NameFacilitationTopic
{{name}}{{facilitation}}{{topic}}
No sessions proposed yet
46 | 47 | {{#registration_id}} 48 | 52 | {{/registration_id}} 53 |
54 |
55 | 56 | -------------------------------------------------------------------------------- /views/voting.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vote for Sessions 7 | 8 | 9 | 10 |
11 | 17 |
18 | 19 |
20 |

Vote on your favourite sessions:

21 | 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {{#sessions}} 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | {{/sessions}} 46 | {{^sessions}} 47 | 48 | 49 | 50 | {{/sessions}} 51 | 52 |
VotesName:Facilitation:Topic:Interested?
{{vote_count}}{{name}}{{facilitation}}{{topic}} 42 | 43 |
No sessions available for voting
53 | 54 |

- refresh the page to see what the current count is!

55 | 56 |
57 | 58 |
59 |
60 |
61 |
62 | 63 | --------------------------------------------------------------------------------